diff --git a/.github/workflows/conan-package-create.yml b/.github/workflows/conan-package-create.yml index 701a978cd8..e8329fa7b1 100644 --- a/.github/workflows/conan-package-create.yml +++ b/.github/workflows/conan-package-create.yml @@ -1,158 +1,153 @@ name: Create and Upload Conan package on: - workflow_call: - inputs: - project_name: - required: true - type: string + workflow_call: + inputs: + project_name: + required: true + type: string - recipe_id_full: - required: true - type: string + recipe_id_full: + required: true + type: string - build_id: - required: true - type: number + build_id: + required: true + type: number - build_info: - required: false - default: true - type: boolean + build_info: + required: false + default: true + type: boolean - recipe_id_latest: - required: false - type: string + recipe_id_latest: + required: false + type: string - runs_on: - required: true - type: string + runs_on: + required: true + type: string - python_version: - required: true - type: string + python_version: + required: true + type: string - conan_config_branch: - required: false - type: string + conan_config_branch: + required: false + type: string - conan_logging_level: - required: false - type: string + conan_logging_level: + required: false + type: string - conan_clean_local_cache: - required: false - type: boolean - default: false + conan_clean_local_cache: + required: false + type: boolean + default: false - conan_upload_community: - required: false - default: true - type: boolean + conan_upload_community: + required: false + default: true + type: boolean env: - CONAN_LOGIN_USERNAME: ${{ secrets.CONAN_USER }} - CONAN_PASSWORD: ${{ secrets.CONAN_PASS }} - CONAN_LOG_RUN_TO_OUTPUT: 1 - CONAN_LOGGING_LEVEL: ${{ inputs.conan_logging_level }} - CONAN_NON_INTERACTIVE: 1 + CONAN_LOGIN_USERNAME: ${{ secrets.CONAN_USER }} + CONAN_PASSWORD: ${{ secrets.CONAN_PASS }} + CONAN_LOG_RUN_TO_OUTPUT: 1 + CONAN_LOGGING_LEVEL: ${{ inputs.conan_logging_level }} + CONAN_NON_INTERACTIVE: 1 jobs: - conan-package-create: - runs-on: ${{ inputs.runs_on }} + conan-package-create: + runs-on: ${{ inputs.runs_on }} - steps: - - name: Checkout - uses: actions/checkout@v3 + steps: + - name: Checkout + uses: actions/checkout@v3 - - name: Setup Python and pip - uses: actions/setup-python@v4 - with: - python-version: ${{ inputs.python_version }} - cache: 'pip' - cache-dependency-path: .github/workflows/requirements-conan-package.txt + - name: Setup Python and pip + uses: actions/setup-python@v4 + with: + python-version: ${{ inputs.python_version }} + cache: 'pip' + cache-dependency-path: .github/workflows/requirements-conan-package.txt - - name: Install Python requirements for runner - run: pip install -r https://raw.githubusercontent.com/Ultimaker/Cura/main/.github/workflows/requirements-conan-package.txt - # Note the runner requirements are always installed from the main branch in the Ultimaker/Cura repo + - name: Install Python requirements for runner + run: pip install -r https://raw.githubusercontent.com/Ultimaker/Cura/main/.github/workflows/requirements-conan-package.txt + # Note the runner requirements are always installed from the main branch in the Ultimaker/Cura repo - - name: Use Conan download cache (Bash) - if: ${{ runner.os != 'Windows' }} - run: conan config set storage.download_cache="$HOME/.conan/conan_download_cache" + - name: Use Conan download cache (Bash) + if: ${{ runner.os != 'Windows' }} + run: conan config set storage.download_cache="$HOME/.conan/conan_download_cache" - - name: Use Conan download cache (Powershell) - if: ${{ runner.os == 'Windows' }} - run: conan config set storage.download_cache="C:\Users\runneradmin\.conan\conan_download_cache" + - name: Use Conan download cache (Powershell) + if: ${{ runner.os == 'Windows' }} + run: conan config set storage.download_cache="C:\Users\runneradmin\.conan\conan_download_cache" - - name: Cache Conan local repository packages (Bash) - uses: actions/cache@v3 - if: ${{ runner.os != 'Windows' }} - with: - path: | - $HOME/.conan/data - $HOME/.conan/conan_download_cache - key: conan-${{ inputs.runs_on }}-${{ runner.arch }}-create-cache + - name: Cache Conan local repository packages (Bash) + uses: actions/cache@v3 + if: ${{ runner.os != 'Windows' }} + with: + path: | + $HOME/.conan/data + $HOME/.conan/conan_download_cache + key: conan-${{ inputs.runs_on }}-${{ runner.arch }}-create-cache - - name: Cache Conan local repository packages (Powershell) - uses: actions/cache@v3 - if: ${{ runner.os == 'Windows' }} - with: - path: | - C:\Users\runneradmin\.conan\data - C:\.conan - C:\Users\runneradmin\.conan\conan_download_cache - key: conan-${{ inputs.runs_on }}-${{ runner.arch }}-create-cache + - name: Cache Conan local repository packages (Powershell) + uses: actions/cache@v3 + if: ${{ runner.os == 'Windows' }} + with: + path: | + C:\Users\runneradmin\.conan\data + C:\.conan + C:\Users\runneradmin\.conan\conan_download_cache + key: conan-${{ inputs.runs_on }}-${{ runner.arch }}-create-cache - - name: Install MacOS system requirements - if: ${{ runner.os == 'Macos' }} - run: brew install autoconf automake ninja + - name: Install MacOS system requirements + if: ${{ runner.os == 'Macos' }} + run: brew install autoconf automake ninja - # NOTE: Due to what are probably github issues, we have to remove the cache and reconfigure before the rest. - # This is maybe because grub caches the disk it uses last time, which is recreated each time. - - name: Install Linux system requirements - if: ${{ runner.os == 'Linux' }} - run: | - sudo rm /var/cache/debconf/config.dat - sudo dpkg --configure -a - sudo add-apt-repository ppa:ubuntu-toolchain-r/test -y - sudo apt update - sudo apt upgrade - sudo apt install build-essential checkinstall libegl-dev zlib1g-dev libssl-dev ninja-build autoconf libx11-dev libx11-xcb-dev libfontenc-dev libice-dev libsm-dev libxau-dev libxaw7-dev libxcomposite-dev libxcursor-dev libxdamage-dev libxdmcp-dev libxext-dev libxfixes-dev libxi-dev libxinerama-dev libxkbfile-dev libxmu-dev libxmuu-dev libxpm-dev libxrandr-dev libxrender-dev libxres-dev libxss-dev libxt-dev libxtst-dev libxv-dev libxvmc-dev libxxf86vm-dev xtrans-dev libxcb-render0-dev libxcb-render-util0-dev libxcb-xkb-dev libxcb-icccm4-dev libxcb-image0-dev libxcb-keysyms1-dev libxcb-randr0-dev libxcb-shape0-dev libxcb-sync-dev libxcb-xfixes0-dev libxcb-xinerama0-dev xkb-data libxcb-dri3-dev uuid-dev libxcb-util-dev libxkbcommon-x11-dev pkg-config flex bison -y + # NOTE: Due to what are probably github issues, we have to remove the cache and reconfigure before the rest. + # This is maybe because grub caches the disk it uses last time, which is recreated each time. + - name: Install Linux system requirements + if: ${{ runner.os == 'Linux' }} + run: | + sudo rm /var/cache/debconf/config.dat + sudo dpkg --configure -a + sudo add-apt-repository ppa:ubuntu-toolchain-r/test -y + sudo apt update + sudo apt upgrade + sudo apt install build-essential checkinstall libegl-dev zlib1g-dev libssl-dev ninja-build autoconf libx11-dev libx11-xcb-dev libfontenc-dev libice-dev libsm-dev libxau-dev libxaw7-dev libxcomposite-dev libxcursor-dev libxdamage-dev libxdmcp-dev libxext-dev libxfixes-dev libxi-dev libxinerama-dev libxkbfile-dev libxmu-dev libxmuu-dev libxpm-dev libxrandr-dev libxrender-dev libxres-dev libxss-dev libxt-dev libxtst-dev libxv-dev libxvmc-dev libxxf86vm-dev xtrans-dev libxcb-render0-dev libxcb-render-util0-dev libxcb-xkb-dev libxcb-icccm4-dev libxcb-image0-dev libxcb-keysyms1-dev libxcb-randr0-dev libxcb-shape0-dev libxcb-sync-dev libxcb-xfixes0-dev libxcb-xinerama0-dev xkb-data libxcb-dri3-dev uuid-dev libxcb-util-dev libxkbcommon-x11-dev pkg-config flex bison -y - - name: Install GCC-12 on ubuntu-22.04 - if: ${{ startsWith(inputs.runs_on, 'ubuntu-22.04') }} - run: | - sudo apt install g++-12 gcc-12 -y - sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-12 12 - sudo update-alternatives --install /usr/bin/g++ g++ /usr/bin/g++-12 12 + - name: Install GCC-132 on ubuntu + if: ${{ startsWith(inputs.runs_on, 'ubuntu') }} + run: | + sudo apt install g++-13 gcc-13 -y + sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-13 13 + sudo update-alternatives --install /usr/bin/g++ g++ /usr/bin/g++-13 13 - - name: Use GCC-10 on ubuntu-20.04 - if: ${{ startsWith(inputs.runs_on, 'ubuntu-20.04') }} - run: | - sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-10 10 - sudo update-alternatives --install /usr/bin/g++ g++ /usr/bin/g++-10 10 + - name: Create the default Conan profile + run: conan profile new default --detect - - name: Create the default Conan profile - run: conan profile new default --detect + - name: Get Conan configuration from branch + if: ${{ inputs.conan_config_branch != '' }} + run: conan config install https://github.com/Ultimaker/conan-config.git -a "-b ${{ inputs.conan_config_branch }}" - - name: Get Conan configuration from branch - if: ${{ inputs.conan_config_branch != '' }} - run: conan config install https://github.com/Ultimaker/conan-config.git -a "-b ${{ inputs.conan_config_branch }}" + - name: Get Conan configuration + run: | + conan config install https://github.com/Ultimaker/conan-config.git + conan config install https://github.com/Ultimaker/conan-config.git -a "-b runner/${{ runner.os }}/${{ runner.arch }}" - - name: Get Conan configuration - if: ${{ inputs.conan_config_branch == '' }} - run: conan config install https://github.com/Ultimaker/conan-config.git + - name: Add Cura private Artifactory remote + run: conan remote add cura-private https://ultimaker.jfrog.io/artifactory/api/conan/cura-private True - - name: Add Cura private Artifactory remote - run: conan remote add cura-private https://ultimaker.jfrog.io/artifactory/api/conan/cura-private True + - name: Create the Packages + run: conan install ${{ inputs.recipe_id_full }} --build=missing --update -c tools.build:skip_test=True - - name: Create the Packages - run: conan install ${{ inputs.recipe_id_full }} --build=missing --update + - name: Upload the Package(s) + if: ${{ always() && inputs.conan_upload_community }} + run: conan upload ${{ inputs.recipe_id_full }} -r cura --all -c - - name: Upload the Package(s) - if: ${{ always() && inputs.conan_upload_community }} - run: conan upload ${{ inputs.recipe_id_full }} -r cura --all -c - - - name: Upload the Package(s) to the private Artifactory - if: ${{ always() && ! inputs.conan_upload_community }} - run: conan upload ${{ inputs.recipe_id_full }} -r cura-private --all -c + - name: Upload the Package(s) to the private Artifactory + if: ${{ always() && ! inputs.conan_upload_community }} + run: conan upload ${{ inputs.recipe_id_full }} -r cura-private --all -c diff --git a/.github/workflows/conan-package.yml b/.github/workflows/conan-package.yml index 9949621251..34652de39b 100644 --- a/.github/workflows/conan-package.yml +++ b/.github/workflows/conan-package.yml @@ -49,15 +49,15 @@ on: - '[1-9].[0-9][0-9].[0-9]*' env: - CONAN_LOGIN_USERNAME_CURA: ${{ secrets.CONAN_USER }} - CONAN_PASSWORD_CURA: ${{ secrets.CONAN_PASS }} - CONAN_LOGIN_USERNAME_CURA_CE: ${{ secrets.CONAN_USER }} - CONAN_PASSWORD_CURA_CE: ${{ secrets.CONAN_PASS }} - CONAN_LOG_RUN_TO_OUTPUT: 1 - CONAN_LOGGING_LEVEL: ${{ inputs.conan_logging_level }} - CONAN_NON_INTERACTIVE: 1 + CONAN_LOGIN_USERNAME_CURA: ${{ secrets.CONAN_USER }} + CONAN_PASSWORD_CURA: ${{ secrets.CONAN_PASS }} + CONAN_LOGIN_USERNAME_CURA_CE: ${{ secrets.CONAN_USER }} + CONAN_PASSWORD_CURA_CE: ${{ secrets.CONAN_PASS }} + CONAN_LOG_RUN_TO_OUTPUT: 1 + CONAN_LOGGING_LEVEL: ${{ inputs.conan_logging_level }} + CONAN_NON_INTERACTIVE: 1 -permissions: {} +permissions: { } jobs: conan-recipe-version: permissions: @@ -103,18 +103,23 @@ jobs: sudo apt update sudo apt upgrade sudo apt install efibootmgr build-essential checkinstall libegl-dev zlib1g-dev libssl-dev ninja-build autoconf libx11-dev libx11-xcb-dev libfontenc-dev libice-dev libsm-dev libxau-dev libxaw7-dev libxcomposite-dev libxcursor-dev libxdamage-dev libxdmcp-dev libxext-dev libxfixes-dev libxi-dev libxinerama-dev libxkbfile-dev libxmu-dev libxmuu-dev libxpm-dev libxrandr-dev libxrender-dev libxres-dev libxss-dev libxt-dev libxtst-dev libxv-dev libxvmc-dev libxxf86vm-dev xtrans-dev libxcb-render0-dev libxcb-render-util0-dev libxcb-xkb-dev libxcb-icccm4-dev libxcb-image0-dev libxcb-keysyms1-dev libxcb-randr0-dev libxcb-shape0-dev libxcb-sync-dev libxcb-xfixes0-dev libxcb-xinerama0-dev xkb-data libxcb-dri3-dev uuid-dev libxcb-util-dev libxkbcommon-x11-dev pkg-config flex bison g++-12 gcc-12 -y - sudo apt install g++-12 gcc-12 -y - sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-12 12 - sudo update-alternatives --install /usr/bin/g++ g++ /usr/bin/g++-12 12 + + - name: Install GCC-13 + run: | + sudo apt install g++-13 gcc-13 -y + sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-13 13 + sudo update-alternatives --install /usr/bin/g++ g++ /usr/bin/g++-13 13 - name: Create the default Conan profile run: conan profile new default --detect --force - name: Get Conan configuration - run: conan config install https://github.com/Ultimaker/conan-config.git + run: | + conan config install https://github.com/Ultimaker/conan-config.git + conan config install https://github.com/Ultimaker/conan-config.git -a "-b runner/${{ runner.os }}/${{ runner.arch }}" - name: Create the Packages - run: conan create . ${{ needs.conan-recipe-version.outputs.recipe_id_full }} --build=missing --update -o ${{ needs.conan-recipe-version.outputs.project_name }}:devtools=True + run: conan create . ${{ needs.conan-recipe-version.outputs.recipe_id_full }} --build=missing --update -o ${{ needs.conan-recipe-version.outputs.project_name }}:devtools=True -c tools.build:skip_test=True - name: Create the latest alias if: always() diff --git a/.github/workflows/conan-recipe-export.yml b/.github/workflows/conan-recipe-export.yml index 869a9de59e..ba5aaa49a1 100644 --- a/.github/workflows/conan-recipe-export.yml +++ b/.github/workflows/conan-recipe-export.yml @@ -1,106 +1,107 @@ name: Export Conan Recipe to server on: - workflow_call: - inputs: - recipe_id_full: - required: true - type: string + workflow_call: + inputs: + recipe_id_full: + required: true + type: string - recipe_id_latest: - required: false - type: string + recipe_id_latest: + required: false + type: string - runs_on: - required: true - type: string + runs_on: + required: true + type: string - python_version: - required: true - type: string + python_version: + required: true + type: string - conan_config_branch: - required: false - type: string + conan_config_branch: + required: false + type: string - conan_logging_level: - required: false - type: string + conan_logging_level: + required: false + type: string - conan_export_binaries: - required: false - type: boolean + conan_export_binaries: + required: false + type: boolean - conan_upload_community: - required: false - default: true - type: boolean + conan_upload_community: + required: false + default: true + type: boolean env: - CONAN_LOGIN_USERNAME: ${{ secrets.CONAN_USER }} - CONAN_PASSWORD: ${{ secrets.CONAN_PASS }} - CONAN_LOG_RUN_TO_OUTPUT: 1 - CONAN_LOGGING_LEVEL: ${{ inputs.conan_logging_level }} - CONAN_NON_INTERACTIVE: 1 + CONAN_LOGIN_USERNAME: ${{ secrets.CONAN_USER }} + CONAN_PASSWORD: ${{ secrets.CONAN_PASS }} + CONAN_LOG_RUN_TO_OUTPUT: 1 + CONAN_LOGGING_LEVEL: ${{ inputs.conan_logging_level }} + CONAN_NON_INTERACTIVE: 1 jobs: - package-export: - runs-on: ${{ inputs.runs_on }} + package-export: + runs-on: ${{ inputs.runs_on }} - steps: - - name: Checkout project - uses: actions/checkout@v3 + steps: + - name: Checkout project + uses: actions/checkout@v3 - - name: Setup Python and pip - uses: actions/setup-python@v4 - with: - python-version: ${{ inputs.python_version }} - cache: 'pip' - cache-dependency-path: .github/workflows/requirements-conan-package.txt + - name: Setup Python and pip + uses: actions/setup-python@v4 + with: + python-version: ${{ inputs.python_version }} + cache: 'pip' + cache-dependency-path: .github/workflows/requirements-conan-package.txt - - name: Install Python requirements and Create default Conan profile - run: | - pip install -r https://raw.githubusercontent.com/Ultimaker/Cura/main/.github/workflows/requirements-conan-package.txt - conan profile new default --detect - # Note the runner requirements are always installed from the main branch in the Ultimaker/Cura repo + - name: Install Python requirements and Create default Conan profile + run: | + pip install -r https://raw.githubusercontent.com/Ultimaker/Cura/main/.github/workflows/requirements-conan-package.txt + conan profile new default --detect + # Note the runner requirements are always installed from the main branch in the Ultimaker/Cura repo - - name: Cache Conan local repository packages - uses: actions/cache@v3 - with: - path: $HOME/.conan/data - key: ${{ runner.os }}-conan-export-cache + - name: Cache Conan local repository packages + uses: actions/cache@v3 + with: + path: $HOME/.conan/data + key: ${{ runner.os }}-conan-export-cache - - name: Get Conan configuration from branch - if: ${{ inputs.conan_config_branch != '' }} - run: conan config install https://github.com/Ultimaker/conan-config.git -a "-b ${{ inputs.conan_config_branch }}" + - name: Get Conan configuration from branch + if: ${{ inputs.conan_config_branch != '' }} + run: conan config install https://github.com/Ultimaker/conan-config.git -a "-b ${{ inputs.conan_config_branch }}" - - name: Get Conan configuration - if: ${{ inputs.conan_config_branch == '' }} - run: conan config install https://github.com/Ultimaker/conan-config.git + - name: Get Conan configuration + run: | + conan config install https://github.com/Ultimaker/conan-config.git + conan config install https://github.com/Ultimaker/conan-config.git -a "-b runner/${{ runner.os }}/${{ runner.arch }}" - - name: Add Cura private Artifactory remote - run: conan remote add cura-private https://ultimaker.jfrog.io/artifactory/api/conan/cura-private True + - name: Add Cura private Artifactory remote + run: conan remote add cura-private https://ultimaker.jfrog.io/artifactory/api/conan/cura-private True - - name: Export the Package (binaries) - if: ${{ inputs.conan_export_binaries }} - run: conan create . ${{ inputs.recipe_id_full }} --build=missing --update + - name: Export the Package (binaries) + if: ${{ inputs.conan_export_binaries }} + run: conan create . ${{ inputs.recipe_id_full }} --build=missing --update -c tools.build:skip_test=True - - name: Export the Package - if: ${{ !inputs.conan_export_binaries }} - run: conan export . ${{ inputs.recipe_id_full }} + - name: Export the Package + if: ${{ !inputs.conan_export_binaries }} + run: conan export . ${{ inputs.recipe_id_full }} - - name: Create the latest alias - if: always() - run: conan alias ${{ inputs.recipe_id_latest }} ${{ inputs.recipe_id_full }} + - name: Create the latest alias + if: always() + run: conan alias ${{ inputs.recipe_id_latest }} ${{ inputs.recipe_id_full }} - - name: Upload the Package(s) - if: ${{ always() && inputs.conan_upload_community }} - run: | - conan upload ${{ inputs.recipe_id_full }} -r cura --all -c - conan upload ${{ inputs.recipe_id_latest }} -r cura -c + - name: Upload the Package(s) + if: ${{ always() && inputs.conan_upload_community }} + run: | + conan upload ${{ inputs.recipe_id_full }} -r cura --all -c + conan upload ${{ inputs.recipe_id_latest }} -r cura -c - - name: Upload the Package(s) to the private Artifactory - if: ${{ always() && ! inputs.conan_upload_community }} - run: | - conan upload ${{ inputs.recipe_id_full }} -r cura-private --all -c - conan upload ${{ inputs.recipe_id_latest }} -r cura-private -c + - name: Upload the Package(s) to the private Artifactory + if: ${{ always() && ! inputs.conan_upload_community }} + run: | + conan upload ${{ inputs.recipe_id_full }} -r cura-private --all -c + conan upload ${{ inputs.recipe_id_latest }} -r cura-private -c diff --git a/.github/workflows/linux.yml b/.github/workflows/linux.yml index 5a18199e3c..2e15584299 100644 --- a/.github/workflows/linux.yml +++ b/.github/workflows/linux.yml @@ -2,270 +2,263 @@ name: Linux Installer run-name: ${{ inputs.cura_conan_version }} for Linux-${{ inputs.architecture }} by @${{ github.actor }} on: - workflow_dispatch: - inputs: - cura_conan_version: - description: 'Cura Conan Version' - default: 'cura/latest@ultimaker/testing' - required: true - type: string - conan_args: - description: 'Conan args: eq.: --require-override' - default: '' - required: false - type: string - enterprise: - description: 'Build Cura as an Enterprise edition' - default: false - required: true - type: boolean - staging: - description: 'Use staging API' - default: false - required: true - type: boolean - architecture: - description: 'Architecture' - required: true - default: 'X64' - type: choice - options: - - X64 - operating_system: - description: 'OS' - required: true - default: 'ubuntu-22.04' - type: choice - options: - - ubuntu-22.04 - - ubuntu-20.04 - workflow_call: - inputs: - cura_conan_version: - description: 'Cura Conan Version' - default: 'cura/latest@ultimaker/testing' - required: true - type: string - conan_args: - description: 'Conan args: eq.: --require-override' - default: '' - required: false - type: string - enterprise: - description: 'Build Cura as an Enterprise edition' - default: false - required: true - type: boolean - staging: - description: 'Use staging API' - default: false - required: true - type: boolean - architecture: - description: 'Architecture' - required: true - default: 'X64' - type: string - operating_system: - description: 'OS' - required: true - default: 'ubuntu-22.04' - type: string + workflow_dispatch: + inputs: + cura_conan_version: + description: 'Cura Conan Version' + default: 'cura/latest@ultimaker/testing' + required: true + type: string + conan_args: + description: 'Conan args: eq.: --require-override' + default: '' + required: false + type: string + enterprise: + description: 'Build Cura as an Enterprise edition' + default: false + required: true + type: boolean + staging: + description: 'Use staging API' + default: false + required: true + type: boolean + architecture: + description: 'Architecture' + required: true + default: 'X64' + type: choice + options: + - X64 + operating_system: + description: 'OS' + required: true + default: 'ubuntu-22.04' + type: choice + options: + - ubuntu-22.04 + - ubuntu-20.04 + workflow_call: + inputs: + cura_conan_version: + description: 'Cura Conan Version' + default: 'cura/latest@ultimaker/testing' + required: true + type: string + conan_args: + description: 'Conan args: eq.: --require-override' + default: '' + required: false + type: string + enterprise: + description: 'Build Cura as an Enterprise edition' + default: false + required: true + type: boolean + staging: + description: 'Use staging API' + default: false + required: true + type: boolean + architecture: + description: 'Architecture' + required: true + default: 'X64' + type: string + operating_system: + description: 'OS' + required: true + default: 'ubuntu-22.04' + type: string env: - CONAN_LOGIN_USERNAME_CURA: ${{ secrets.CONAN_USER }} - CONAN_PASSWORD_CURA: ${{ secrets.CONAN_PASS }} - GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }} - CURA_CONAN_VERSION: ${{ inputs.cura_conan_version }} - ENTERPRISE: ${{ inputs.enterprise }} - STAGING: ${{ inputs.staging }} + CONAN_LOGIN_USERNAME_CURA: ${{ secrets.CONAN_USER }} + CONAN_PASSWORD_CURA: ${{ secrets.CONAN_PASS }} + GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }} + CURA_CONAN_VERSION: ${{ inputs.cura_conan_version }} + ENTERPRISE: ${{ inputs.enterprise }} + STAGING: ${{ inputs.staging }} jobs: - cura-installer-create: - runs-on: ${{ inputs.operating_system }} + cura-installer-create: + runs-on: ${{ inputs.operating_system }} - steps: - - name: Checkout - uses: actions/checkout@v3 + steps: + - name: Checkout + uses: actions/checkout@v3 - - name: Setup Python and pip - uses: actions/setup-python@v4 - with: - python-version: '3.10.x' - cache: 'pip' - cache-dependency-path: .github/workflows/requirements-conan-package.txt - - - name: Install Python requirements for runner - run: pip install -r .github/workflows/requirements-conan-package.txt - - - name: Cache Conan local repository packages (Bash) - uses: actions/cache@v3 - with: - path: | - $HOME/.conan/data - $HOME/.conan/conan_download_cache - key: conan-${{ runner.os }}-${{ runner.arch }}-installer-cache - - - name: Hack needed specifically for ubuntu-22.04 from mid-Feb 2023 onwards - if: ${{ startsWith(inputs.operating_system, 'ubuntu-22.04') }} - run: sudo apt remove libodbc2 libodbcinst2 unixodbc-common -y - - # NOTE: Due to what are probably github issues, we have to remove the cache and reconfigure before the rest. - # This is maybe because grub caches the disk it uses last time, which is recreated each time. - - name: Install Linux system requirements - run: | - sudo rm /var/cache/debconf/config.dat - sudo dpkg --configure -a - sudo add-apt-repository ppa:ubuntu-toolchain-r/test -y - sudo apt update - sudo apt upgrade - sudo apt install build-essential checkinstall libegl-dev zlib1g-dev libssl-dev ninja-build autoconf libx11-dev libx11-xcb-dev libfontenc-dev libice-dev libsm-dev libxau-dev libxaw7-dev libxcomposite-dev libxcursor-dev libxdamage-dev libxdmcp-dev libxext-dev libxfixes-dev libxi-dev libxinerama-dev libxkbfile-dev libxmu-dev libxmuu-dev libxpm-dev libxrandr-dev libxrender-dev libxres-dev libxss-dev libxt-dev libxtst-dev libxv-dev libxvmc-dev libxxf86vm-dev xtrans-dev libxcb-render0-dev libxcb-render-util0-dev libxcb-xkb-dev libxcb-icccm4-dev libxcb-image0-dev libxcb-keysyms1-dev libxcb-randr0-dev libxcb-shape0-dev libxcb-sync-dev libxcb-xfixes0-dev libxcb-xinerama0-dev xkb-data libxcb-dri3-dev uuid-dev libxcb-util-dev libxkbcommon-x11-dev pkg-config -y - wget --no-check-certificate --quiet https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage -O $GITHUB_WORKSPACE/appimagetool - chmod +x $GITHUB_WORKSPACE/appimagetool - echo "APPIMAGETOOL_LOCATION=$GITHUB_WORKSPACE/appimagetool" >> $GITHUB_ENV - - - name: Install GCC-12 on ubuntu-22.04 - if: ${{ startsWith(inputs.operating_system, 'ubuntu-22.04') }} - run: | - sudo apt install g++-12 gcc-12 -y - sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-12 12 - sudo update-alternatives --install /usr/bin/g++ g++ /usr/bin/g++-12 12 - - - name: Use GCC-10 on ubuntu-20.04 - if: ${{ startsWith(inputs.operating_system, 'ubuntu-20.04') }} - run: | - sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-10 10 - sudo update-alternatives --install /usr/bin/g++ g++ /usr/bin/g++-10 10 - - - name: Create the default Conan profile - run: conan profile new default --detect --force - - - name: Configure GPG Key Linux (Bash) - run: echo -n "$GPG_PRIVATE_KEY" | base64 --decode | gpg --import - - - name: Get Conan configuration - run: | - conan config install https://github.com/Ultimaker/conan-config.git - conan config install https://github.com/Ultimaker/conan-config.git -a "-b runner/${{ runner.os }}/${{ runner.arch }}" - - - name: Use Conan download cache (Bash) - run: conan config set storage.download_cache="$HOME/.conan/conan_download_cache" - - - name: Create the Packages (Bash) - run: conan install $CURA_CONAN_VERSION ${{ inputs.conan_args }} --build=missing --update -if cura_inst -g VirtualPythonEnv -o cura:enterprise=$ENTERPRISE -o cura:staging=$STAGING --json "cura_inst/conan_install_info.json" - - - name: Upload the Package(s) - if: always() - run: | - conan upload "*" -r cura --all -c - - - name: Set Environment variables for Cura (bash) - run: | - . ./cura_inst/bin/activate_github_actions_env.sh - . ./cura_inst/bin/activate_github_actions_version_env.sh - - # FIXME: This is a workaround to ensure that we use and pack a shared library for OpenSSL 1.1.1l. We currently compile - # OpenSSL statically for CPython, but our Python Dependenies (such as PyQt6) require a shared library. - # Because Conan won't allow for building the same library with two different options (easily) we need to install it explicitly - # and do a manual copy to the VirtualEnv, such that Pyinstaller can find it. - - - name: Install OpenSSL shared - run: conan install openssl/1.1.1l@_/_ --build=missing --update -o openssl:shared=True -g deploy - - - name: Copy OpenSSL shared (Bash) - run: | - cp ./openssl/lib/*.so* ./cura_inst/bin/ || true - cp ./openssl/lib/*.dylib* ./cura_inst/bin/ || true - - - name: Create the Cura dist - run: pyinstaller ./cura_inst/UltiMaker-Cura.spec - - - name: Output the name file name and extension - id: filename - shell: python - run: | - import os - enterprise = "-Enterprise" if "${{ inputs.enterprise }}" == "true" else "" - if "${{ inputs.operating_system }}" == "ubuntu-22.04": - installer_filename = f"UltiMaker-Cura-{os.getenv('CURA_VERSION_FULL')}{enterprise}-linux-modern-${{ inputs.architecture }}" - else: - installer_filename = f"UltiMaker-Cura-{os.getenv('CURA_VERSION_FULL')}{enterprise}-linux-${{ inputs.architecture }}" - output_env = os.environ["GITHUB_OUTPUT"] - content = "" - if os.path.exists(output_env): - with open(output_env, "r") as f: - content = f.read() - with open(output_env, "w") as f: - f.write(content) - f.writelines(f"INSTALLER_FILENAME={installer_filename}\n") - - - name: Summarize the used Conan dependencies - shell: python - run: | - import os - import json - from pathlib import Path - - conan_install_info_path = Path("cura_inst/conan_install_info.json") - conan_info = {"installed": []} - if os.path.exists(conan_install_info_path): - with open(conan_install_info_path, "r") as f: - conan_info = json.load(f) - sorted_deps = sorted([dep["recipe"]["id"].replace('#', r' rev: ') for dep in conan_info["installed"]]) - - summary_env = os.environ["GITHUB_STEP_SUMMARY"] - content = "" - if os.path.exists(summary_env): - with open(summary_env, "r") as f: - content = f.read() - - with open(summary_env, "w") as f: - f.write(content) - f.writelines("# ${{ steps.filename.outputs.INSTALLER_FILENAME }}\n") - f.writelines("## Conan packages:\n") - for dep in sorted_deps: - f.writelines(f"`{dep}`\n") - - - name: Summarize the used Python modules - shell: python - run: | - import os - import pkg_resources - summary_env = os.environ["GITHUB_STEP_SUMMARY"] - content = "" - if os.path.exists(summary_env): - with open(summary_env, "r") as f: - content = f.read() - - with open(summary_env, "w") as f: - f.write(content) - f.writelines("## Python modules:\n") - for package in pkg_resources.working_set: - f.writelines(f"`{package.key}/{package.version}`\n") - - - name: Create the Linux AppImage (Bash) - run: | - python ../cura_inst/packaging/AppImage/create_appimage.py ./UltiMaker-Cura $CURA_VERSION_FULL "${{ steps.filename.outputs.INSTALLER_FILENAME }}.AppImage" - chmod +x "${{ steps.filename.outputs.INSTALLER_FILENAME }}.AppImage" - working-directory: dist - - - name: Upload the AppImage - uses: actions/upload-artifact@v3 - with: - name: ${{ steps.filename.outputs.INSTALLER_FILENAME }}-AppImage - path: | - dist/${{ steps.filename.outputs.INSTALLER_FILENAME }}.AppImage - retention-days: 5 - - notify-export: - if: ${{ always() }} - needs: [ cura-installer-create ] - - uses: ultimaker/cura/.github/workflows/notify.yml@main + - name: Setup Python and pip + uses: actions/setup-python@v4 with: - success: ${{ contains(join(needs.*.result, ','), 'success') }} - success_title: "Create the Cura distributions" - success_body: "Installers for ${{ inputs.cura_conan_version }}" - failure_title: "Failed to create the Cura distributions" - failure_body: "Failed to create at least 1 installer for ${{ inputs.cura_conan_version }}" - secrets: inherit + python-version: '3.10.x' + cache: 'pip' + cache-dependency-path: .github/workflows/requirements-conan-package.txt + + - name: Install Python requirements for runner + run: pip install -r .github/workflows/requirements-conan-package.txt + + - name: Cache Conan local repository packages (Bash) + uses: actions/cache@v3 + with: + path: | + $HOME/.conan/data + $HOME/.conan/conan_download_cache + key: conan-${{ runner.os }}-${{ runner.arch }}-installer-cache + + - name: Hack needed specifically for ubuntu-22.04 from mid-Feb 2023 onwards + if: ${{ startsWith(inputs.operating_system, 'ubuntu-22.04') }} + run: sudo apt remove libodbc2 libodbcinst2 unixodbc-common -y + + # NOTE: Due to what are probably github issues, we have to remove the cache and reconfigure before the rest. + # This is maybe because grub caches the disk it uses last time, which is recreated each time. + - name: Install Linux system requirements + run: | + sudo rm /var/cache/debconf/config.dat + sudo dpkg --configure -a + sudo add-apt-repository ppa:ubuntu-toolchain-r/test -y + sudo apt update + sudo apt upgrade + sudo apt install build-essential checkinstall libegl-dev zlib1g-dev libssl-dev ninja-build autoconf libx11-dev libx11-xcb-dev libfontenc-dev libice-dev libsm-dev libxau-dev libxaw7-dev libxcomposite-dev libxcursor-dev libxdamage-dev libxdmcp-dev libxext-dev libxfixes-dev libxi-dev libxinerama-dev libxkbfile-dev libxmu-dev libxmuu-dev libxpm-dev libxrandr-dev libxrender-dev libxres-dev libxss-dev libxt-dev libxtst-dev libxv-dev libxvmc-dev libxxf86vm-dev xtrans-dev libxcb-render0-dev libxcb-render-util0-dev libxcb-xkb-dev libxcb-icccm4-dev libxcb-image0-dev libxcb-keysyms1-dev libxcb-randr0-dev libxcb-shape0-dev libxcb-sync-dev libxcb-xfixes0-dev libxcb-xinerama0-dev xkb-data libxcb-dri3-dev uuid-dev libxcb-util-dev libxkbcommon-x11-dev pkg-config -y + wget --no-check-certificate --quiet https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage -O $GITHUB_WORKSPACE/appimagetool + chmod +x $GITHUB_WORKSPACE/appimagetool + echo "APPIMAGETOOL_LOCATION=$GITHUB_WORKSPACE/appimagetool" >> $GITHUB_ENV + + - name: Install GCC-13 + run: | + sudo apt install g++-13 gcc-13 -y + sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-13 13 + sudo update-alternatives --install /usr/bin/g++ g++ /usr/bin/g++-13 13 + + - name: Create the default Conan profile + run: conan profile new default --detect --force + + - name: Configure GPG Key Linux (Bash) + run: echo -n "$GPG_PRIVATE_KEY" | base64 --decode | gpg --import + + - name: Get Conan configuration + run: | + conan config install https://github.com/Ultimaker/conan-config.git + conan config install https://github.com/Ultimaker/conan-config.git -a "-b runner/${{ runner.os }}/${{ runner.arch }}" + + - name: Use Conan download cache (Bash) + run: conan config set storage.download_cache="$HOME/.conan/conan_download_cache" + + - name: Create the Packages (Bash) + run: conan install $CURA_CONAN_VERSION ${{ inputs.conan_args }} --build=missing --update -if cura_inst -g VirtualPythonEnv -o cura:enterprise=$ENTERPRISE -o cura:staging=$STAGING --json "cura_inst/conan_install_info.json" + + - name: Upload the Package(s) + if: always() + run: | + conan upload "*" -r cura --all -c + + - name: Set Environment variables for Cura (bash) + run: | + . ./cura_inst/bin/activate_github_actions_env.sh + . ./cura_inst/bin/activate_github_actions_version_env.sh + + # FIXME: This is a workaround to ensure that we use and pack a shared library for OpenSSL 1.1.1l. We currently compile + # OpenSSL statically for CPython, but our Python Dependenies (such as PyQt6) require a shared library. + # Because Conan won't allow for building the same library with two different options (easily) we need to install it explicitly + # and do a manual copy to the VirtualEnv, such that Pyinstaller can find it. + + - name: Install OpenSSL shared + run: conan install openssl/1.1.1l@_/_ --build=missing --update -o openssl:shared=True -g deploy + + - name: Copy OpenSSL shared (Bash) + run: | + cp ./openssl/lib/*.so* ./cura_inst/bin/ || true + cp ./openssl/lib/*.dylib* ./cura_inst/bin/ || true + + - name: Create the Cura dist + run: pyinstaller ./cura_inst/UltiMaker-Cura.spec + + - name: Output the name file name and extension + id: filename + shell: python + run: | + import os + enterprise = "-Enterprise" if "${{ inputs.enterprise }}" == "true" else "" + if "${{ inputs.operating_system }}" == "ubuntu-22.04": + installer_filename = f"UltiMaker-Cura-{os.getenv('CURA_VERSION_FULL')}{enterprise}-linux-modern-${{ inputs.architecture }}" + else: + installer_filename = f"UltiMaker-Cura-{os.getenv('CURA_VERSION_FULL')}{enterprise}-linux-${{ inputs.architecture }}" + output_env = os.environ["GITHUB_OUTPUT"] + content = "" + if os.path.exists(output_env): + with open(output_env, "r") as f: + content = f.read() + with open(output_env, "w") as f: + f.write(content) + f.writelines(f"INSTALLER_FILENAME={installer_filename}\n") + + - name: Summarize the used Conan dependencies + shell: python + run: | + import os + import json + from pathlib import Path + + conan_install_info_path = Path("cura_inst/conan_install_info.json") + conan_info = {"installed": []} + if os.path.exists(conan_install_info_path): + with open(conan_install_info_path, "r") as f: + conan_info = json.load(f) + sorted_deps = sorted([dep["recipe"]["id"].replace('#', r' rev: ') for dep in conan_info["installed"]]) + + summary_env = os.environ["GITHUB_STEP_SUMMARY"] + content = "" + if os.path.exists(summary_env): + with open(summary_env, "r") as f: + content = f.read() + + with open(summary_env, "w") as f: + f.write(content) + f.writelines("# ${{ steps.filename.outputs.INSTALLER_FILENAME }}\n") + f.writelines("## Conan packages:\n") + for dep in sorted_deps: + f.writelines(f"`{dep}`\n") + + - name: Summarize the used Python modules + shell: python + run: | + import os + import pkg_resources + summary_env = os.environ["GITHUB_STEP_SUMMARY"] + content = "" + if os.path.exists(summary_env): + with open(summary_env, "r") as f: + content = f.read() + + with open(summary_env, "w") as f: + f.write(content) + f.writelines("## Python modules:\n") + for package in pkg_resources.working_set: + f.writelines(f"`{package.key}/{package.version}`\n") + + - name: Create the Linux AppImage (Bash) + run: | + python ../cura_inst/packaging/AppImage/create_appimage.py ./UltiMaker-Cura $CURA_VERSION_FULL "${{ steps.filename.outputs.INSTALLER_FILENAME }}.AppImage" + chmod +x "${{ steps.filename.outputs.INSTALLER_FILENAME }}.AppImage" + working-directory: dist + + - name: Upload the AppImage + uses: actions/upload-artifact@v3 + with: + name: ${{ steps.filename.outputs.INSTALLER_FILENAME }}-AppImage + path: | + dist/${{ steps.filename.outputs.INSTALLER_FILENAME }}.AppImage + retention-days: 5 + + notify-export: + if: ${{ always() }} + needs: [ cura-installer-create ] + + uses: ultimaker/cura/.github/workflows/notify.yml@main + with: + success: ${{ contains(join(needs.*.result, ','), 'success') }} + success_title: "Create the Cura distributions" + success_body: "Installers for ${{ inputs.cura_conan_version }}" + failure_title: "Failed to create the Cura distributions" + failure_body: "Failed to create at least 1 installer for ${{ inputs.cura_conan_version }}" + secrets: inherit diff --git a/.github/workflows/requirements-conan-package.txt b/.github/workflows/requirements-conan-package.txt index 6b4d4cffc8..9380d1cb98 100644 --- a/.github/workflows/requirements-conan-package.txt +++ b/.github/workflows/requirements-conan-package.txt @@ -1,2 +1,2 @@ -conan==1.60.2 +conan>=1.60.2,<2.0.0 sip diff --git a/.github/workflows/unit-test.yml b/.github/workflows/unit-test.yml index f08acbdb04..8321f42a23 100644 --- a/.github/workflows/unit-test.yml +++ b/.github/workflows/unit-test.yml @@ -2,163 +2,165 @@ name: unit-test on: - push: - paths: - - 'plugins/**' - - 'resources/**' - - 'cura/**' - - 'icons/**' - - 'tests/**' - - 'packaging/**' - - '.github/workflows/conan-*.yml' - - '.github/workflows/unit-test.yml' - - '.github/workflows/notify.yml' - - '.github/workflows/requirements-conan-package.txt' - - 'requirements*.txt' - - 'conanfile.py' - - 'conandata.yml' - - 'GitVersion.yml' - - '*.jinja' - branches: - - main - - 'CURA-*' - - '[1-9]+.[0-9]+' - tags: - - '[0-9]+.[0-9]+.[0-9]+' - - '[0-9]+.[0-9]+-beta' - pull_request: - paths: - - 'plugins/**' - - 'resources/**' - - 'cura/**' - - 'icons/**' - - 'tests/**' - - 'packaging/**' - - '.github/workflows/conan-*.yml' - - '.github/workflows/unit-test.yml' - - '.github/workflows/notify.yml' - - '.github/workflows/requirements-conan-package.txt' - - 'requirements*.txt' - - 'conanfile.py' - - 'conandata.yml' - - 'GitVersion.yml' - - '*.jinja' - branches: - - main - - '[1-9]+.[0-9]+' - tags: - - '[0-9]+.[0-9]+.[0-9]+' - - '[0-9]+.[0-9]+-beta' + push: + paths: + - 'plugins/**' + - 'resources/**' + - 'cura/**' + - 'icons/**' + - 'tests/**' + - 'packaging/**' + - '.github/workflows/conan-*.yml' + - '.github/workflows/unit-test.yml' + - '.github/workflows/notify.yml' + - '.github/workflows/requirements-conan-package.txt' + - 'requirements*.txt' + - 'conanfile.py' + - 'conandata.yml' + - 'GitVersion.yml' + - '*.jinja' + branches: + - main + - 'CURA-*' + - '[1-9]+.[0-9]+' + tags: + - '[0-9]+.[0-9]+.[0-9]+' + - '[0-9]+.[0-9]+-beta' + pull_request: + paths: + - 'plugins/**' + - 'resources/**' + - 'cura/**' + - 'icons/**' + - 'tests/**' + - 'packaging/**' + - '.github/workflows/conan-*.yml' + - '.github/workflows/unit-test.yml' + - '.github/workflows/notify.yml' + - '.github/workflows/requirements-conan-package.txt' + - 'requirements*.txt' + - 'conanfile.py' + - 'conandata.yml' + - 'GitVersion.yml' + - '*.jinja' + branches: + - main + - '[1-9]+.[0-9]+' + tags: + - '[0-9]+.[0-9]+.[0-9]+' + - '[0-9]+.[0-9]+-beta' env: - CONAN_LOGIN_USERNAME_CURA: ${{ secrets.CONAN_USER }} - CONAN_PASSWORD_CURA: ${{ secrets.CONAN_PASS }} - CONAN_LOGIN_USERNAME_CURA_CE: ${{ secrets.CONAN_USER }} - CONAN_PASSWORD_CURA_CE: ${{ secrets.CONAN_PASS }} - CONAN_LOG_RUN_TO_OUTPUT: 1 - CONAN_LOGGING_LEVEL: info - CONAN_NON_INTERACTIVE: 1 + CONAN_LOGIN_USERNAME_CURA: ${{ secrets.CONAN_USER }} + CONAN_PASSWORD_CURA: ${{ secrets.CONAN_PASS }} + CONAN_LOGIN_USERNAME_CURA_CE: ${{ secrets.CONAN_USER }} + CONAN_PASSWORD_CURA_CE: ${{ secrets.CONAN_PASS }} + CONAN_LOG_RUN_TO_OUTPUT: 1 + CONAN_LOGGING_LEVEL: info + CONAN_NON_INTERACTIVE: 1 permissions: contents: read jobs: - conan-recipe-version: - uses: ultimaker/cura/.github/workflows/conan-recipe-version.yml@main + conan-recipe-version: + uses: ultimaker/cura/.github/workflows/conan-recipe-version.yml@main + with: + project_name: cura + + testing: + runs-on: ubuntu-22.04 + needs: [ conan-recipe-version ] + + steps: + - name: Checkout + uses: actions/checkout@v3 with: - project_name: cura + fetch-depth: 2 - testing: - runs-on: ubuntu-22.04 - needs: [ conan-recipe-version ] + - name: Setup Python and pip + uses: actions/setup-python@v4 + with: + python-version: '3.11.x' + architecture: 'x64' + cache: 'pip' + cache-dependency-path: .github/workflows/requirements-conan-package.txt - steps: - - name: Checkout - uses: actions/checkout@v3 - with: - fetch-depth: 2 + - name: Install Python requirements and Create default Conan profile + run: pip install -r requirements-conan-package.txt + working-directory: .github/workflows/ - - name: Setup Python and pip - uses: actions/setup-python@v4 - with: - python-version: '3.11.x' - architecture: 'x64' - cache: 'pip' - cache-dependency-path: .github/workflows/requirements-conan-package.txt + - name: Use Conan download cache (Bash) + if: ${{ runner.os != 'Windows' }} + run: conan config set storage.download_cache="$HOME/.conan/conan_download_cache" - - name: Install Python requirements and Create default Conan profile - run: pip install -r requirements-conan-package.txt - working-directory: .github/workflows/ + - name: Cache Conan local repository packages (Bash) + uses: actions/cache@v3 + if: ${{ runner.os != 'Windows' }} + with: + path: | + $HOME/.conan/data + $HOME/.conan/conan_download_cache + key: conan-${{ runner.os }}-${{ runner.arch }}-unit-cache - - name: Use Conan download cache (Bash) - if: ${{ runner.os != 'Windows' }} - run: conan config set storage.download_cache="$HOME/.conan/conan_download_cache" + # NOTE: Due to what are probably github issues, we have to remove the cache and reconfigure before the rest. + # This is maybe because grub caches the disk it uses last time, which is recreated each time. + - name: Install Linux system requirements + if: ${{ runner.os == 'Linux' }} + run: | + sudo rm /var/cache/debconf/config.dat + sudo dpkg --configure -a + sudo add-apt-repository ppa:ubuntu-toolchain-r/test -y + sudo apt update + sudo apt upgrade + sudo apt install build-essential checkinstall libegl-dev zlib1g-dev libssl-dev ninja-build autoconf libx11-dev libx11-xcb-dev libfontenc-dev libice-dev libsm-dev libxau-dev libxaw7-dev libxcomposite-dev libxcursor-dev libxdamage-dev libxdmcp-dev libxext-dev libxfixes-dev libxi-dev libxinerama-dev libxkbfile-dev libxmu-dev libxmuu-dev libxpm-dev libxrandr-dev libxrender-dev libxres-dev libxss-dev libxt-dev libxtst-dev libxv-dev libxvmc-dev libxxf86vm-dev xtrans-dev libxcb-render0-dev libxcb-render-util0-dev libxcb-xkb-dev libxcb-icccm4-dev libxcb-image0-dev libxcb-keysyms1-dev libxcb-randr0-dev libxcb-shape0-dev libxcb-sync-dev libxcb-xfixes0-dev libxcb-xinerama0-dev xkb-data libxcb-dri3-dev uuid-dev libxcb-util-dev libxkbcommon-x11-dev pkg-config -y - - name: Cache Conan local repository packages (Bash) - uses: actions/cache@v3 - if: ${{ runner.os != 'Windows' }} - with: - path: | - $HOME/.conan/data - $HOME/.conan/conan_download_cache - key: conan-${{ runner.os }}-${{ runner.arch }}-unit-cache + - name: Install GCC-13 + run: | + sudo apt install g++-13 gcc-13 -y + sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-13 13 + sudo update-alternatives --install /usr/bin/g++ g++ /usr/bin/g++-13 13 - # NOTE: Due to what are probably github issues, we have to remove the cache and reconfigure before the rest. - # This is maybe because grub caches the disk it uses last time, which is recreated each time. - - name: Install Linux system requirements - if: ${{ runner.os == 'Linux' }} - run: | - sudo rm /var/cache/debconf/config.dat - sudo dpkg --configure -a - sudo add-apt-repository ppa:ubuntu-toolchain-r/test -y - sudo apt update - sudo apt upgrade - sudo apt install build-essential checkinstall libegl-dev zlib1g-dev libssl-dev ninja-build autoconf libx11-dev libx11-xcb-dev libfontenc-dev libice-dev libsm-dev libxau-dev libxaw7-dev libxcomposite-dev libxcursor-dev libxdamage-dev libxdmcp-dev libxext-dev libxfixes-dev libxi-dev libxinerama-dev libxkbfile-dev libxmu-dev libxmuu-dev libxpm-dev libxrandr-dev libxrender-dev libxres-dev libxss-dev libxt-dev libxtst-dev libxv-dev libxvmc-dev libxxf86vm-dev xtrans-dev libxcb-render0-dev libxcb-render-util0-dev libxcb-xkb-dev libxcb-icccm4-dev libxcb-image0-dev libxcb-keysyms1-dev libxcb-randr0-dev libxcb-shape0-dev libxcb-sync-dev libxcb-xfixes0-dev libxcb-xinerama0-dev xkb-data libxcb-dri3-dev uuid-dev libxcb-util-dev libxkbcommon-x11-dev pkg-config -y + - name: Get Conan configuration + run: | + conan config install https://github.com/Ultimaker/conan-config.git + conan config install https://github.com/Ultimaker/conan-config.git -a "-b runner/${{ runner.os }}/${{ runner.arch }}" - - name: Install GCC-12 on ubuntu-22.04 - run: | - sudo apt install g++-12 gcc-12 -y - sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-12 12 - sudo update-alternatives --install /usr/bin/g++ g++ /usr/bin/g++-12 12 - - - name: Get Conan configuration - run: conan config install https://github.com/Ultimaker/conan-config.git + - name: Get Conan profile + run: conan profile new default --detect --force - - name: Get Conan profile - run: conan profile new default --detect --force + - name: Install dependencies + run: conan install . ${{ needs.conan-recipe-version.outputs.recipe_id_full }} --build=missing --update -o cura:devtools=True -g VirtualPythonEnv -if venv - - name: Install dependencies - run: conan install . ${{ needs.conan-recipe-version.outputs.recipe_id_full }} --build=missing --update -o cura:devtools=True -g VirtualPythonEnv -if venv + - name: Upload the Dependency package(s) + run: conan upload "*" -r cura --all -c - - name: Upload the Dependency package(s) - run: conan upload "*" -r cura --all -c + - name: Set Environment variables for Cura (bash) + if: ${{ runner.os != 'Windows' }} + run: | + . ./venv/bin/activate_github_actions_env.sh - - name: Set Environment variables for Cura (bash) - if: ${{ runner.os != 'Windows' }} - run: | - . ./venv/bin/activate_github_actions_env.sh + - name: Run Unit Test + id: run-test + run: | + pytest --junitxml=junit_cura.xml + working-directory: tests - - name: Run Unit Test - id: run-test - run: | - pytest --junitxml=junit_cura.xml - working-directory: tests + - name: Save PR metadata + if: always() + run: | + echo ${{ github.event.number }} > pr-id.txt + echo ${{ github.event.pull_request.head.repo.full_name }} > pr-head-repo.txt + echo ${{ github.event.pull_request.head.ref }} > pr-head-ref.txt + working-directory: tests - - name: Save PR metadata - if: always() - run: | - echo ${{ github.event.number }} > pr-id.txt - echo ${{ github.event.pull_request.head.repo.full_name }} > pr-head-repo.txt - echo ${{ github.event.pull_request.head.ref }} > pr-head-ref.txt - working-directory: tests - - - name: Upload Test Results - if: always() - uses: actions/upload-artifact@v3 - with: - name: test-result - path: | - tests/**/*.xml - tests/pr-id.txt - tests/pr-head-repo.txt - tests/pr-head-ref.txt + - name: Upload Test Results + if: always() + uses: actions/upload-artifact@v3 + with: + name: test-result + path: | + tests/**/*.xml + tests/pr-id.txt + tests/pr-head-repo.txt + tests/pr-head-ref.txt diff --git a/.github/workflows/update-translation.yml b/.github/workflows/update-translation.yml index 65693be937..55ce144666 100644 --- a/.github/workflows/update-translation.yml +++ b/.github/workflows/update-translation.yml @@ -1,82 +1,87 @@ name: update-translations on: - push: - paths: - - 'plugins/**' - - 'resources/**' - - 'cura/**' - - 'icons/**' - - 'tests/**' - - 'packaging/**' - - '.github/workflows/conan-*.yml' - - '.github/workflows/notify.yml' - - '.github/workflows/requirements-conan-package.txt' - - 'requirements*.txt' - - 'conanfile.py' - - 'conandata.yml' - - 'GitVersion.yml' - - '*.jinja' - branches: - - '[1-9].[0-9]' - - '[1-9].[0-9][0-9]' - tags: - - '[1-9].[0-9].[0-9]*' - - '[1-9].[0-9].[0-9]' - - '[1-9].[0-9][0-9].[0-9]*' + push: + paths: + - 'plugins/**' + - 'resources/**' + - 'cura/**' + - 'icons/**' + - 'tests/**' + - 'packaging/**' + - '.github/workflows/conan-*.yml' + - '.github/workflows/notify.yml' + - '.github/workflows/requirements-conan-package.txt' + - 'requirements*.txt' + - 'conanfile.py' + - 'conandata.yml' + - 'GitVersion.yml' + - '*.jinja' + branches: + - '[1-9].[0-9]' + - '[1-9].[0-9][0-9]' + tags: + - '[1-9].[0-9].[0-9]*' + - '[1-9].[0-9].[0-9]' + - '[1-9].[0-9][0-9].[0-9]*' jobs: - update-translations: - name: Update translations + update-translations: + name: Update translations - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v3 + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 - - name: Cache Conan data - id: cache-conan - uses: actions/cache@v3 - with: - path: ~/.conan - key: ${{ runner.os }}-conan + - name: Cache Conan data + id: cache-conan + uses: actions/cache@v3 + with: + path: ~/.conan + key: ${{ runner.os }}-conan - - name: Setup Python and pip - uses: actions/setup-python@v4 - with: - python-version: 3.10.x - cache: pip - cache-dependency-path: .github/workflows/requirements-conan-package.txt + - name: Setup Python and pip + uses: actions/setup-python@v4 + with: + python-version: 3.11.x + cache: pip + cache-dependency-path: .github/workflows/requirements-conan-package.txt - - name: Install Python requirements for runner - run: pip install -r .github/workflows/requirements-conan-package.txt + - name: Install Python requirements for runner + run: pip install -r .github/workflows/requirements-conan-package.txt - # NOTE: Due to what are probably github issues, we have to remove the cache and reconfigure before the rest. - # This is maybe because grub caches the disk it uses last time, which is recreated each time. - - name: Install Linux system requirements - if: ${{ runner.os == 'Linux' }} - run: | - sudo rm /var/cache/debconf/config.dat - sudo dpkg --configure -a - sudo add-apt-repository ppa:ubuntu-toolchain-r/test -y - sudo apt update - sudo apt upgrade - sudo apt install efibootmgr build-essential checkinstall libegl-dev zlib1g-dev libssl-dev ninja-build autoconf libx11-dev libx11-xcb-dev libfontenc-dev libice-dev libsm-dev libxau-dev libxaw7-dev libxcomposite-dev libxcursor-dev libxdamage-dev libxdmcp-dev libxext-dev libxfixes-dev libxi-dev libxinerama-dev libxkbfile-dev libxmu-dev libxmuu-dev libxpm-dev libxrandr-dev libxrender-dev libxres-dev libxss-dev libxt-dev libxtst-dev libxv-dev libxvmc-dev libxxf86vm-dev xtrans-dev libxcb-render0-dev libxcb-render-util0-dev libxcb-xkb-dev libxcb-icccm4-dev libxcb-image0-dev libxcb-keysyms1-dev libxcb-randr0-dev libxcb-shape0-dev libxcb-sync-dev libxcb-xfixes0-dev libxcb-xinerama0-dev xkb-data libxcb-dri3-dev uuid-dev libxcb-util-dev libxkbcommon-x11-dev pkg-config flex bison g++-12 gcc-12 -y - sudo apt install g++-12 gcc-12 -y - sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-12 12 - sudo update-alternatives --install /usr/bin/g++ g++ /usr/bin/g++-12 12 + # NOTE: Due to what are probably github issues, we have to remove the cache and reconfigure before the rest. + # This is maybe because grub caches the disk it uses last time, which is recreated each time. + - name: Install Linux system requirements + if: ${{ runner.os == 'Linux' }} + run: | + sudo rm /var/cache/debconf/config.dat + sudo dpkg --configure -a + sudo add-apt-repository ppa:ubuntu-toolchain-r/test -y + sudo apt update + sudo apt upgrade + sudo apt install efibootmgr build-essential checkinstall libegl-dev zlib1g-dev libssl-dev ninja-build autoconf libx11-dev libx11-xcb-dev libfontenc-dev libice-dev libsm-dev libxau-dev libxaw7-dev libxcomposite-dev libxcursor-dev libxdamage-dev libxdmcp-dev libxext-dev libxfixes-dev libxi-dev libxinerama-dev libxkbfile-dev libxmu-dev libxmuu-dev libxpm-dev libxrandr-dev libxrender-dev libxres-dev libxss-dev libxt-dev libxtst-dev libxv-dev libxvmc-dev libxxf86vm-dev xtrans-dev libxcb-render0-dev libxcb-render-util0-dev libxcb-xkb-dev libxcb-icccm4-dev libxcb-image0-dev libxcb-keysyms1-dev libxcb-randr0-dev libxcb-shape0-dev libxcb-sync-dev libxcb-xfixes0-dev libxcb-xinerama0-dev xkb-data libxcb-dri3-dev uuid-dev libxcb-util-dev libxkbcommon-x11-dev pkg-config flex bison g++-12 gcc-12 -y - - name: Create the default Conan profile - run: conan profile new default --detect --force + - name: Install GCC-13 + run: | + sudo apt install g++-13 gcc-13 -y + sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-13 13 + sudo update-alternatives --install /usr/bin/g++ g++ /usr/bin/g++-13 13 - - name: Get Conan configuration - run: conan config install https://github.com/Ultimaker/conan-config.git + - name: Create the default Conan profile + run: conan profile new default --detect --force - - name: generate the files using Conan install - run: conan install . --build=missing --update -o cura:devtools=True + - name: Get Conan configuration + run: | + conan config install https://github.com/Ultimaker/conan-config.git + conan config install https://github.com/Ultimaker/conan-config.git -a "-b runner/${{ runner.os }}/${{ runner.arch }}" - - uses: stefanzweifel/git-auto-commit-action@v4 - with: - file_pattern: resources/i18n/*.po resources/i18n/*.pot - status_options: --untracked-files=no - commit_message: update translations + - name: generate the files using Conan install + run: conan install . --build=missing --update -o cura:devtools=True + + - uses: stefanzweifel/git-auto-commit-action@v4 + with: + file_pattern: resources/i18n/*.po resources/i18n/*.pot + status_options: --untracked-files=no + commit_message: update translations diff --git a/cura/Arranging/ArrangeObjectsJob.py b/cura/Arranging/ArrangeObjectsJob.py index 6ba6717191..48d2436482 100644 --- a/cura/Arranging/ArrangeObjectsJob.py +++ b/cura/Arranging/ArrangeObjectsJob.py @@ -8,17 +8,20 @@ from UM.Logger import Logger from UM.Message import Message from UM.Scene.SceneNode import SceneNode from UM.i18n import i18nCatalog -from cura.Arranging.Nest2DArrange import arrange +from cura.Arranging.GridArrange import GridArrange +from cura.Arranging.Nest2DArrange import Nest2DArrange i18n_catalog = i18nCatalog("cura") class ArrangeObjectsJob(Job): - def __init__(self, nodes: List[SceneNode], fixed_nodes: List[SceneNode], min_offset = 8) -> None: + def __init__(self, nodes: List[SceneNode], fixed_nodes: List[SceneNode], min_offset = 8, + *, grid_arrange: bool = False) -> None: super().__init__() self._nodes = nodes self._fixed_nodes = fixed_nodes self._min_offset = min_offset + self._grid_arrange = grid_arrange def run(self): found_solution_for_all = False @@ -29,10 +32,18 @@ class ArrangeObjectsJob(Job): title = i18n_catalog.i18nc("@info:title", "Finding Location")) status_message.show() + if self._grid_arrange: + arranger = GridArrange(self._nodes, Application.getInstance().getBuildVolume(), self._fixed_nodes) + else: + arranger = Nest2DArrange(self._nodes, Application.getInstance().getBuildVolume(), self._fixed_nodes, + factor=1000) + + found_solution_for_all = False try: - found_solution_for_all = arrange(self._nodes, Application.getInstance().getBuildVolume(), self._fixed_nodes) + found_solution_for_all = arranger.arrange() except: # If the thread crashes, the message should still close - Logger.logException("e", "Unable to arrange the objects on the buildplate. The arrange algorithm has crashed.") + Logger.logException("e", + "Unable to arrange the objects on the buildplate. The arrange algorithm has crashed.") status_message.hide() diff --git a/cura/Arranging/Arranger.py b/cura/Arranging/Arranger.py new file mode 100644 index 0000000000..f7f9870cf9 --- /dev/null +++ b/cura/Arranging/Arranger.py @@ -0,0 +1,28 @@ +from typing import List, TYPE_CHECKING, Optional, Tuple, Set + +if TYPE_CHECKING: + from UM.Operations.GroupedOperation import GroupedOperation + + +class Arranger: + def createGroupOperationForArrange(self, *, add_new_nodes_in_scene: bool = False) -> Tuple["GroupedOperation", int]: + """ + Find placement for a set of scene nodes, but don't actually move them just yet. + :param add_new_nodes_in_scene: Whether to create new scene nodes before applying the transformations and rotations + :return: tuple (found_solution_for_all, node_items) + WHERE + found_solution_for_all: Whether the algorithm found a place on the buildplate for all the objects + node_items: A list of the nodes return by libnest2d, which contain the new positions on the buildplate + """ + raise NotImplementedError + + def arrange(self, *, add_new_nodes_in_scene: bool = False) -> bool: + """ + Find placement for a set of scene nodes, and move them by using a single grouped operation. + :param add_new_nodes_in_scene: Whether to create new scene nodes before applying the transformations and rotations + :return: found_solution_for_all: Whether the algorithm found a place on the buildplate for all the objects + """ + grouped_operation, not_fit_count = self.createGroupOperationForArrange( + add_new_nodes_in_scene=add_new_nodes_in_scene) + grouped_operation.push() + return not_fit_count == 0 diff --git a/cura/Arranging/GridArrange.py b/cura/Arranging/GridArrange.py new file mode 100644 index 0000000000..493c81b27c --- /dev/null +++ b/cura/Arranging/GridArrange.py @@ -0,0 +1,331 @@ +import math +from typing import List, TYPE_CHECKING, Tuple, Set + +if TYPE_CHECKING: + from UM.Scene.SceneNode import SceneNode + from cura.BuildVolume import BuildVolume + +from UM.Application import Application +from UM.Math.Vector import Vector +from UM.Operations.AddSceneNodeOperation import AddSceneNodeOperation +from UM.Operations.GroupedOperation import GroupedOperation +from UM.Operations.TranslateOperation import TranslateOperation +from cura.Arranging.Arranger import Arranger + + +class GridArrange(Arranger): + def __init__(self, nodes_to_arrange: List["SceneNode"], build_volume: "BuildVolume", fixed_nodes: List["SceneNode"] = None): + if fixed_nodes is None: + fixed_nodes = [] + self._nodes_to_arrange = nodes_to_arrange + self._build_volume = build_volume + self._build_volume_bounding_box = build_volume.getBoundingBox() + self._fixed_nodes = fixed_nodes + + self._margin_x: float = 1 + self._margin_y: float = 1 + + self._grid_width = 0 + self._grid_height = 0 + for node in self._nodes_to_arrange: + bounding_box = node.getBoundingBox() + self._grid_width = max(self._grid_width, bounding_box.width) + self._grid_height = max(self._grid_height, bounding_box.depth) + self._grid_width += self._margin_x + self._grid_height += self._margin_y + + # Round up the grid size to the nearest cm + grid_precision = 10 # 1cm + self._grid_width = math.ceil(self._grid_width / grid_precision) * grid_precision + self._grid_height = math.ceil(self._grid_height / grid_precision) * grid_precision + + self._offset_x = 0 + self._offset_y = 0 + self._findOptimalGridOffset() + + coord_initial_leftover_x = self._build_volume_bounding_box.right + 2 * self._grid_width + coord_initial_leftover_y = (self._build_volume_bounding_box.back + self._build_volume_bounding_box.front) * 0.5 + self._initial_leftover_grid_x, self._initial_leftover_grid_y = self._coordSpaceToGridSpace( + coord_initial_leftover_x, coord_initial_leftover_y) + self._initial_leftover_grid_x = math.floor(self._initial_leftover_grid_x) + self._initial_leftover_grid_y = math.floor(self._initial_leftover_grid_y) + + # Find grid indexes that intersect with fixed objects + self._fixed_nodes_grid_ids = set() + for node in self._fixed_nodes: + self._fixed_nodes_grid_ids = self._fixed_nodes_grid_ids.union( + self._intersectingGridIdxInclusive(node.getBoundingBox())) + + #grid indexes that are in disallowed area + for polygon in self._build_volume.getDisallowedAreas(): + self._fixed_nodes_grid_ids = self._fixed_nodes_grid_ids.union( + self._getIntersectingGridIdForPolygon(polygon)) + + self._build_plate_grid_ids = self._intersectingGridIdxExclusive(self._build_volume_bounding_box) + + # Filter out the corner grid squares if the build plate shape is elliptic + if self._build_volume.getShape() == "elliptic": + self._build_plate_grid_ids = set( + filter(lambda grid_id: self._checkGridUnderDiscSpace(grid_id[0], grid_id[1]), + self._build_plate_grid_ids)) + + self._allowed_grid_idx = self._build_plate_grid_ids.difference(self._fixed_nodes_grid_ids) + + def createGroupOperationForArrange(self, *, add_new_nodes_in_scene: bool = False) -> Tuple[GroupedOperation, int]: + # Find the sequence in which items are placed + coord_build_plate_center_x = self._build_volume_bounding_box.width * 0.5 + self._build_volume_bounding_box.left + coord_build_plate_center_y = self._build_volume_bounding_box.depth * 0.5 + self._build_volume_bounding_box.back + grid_build_plate_center_x, grid_build_plate_center_y = self._coordSpaceToGridSpace(coord_build_plate_center_x, + coord_build_plate_center_y) + + sequence: List[Tuple[int, int]] = list(self._allowed_grid_idx) + sequence.sort(key=lambda grid_id: (grid_build_plate_center_x - grid_id[0]) ** 2 + ( + grid_build_plate_center_y - grid_id[1]) ** 2) + scene_root = Application.getInstance().getController().getScene().getRoot() + grouped_operation = GroupedOperation() + + for grid_id, node in zip(sequence, self._nodes_to_arrange): + if add_new_nodes_in_scene: + grouped_operation.addOperation(AddSceneNodeOperation(node, scene_root)) + grid_x, grid_y = grid_id + operation = self._moveNodeOnGrid(node, grid_x, grid_y) + grouped_operation.addOperation(operation) + + leftover_nodes = self._nodes_to_arrange[len(sequence):] + + left_over_grid_y = self._initial_leftover_grid_y + for node in leftover_nodes: + if add_new_nodes_in_scene: + grouped_operation.addOperation(AddSceneNodeOperation(node, scene_root)) + # find the first next grid position that isn't occupied by a fixed node + while (self._initial_leftover_grid_x, left_over_grid_y) in self._fixed_nodes_grid_ids: + left_over_grid_y = left_over_grid_y - 1 + + operation = self._moveNodeOnGrid(node, self._initial_leftover_grid_x, left_over_grid_y) + grouped_operation.addOperation(operation) + left_over_grid_y = left_over_grid_y - 1 + + return grouped_operation, len(leftover_nodes) + + def _findOptimalGridOffset(self): + if len(self._fixed_nodes) == 0: + self._offset_x = 0 + self._offset_y = 0 + return + + if len(self._fixed_nodes) == 1: + center_grid_x = 0.5 * self._grid_width + self._build_volume_bounding_box.left + center_grid_y = 0.5 * self._grid_height + self._build_volume_bounding_box.back + + bounding_box = self._fixed_nodes[0].getBoundingBox() + center_node_x = (bounding_box.left + bounding_box.right) * 0.5 + center_node_y = (bounding_box.back + bounding_box.front) * 0.5 + + self._offset_x = center_node_x - center_grid_x + self._offset_y = center_node_y - center_grid_y + + return + + # If there are multiple fixed nodes, an optimal solution is not always possible + # We will try to find an offset that minimizes the number of grid intersections + # with fixed nodes. The algorithm below achieves this by utilizing a scanline + # algorithm. In this algorithm each axis is solved separately as offsetting + # is completely independent in each axis. The comments explaining the algorithm + # below are for the x-axis, but the same applies for the y-axis. + # + # Each node either occupies ceil((node.right - node.right) / grid_width) or + # ceil((node.right - node.right) / grid_width) + 1 grid squares. We will call + # these the node's "footprint". + # + # ┌────────────────┐ + # minimum foot-print │ NODE │ + # └────────────────┘ + # │ grid 1 │ grid 2 │ grid 3 │ grid 4 | grid 5 | + # ┌────────────────┐ + # maximum foot-print │ NODE │ + # └────────────────┘ + # + # The algorithm will find the grid offset such that the number of nodes with + # a _minimal_ footprint is _maximized_. + + # The scanline algorithm works as follows, we create events for both end points + # of each node's footprint. The event have two properties, + # - the coordinate: the amount the endpoint can move to the + # left before it crosses a grid line + # - the change: either +1 or -1, indicating whether crossing the grid line + # would result in a minimal footprint node becoming a maximal footprint + class Event: + def __init__(self, coord: float, change: float): + self.coord = coord + self.change = change + + # create events for both the horizontal and vertical axis + events_horizontal: List[Event] = [] + events_vertical: List[Event] = [] + + for node in self._fixed_nodes: + bounding_box = node.getBoundingBox() + + left = bounding_box.left - self._build_volume_bounding_box.left + right = bounding_box.right - self._build_volume_bounding_box.left + back = bounding_box.back - self._build_volume_bounding_box.back + front = bounding_box.front - self._build_volume_bounding_box.back + + value_left = math.ceil(left / self._grid_width) * self._grid_width - left + value_right = math.ceil(right / self._grid_width) * self._grid_width - right + value_back = math.ceil(back / self._grid_height) * self._grid_height - back + value_front = math.ceil(front / self._grid_height) * self._grid_height - front + + # give nodes a weight according to their size. This + # weight is heuristically chosen to be proportional to + # the number of grid squares the node-boundary occupies + weight = bounding_box.width + bounding_box.depth + + events_horizontal.append(Event(value_left, weight)) + events_horizontal.append(Event(value_right, -weight)) + events_vertical.append(Event(value_back, weight)) + events_vertical.append(Event(value_front, -weight)) + + events_horizontal.sort(key=lambda event: event.coord) + events_vertical.sort(key=lambda event: event.coord) + + def findOptimalShiftAxis(events: List[Event], interval: float) -> float: + # executing the actual scanline algorithm + # iteratively go through events (left to right) and keep track of the + # current footprint. The optimal location is the one with the minimal + # footprint. If there are multiple locations with the same minimal + # footprint, the optimal location is the one with the largest range + # between the left and right endpoint of the footprint. + prev_offset = events[-1].coord - interval + current_minimal_footprint_count = 0 + + best_minimal_footprint_count = float('inf') + best_offset_span = float('-inf') + best_offset = 0.0 + + for event in events: + offset_span = event.coord - prev_offset + + if current_minimal_footprint_count < best_minimal_footprint_count or ( + current_minimal_footprint_count == best_minimal_footprint_count and offset_span > best_offset_span): + best_minimal_footprint_count = current_minimal_footprint_count + best_offset_span = offset_span + best_offset = event.coord + + current_minimal_footprint_count += event.change + prev_offset = event.coord + + return best_offset - best_offset_span * 0.5 + + center_grid_x = 0.5 * self._grid_width + center_grid_y = 0.5 * self._grid_height + + optimal_center_x = self._grid_width - findOptimalShiftAxis(events_horizontal, self._grid_width) + optimal_center_y = self._grid_height - findOptimalShiftAxis(events_vertical, self._grid_height) + + self._offset_x = optimal_center_x - center_grid_x + self._offset_y = optimal_center_y - center_grid_y + + def _moveNodeOnGrid(self, node: "SceneNode", grid_x: int, grid_y: int) -> "Operation.Operation": + coord_grid_x, coord_grid_y = self._gridSpaceToCoordSpace(grid_x, grid_y) + center_grid_x = coord_grid_x + (0.5 * self._grid_width) + center_grid_y = coord_grid_y + (0.5 * self._grid_height) + + bounding_box = node.getBoundingBox() + center_node_x = (bounding_box.left + bounding_box.right) * 0.5 + center_node_y = (bounding_box.back + bounding_box.front) * 0.5 + + delta_x = center_grid_x - center_node_x + delta_y = center_grid_y - center_node_y + + return TranslateOperation(node, Vector(delta_x, 0, delta_y)) + + def _getGridCornerPoints(self, bounding_box: "BoundingVolume") -> Tuple[float, float, float, float]: + coord_x1 = bounding_box.left + coord_x2 = bounding_box.right + coord_y1 = bounding_box.back + coord_y2 = bounding_box.front + grid_x1, grid_y1 = self._coordSpaceToGridSpace(coord_x1, coord_y1) + grid_x2, grid_y2 = self._coordSpaceToGridSpace(coord_x2, coord_y2) + return grid_x1, grid_y1, grid_x2, grid_y2 + + def _getIntersectingGridIdForPolygon(self, polygon)-> Set[Tuple[int, int]]: + # (x0, y0) + # | + # v + # ┌─────────────┐ + # │ │ + # │ │ + # └─────────────┘ < (x1, y1) + x0 = float('inf') + y0 = float('inf') + x1 = float('-inf') + y1 = float('-inf') + grid_idx = set() + for [x, y] in polygon.getPoints(): + x0 = min(x0, x) + y0 = min(y0, y) + x1 = max(x1, x) + y1 = max(y1, y) + grid_x1, grid_y1 = self._coordSpaceToGridSpace(x0, y0) + grid_x2, grid_y2 = self._coordSpaceToGridSpace(x1, y1) + + for grid_x in range(math.floor(grid_x1), math.ceil(grid_x2)): + for grid_y in range(math.floor(grid_y1), math.ceil(grid_y2)): + grid_idx.add((grid_x, grid_y)) + return grid_idx + + def _intersectingGridIdxInclusive(self, bounding_box: "BoundingVolume") -> Set[Tuple[int, int]]: + grid_x1, grid_y1, grid_x2, grid_y2 = self._getGridCornerPoints(bounding_box) + grid_idx = set() + for grid_x in range(math.floor(grid_x1), math.ceil(grid_x2)): + for grid_y in range(math.floor(grid_y1), math.ceil(grid_y2)): + grid_idx.add((grid_x, grid_y)) + return grid_idx + + def _intersectingGridIdxExclusive(self, bounding_box: "BoundingVolume") -> Set[Tuple[int, int]]: + grid_x1, grid_y1, grid_x2, grid_y2 = self._getGridCornerPoints(bounding_box) + grid_idx = set() + for grid_x in range(math.ceil(grid_x1), math.floor(grid_x2)): + for grid_y in range(math.ceil(grid_y1), math.floor(grid_y2)): + grid_idx.add((grid_x, grid_y)) + return grid_idx + + def _gridSpaceToCoordSpace(self, x: float, y: float) -> Tuple[float, float]: + grid_x = x * self._grid_width + self._build_volume_bounding_box.left + self._offset_x + grid_y = y * self._grid_height + self._build_volume_bounding_box.back + self._offset_y + return grid_x, grid_y + + def _coordSpaceToGridSpace(self, grid_x: float, grid_y: float) -> Tuple[float, float]: + coord_x = (grid_x - self._build_volume_bounding_box.left - self._offset_x) / self._grid_width + coord_y = (grid_y - self._build_volume_bounding_box.back - self._offset_y) / self._grid_height + return coord_x, coord_y + + def _checkGridUnderDiscSpace(self, grid_x: int, grid_y: int) -> bool: + left, back = self._gridSpaceToCoordSpace(grid_x, grid_y) + right, front = self._gridSpaceToCoordSpace(grid_x + 1, grid_y + 1) + corners = [(left, back), (right, back), (right, front), (left, front)] + return all([self._checkPointUnderDiscSpace(x, y) for x, y in corners]) + + def _checkPointUnderDiscSpace(self, x: float, y: float) -> bool: + disc_x, disc_y = self._coordSpaceToDiscSpace(x, y) + distance_to_center_squared = disc_x ** 2 + disc_y ** 2 + return distance_to_center_squared <= 1.0 + + def _coordSpaceToDiscSpace(self, x: float, y: float) -> Tuple[float, float]: + # Transform coordinate system to + # + # coord_build_plate_left = -1 + # | coord_build_plate_right = 1 + # v (0,1) v + # ┌───────┬───────┐ < coord_build_plate_back = -1 + # │ │ │ + # │ │(0,0) │ + # (-1,0)├───────o───────┤(1,0) + # │ │ │ + # │ │ │ + # └───────┴───────┘ < coord_build_plate_front = +1 + # (0,-1) + disc_x = ((x - self._build_volume_bounding_box.left) / self._build_volume_bounding_box.width) * 2.0 - 1.0 + disc_y = ((y - self._build_volume_bounding_box.back) / self._build_volume_bounding_box.depth) * 2.0 - 1.0 + return disc_x, disc_y diff --git a/cura/Arranging/Nest2DArrange.py b/cura/Arranging/Nest2DArrange.py index 21427f1194..5fcd36c1a3 100644 --- a/cura/Arranging/Nest2DArrange.py +++ b/cura/Arranging/Nest2DArrange.py @@ -15,149 +15,137 @@ from UM.Operations.AddSceneNodeOperation import AddSceneNodeOperation from UM.Operations.GroupedOperation import GroupedOperation from UM.Operations.RotateOperation import RotateOperation from UM.Operations.TranslateOperation import TranslateOperation - +from cura.Arranging.Arranger import Arranger if TYPE_CHECKING: from UM.Scene.SceneNode import SceneNode from cura.BuildVolume import BuildVolume -def findNodePlacement(nodes_to_arrange: List["SceneNode"], build_volume: "BuildVolume", fixed_nodes: Optional[List["SceneNode"]] = None, factor = 10000) -> Tuple[bool, List[Item]]: - """ - Find placement for a set of scene nodes, but don't actually move them just yet. - :param nodes_to_arrange: The list of nodes that need to be moved. - :param build_volume: The build volume that we want to place the nodes in. It gets size & disallowed areas from this. - :param fixed_nodes: List of nods that should not be moved, but should be used when deciding where the others nodes - are placed. - :param factor: The library that we use is int based. This factor defines how accurate we want it to be. +class Nest2DArrange(Arranger): + def __init__(self, + nodes_to_arrange: List["SceneNode"], + build_volume: "BuildVolume", + fixed_nodes: Optional[List["SceneNode"]] = None, + *, + factor: int = 10000, + lock_rotation: bool = False): + """ + :param nodes_to_arrange: The list of nodes that need to be moved. + :param build_volume: The build volume that we want to place the nodes in. It gets size & disallowed areas from this. + :param fixed_nodes: List of nods that should not be moved, but should be used when deciding where the others nodes + are placed. + :param factor: The library that we use is int based. This factor defines how accuracte we want it to be. + :param lock_rotation: If set to true the orientation of the object will remain the same + """ + super().__init__() + self._nodes_to_arrange = nodes_to_arrange + self._build_volume = build_volume + self._fixed_nodes = fixed_nodes + self._factor = factor + self._lock_rotation = lock_rotation - :return: tuple (found_solution_for_all, node_items) - WHERE - found_solution_for_all: Whether the algorithm found a place on the buildplate for all the objects - node_items: A list of the nodes return by libnest2d, which contain the new positions on the buildplate - """ - spacing = int(1.5 * factor) # 1.5mm spacing. + def findNodePlacement(self) -> Tuple[bool, List[Item]]: + spacing = int(1.5 * self._factor) # 1.5mm spacing. - machine_width = build_volume.getWidth() - machine_depth = build_volume.getDepth() - build_plate_bounding_box = Box(int(machine_width * factor), int(machine_depth * factor)) + machine_width = self._build_volume.getWidth() + machine_depth = self._build_volume.getDepth() + build_plate_bounding_box = Box(int(machine_width * self._factor), int(machine_depth * self._factor)) - if fixed_nodes is None: - fixed_nodes = [] + if self._fixed_nodes is None: + self._fixed_nodes = [] - # Add all the items we want to arrange - node_items = [] - for node in nodes_to_arrange: - hull_polygon = node.callDecoration("getConvexHull") - if not hull_polygon or hull_polygon.getPoints is None: - Logger.log("w", "Object {} cannot be arranged because it has no convex hull.".format(node.getName())) - continue - converted_points = [] - for point in hull_polygon.getPoints(): - converted_points.append(Point(int(point[0] * factor), int(point[1] * factor))) - item = Item(converted_points) - node_items.append(item) - - # Use a tiny margin for the build_plate_polygon (the nesting doesn't like overlapping disallowed areas) - half_machine_width = 0.5 * machine_width - 1 - half_machine_depth = 0.5 * machine_depth - 1 - build_plate_polygon = Polygon(numpy.array([ - [half_machine_width, -half_machine_depth], - [-half_machine_width, -half_machine_depth], - [-half_machine_width, half_machine_depth], - [half_machine_width, half_machine_depth] - ], numpy.float32)) - - disallowed_areas = build_volume.getDisallowedAreas() - num_disallowed_areas_added = 0 - for area in disallowed_areas: - converted_points = [] - - # Clip the disallowed areas so that they don't overlap the bounding box (The arranger chokes otherwise) - clipped_area = area.intersectionConvexHulls(build_plate_polygon) - - if clipped_area.getPoints() is not None and len(clipped_area.getPoints()) > 2: # numpy array has to be explicitly checked against None - for point in clipped_area.getPoints(): - converted_points.append(Point(int(point[0] * factor), int(point[1] * factor))) - - disallowed_area = Item(converted_points) - disallowed_area.markAsDisallowedAreaInBin(0) - node_items.append(disallowed_area) - num_disallowed_areas_added += 1 - - for node in fixed_nodes: - converted_points = [] - hull_polygon = node.callDecoration("getConvexHull") - - if hull_polygon is not None and hull_polygon.getPoints() is not None and len(hull_polygon.getPoints()) > 2: # numpy array has to be explicitly checked against None + # Add all the items we want to arrange + node_items = [] + for node in self._nodes_to_arrange: + hull_polygon = node.callDecoration("getConvexHull") + if not hull_polygon or hull_polygon.getPoints is None: + Logger.log("w", "Object {} cannot be arranged because it has no convex hull.".format(node.getName())) + continue + converted_points = [] for point in hull_polygon.getPoints(): - converted_points.append(Point(int(point[0] * factor), int(point[1] * factor))) + converted_points.append(Point(int(point[0] * self._factor), int(point[1] * self._factor))) item = Item(converted_points) - item.markAsFixedInBin(0) node_items.append(item) - num_disallowed_areas_added += 1 - config = NfpConfig() - config.accuracy = 1.0 - config.alignment = NfpConfig.Alignment.DONT_ALIGN + # Use a tiny margin for the build_plate_polygon (the nesting doesn't like overlapping disallowed areas) + half_machine_width = 0.5 * machine_width - 1 + half_machine_depth = 0.5 * machine_depth - 1 + build_plate_polygon = Polygon(numpy.array([ + [half_machine_width, -half_machine_depth], + [-half_machine_width, -half_machine_depth], + [-half_machine_width, half_machine_depth], + [half_machine_width, half_machine_depth] + ], numpy.float32)) - num_bins = nest(node_items, build_plate_bounding_box, spacing, config) + disallowed_areas = self._build_volume.getDisallowedAreas() + num_disallowed_areas_added = 0 + for area in disallowed_areas: + converted_points = [] - # Strip the fixed items (previously placed) and the disallowed areas from the results again. - node_items = list(filter(lambda item: not item.isFixed(), node_items)) + # Clip the disallowed areas so that they don't overlap the bounding box (The arranger chokes otherwise) + clipped_area = area.intersectionConvexHulls(build_plate_polygon) - found_solution_for_all = num_bins == 1 + if clipped_area.getPoints() is not None and len( + clipped_area.getPoints()) > 2: # numpy array has to be explicitly checked against None + for point in clipped_area.getPoints(): + converted_points.append(Point(int(point[0] * self._factor), int(point[1] * self._factor))) - return found_solution_for_all, node_items + disallowed_area = Item(converted_points) + disallowed_area.markAsDisallowedAreaInBin(0) + node_items.append(disallowed_area) + num_disallowed_areas_added += 1 + for node in self._fixed_nodes: + converted_points = [] + hull_polygon = node.callDecoration("getConvexHull") -def createGroupOperationForArrange(nodes_to_arrange: List["SceneNode"], - build_volume: "BuildVolume", - fixed_nodes: Optional[List["SceneNode"]] = None, - factor = 10000, - add_new_nodes_in_scene: bool = False) -> Tuple[GroupedOperation, int]: - scene_root = Application.getInstance().getController().getScene().getRoot() - found_solution_for_all, node_items = findNodePlacement(nodes_to_arrange, build_volume, fixed_nodes, factor) + if hull_polygon is not None and hull_polygon.getPoints() is not None and len( + hull_polygon.getPoints()) > 2: # numpy array has to be explicitly checked against None + for point in hull_polygon.getPoints(): + converted_points.append(Point(int(point[0] * self._factor), int(point[1] * self._factor))) + item = Item(converted_points) + item.markAsFixedInBin(0) + node_items.append(item) + num_disallowed_areas_added += 1 - not_fit_count = 0 - grouped_operation = GroupedOperation() - for node, node_item in zip(nodes_to_arrange, node_items): - if add_new_nodes_in_scene: - grouped_operation.addOperation(AddSceneNodeOperation(node, scene_root)) + config = NfpConfig() + config.accuracy = 1.0 + config.alignment = NfpConfig.Alignment.DONT_ALIGN + if self._lock_rotation: + config.rotations = [0.0] - if node_item.binId() == 0: - # We found a spot for it - rotation_matrix = Matrix() - rotation_matrix.setByRotationAxis(node_item.rotation(), Vector(0, -1, 0)) - grouped_operation.addOperation(RotateOperation(node, Quaternion.fromMatrix(rotation_matrix))) - grouped_operation.addOperation(TranslateOperation(node, Vector(node_item.translation().x() / factor, 0, - node_item.translation().y() / factor))) - else: - # We didn't find a spot - grouped_operation.addOperation( - TranslateOperation(node, Vector(200, node.getWorldPosition().y, -not_fit_count * 20), set_position = True)) - not_fit_count += 1 + num_bins = nest(node_items, build_plate_bounding_box, spacing, config) - return grouped_operation, not_fit_count + # Strip the fixed items (previously placed) and the disallowed areas from the results again. + node_items = list(filter(lambda item: not item.isFixed(), node_items)) + found_solution_for_all = num_bins == 1 -def arrange(nodes_to_arrange: List["SceneNode"], - build_volume: "BuildVolume", - fixed_nodes: Optional[List["SceneNode"]] = None, - factor = 10000, - add_new_nodes_in_scene: bool = False) -> bool: - """ - Find placement for a set of scene nodes, and move them by using a single grouped operation. - :param nodes_to_arrange: The list of nodes that need to be moved. - :param build_volume: The build volume that we want to place the nodes in. It gets size & disallowed areas from this. - :param fixed_nodes: List of nods that should not be moved, but should be used when deciding where the others nodes - are placed. - :param factor: The library that we use is int based. This factor defines how accuracte we want it to be. - :param add_new_nodes_in_scene: Whether to create new scene nodes before applying the transformations and rotations + return found_solution_for_all, node_items - :return: found_solution_for_all: Whether the algorithm found a place on the buildplate for all the objects - """ + def createGroupOperationForArrange(self, *, add_new_nodes_in_scene: bool = False) -> Tuple[GroupedOperation, int]: + scene_root = Application.getInstance().getController().getScene().getRoot() + found_solution_for_all, node_items = self.findNodePlacement() - grouped_operation, not_fit_count = createGroupOperationForArrange(nodes_to_arrange, build_volume, fixed_nodes, factor, add_new_nodes_in_scene) - grouped_operation.push() - return not_fit_count == 0 + not_fit_count = 0 + grouped_operation = GroupedOperation() + for node, node_item in zip(self._nodes_to_arrange, node_items): + if add_new_nodes_in_scene: + grouped_operation.addOperation(AddSceneNodeOperation(node, scene_root)) + + if node_item.binId() == 0: + # We found a spot for it + rotation_matrix = Matrix() + rotation_matrix.setByRotationAxis(node_item.rotation(), Vector(0, -1, 0)) + grouped_operation.addOperation(RotateOperation(node, Quaternion.fromMatrix(rotation_matrix))) + grouped_operation.addOperation( + TranslateOperation(node, Vector(node_item.translation().x() / self._factor, 0, + node_item.translation().y() / self._factor))) + else: + # We didn't find a spot + grouped_operation.addOperation( + TranslateOperation(node, Vector(200, node.getWorldPosition().y, -not_fit_count * 20), set_position = True)) + not_fit_count += 1 + + return grouped_operation, not_fit_count diff --git a/cura/BuildVolume.py b/cura/BuildVolume.py index 0d6ecf5810..045156dcce 100755 --- a/cura/BuildVolume.py +++ b/cura/BuildVolume.py @@ -203,6 +203,9 @@ class BuildVolume(SceneNode): if shape: self._shape = shape + def getShape(self) -> str: + return self._shape + def getDiagonalSize(self) -> float: """Get the length of the 3D diagonal through the build volume. diff --git a/cura/CuraActions.py b/cura/CuraActions.py index 6c2d3f4cb8..9a61a1c4f0 100644 --- a/cura/CuraActions.py +++ b/cura/CuraActions.py @@ -22,7 +22,10 @@ from cura.Operations.SetParentOperation import SetParentOperation from cura.MultiplyObjectsJob import MultiplyObjectsJob from cura.Settings.SetObjectExtruderOperation import SetObjectExtruderOperation from cura.Settings.ExtruderManager import ExtruderManager -from cura.Arranging.Nest2DArrange import createGroupOperationForArrange + +from cura.Arranging.GridArrange import GridArrange +from cura.Arranging.Nest2DArrange import Nest2DArrange + from cura.Operations.SetBuildPlateNumberOperation import SetBuildPlateNumberOperation @@ -82,16 +85,25 @@ class CuraActions(QObject): center_operation = TranslateOperation(current_node, Vector(0, center_y, 0), set_position = True) operation.addOperation(center_operation) operation.push() - @pyqtSlot(int) def multiplySelection(self, count: int) -> None: """Multiply all objects in the selection + :param count: The number of times to multiply the selection. + """ + min_offset = cura.CuraApplication.CuraApplication.getInstance().getBuildVolume().getEdgeDisallowedSize() + 2 # Allow for some rounding errors + job = MultiplyObjectsJob(Selection.getAllSelectedObjects(), count, min_offset = max(min_offset, 8)) + job.start() + + @pyqtSlot(int) + def multiplySelectionToGrid(self, count: int) -> None: + """Multiply all objects in the selection :param count: The number of times to multiply the selection. """ min_offset = cura.CuraApplication.CuraApplication.getInstance().getBuildVolume().getEdgeDisallowedSize() + 2 # Allow for some rounding errors - job = MultiplyObjectsJob(Selection.getAllSelectedObjects(), count, min_offset = max(min_offset, 8)) + job = MultiplyObjectsJob(Selection.getAllSelectedObjects(), count, min_offset=max(min_offset, 8), + grid_arrange=True) job.start() @pyqtSlot() @@ -229,9 +241,9 @@ class CuraActions(QObject): if node.callDecoration("isSliceable"): fixed_nodes.append(node) # Add the new nodes to the scene, and arrange them - group_operation, not_fit_count = createGroupOperationForArrange(nodes, application.getBuildVolume(), - fixed_nodes, factor=10000, - add_new_nodes_in_scene=True) + + arranger = GridArrange(nodes, application.getBuildVolume(), fixed_nodes) + group_operation, not_fit_count = arranger.createGroupOperationForArrange(add_new_nodes_in_scene = True) group_operation.push() # deselect currently selected nodes, and select the new nodes diff --git a/cura/CuraApplication.py b/cura/CuraApplication.py index 0de3ccc780..a2edde95a8 100755 --- a/cura/CuraApplication.py +++ b/cura/CuraApplication.py @@ -54,7 +54,6 @@ from cura import ApplicationMetadata from cura.API import CuraAPI from cura.API.Account import Account from cura.Arranging.ArrangeObjectsJob import ArrangeObjectsJob -from cura.Arranging.Nest2DArrange import arrange from cura.Machines.MachineErrorChecker import MachineErrorChecker from cura.Machines.Models.BuildPlateModel import BuildPlateModel from cura.Machines.Models.CustomQualityProfilesDropDownMenuModel import CustomQualityProfilesDropDownMenuModel @@ -115,6 +114,7 @@ from . import CameraAnimation from . import CuraActions from . import PlatformPhysics from . import PrintJobPreviewImageProvider +from .Arranging.Nest2DArrange import Nest2DArrange from .AutoSave import AutoSave from .Machines.Models.CompatibleMachineModel import CompatibleMachineModel from .Machines.Models.MachineListModel import MachineListModel @@ -1444,6 +1444,13 @@ class CuraApplication(QtApplication): # Single build plate @pyqtSlot() def arrangeAll(self) -> None: + self._arrangeAll(grid_arrangement = False) + + @pyqtSlot() + def arrangeAllInGrid(self) -> None: + self._arrangeAll(grid_arrangement = True) + + def _arrangeAll(self, *, grid_arrangement: bool) -> None: nodes_to_arrange = [] active_build_plate = self.getMultiBuildPlateModel().activeBuildPlate locked_nodes = [] @@ -1473,17 +1480,17 @@ class CuraApplication(QtApplication): locked_nodes.append(node) else: nodes_to_arrange.append(node) - self.arrange(nodes_to_arrange, locked_nodes) + self.arrange(nodes_to_arrange, locked_nodes, grid_arrangement = grid_arrangement) - def arrange(self, nodes: List[SceneNode], fixed_nodes: List[SceneNode]) -> None: + def arrange(self, nodes: List[SceneNode], fixed_nodes: List[SceneNode], *, grid_arrangement: bool = False) -> None: """Arrange a set of nodes given a set of fixed nodes :param nodes: nodes that we have to place :param fixed_nodes: nodes that are placed in the arranger before finding spots for nodes + :param grid_arrangement: If set to true if objects are to be placed in a grid """ - min_offset = self.getBuildVolume().getEdgeDisallowedSize() + 2 # Allow for some rounding errors - job = ArrangeObjectsJob(nodes, fixed_nodes, min_offset = max(min_offset, 8)) + job = ArrangeObjectsJob(nodes, fixed_nodes, min_offset = max(min_offset, 8), grid_arrange = grid_arrangement) job.start() @pyqtSlot() @@ -1970,7 +1977,8 @@ class CuraApplication(QtApplication): if select_models_on_load: Selection.add(node) try: - arrange(nodes_to_arrange, self.getBuildVolume(), fixed_nodes) + arranger = Nest2DArrange(nodes_to_arrange, self.getBuildVolume(), fixed_nodes) + arranger.arrange() except: Logger.logException("e", "Failed to arrange the models") diff --git a/cura/MultiplyObjectsJob.py b/cura/MultiplyObjectsJob.py index 1446ae687e..889b6f5d1a 100644 --- a/cura/MultiplyObjectsJob.py +++ b/cura/MultiplyObjectsJob.py @@ -14,17 +14,19 @@ from UM.Operations.TranslateOperation import TranslateOperation from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator from UM.Scene.SceneNode import SceneNode from UM.i18n import i18nCatalog -from cura.Arranging.Nest2DArrange import arrange, createGroupOperationForArrange +from cura.Arranging.GridArrange import GridArrange +from cura.Arranging.Nest2DArrange import Nest2DArrange i18n_catalog = i18nCatalog("cura") class MultiplyObjectsJob(Job): - def __init__(self, objects, count, min_offset = 8): + def __init__(self, objects, count: int, min_offset: int = 8 ,* , grid_arrange: bool = False): super().__init__() self._objects = objects - self._count = count - self._min_offset = min_offset + self._count: int = count + self._min_offset: int = min_offset + self._grid_arrange: bool = grid_arrange def run(self) -> None: status_message = Message(i18n_catalog.i18nc("@info:status", "Multiplying and placing objects"), lifetime = 0, @@ -39,7 +41,7 @@ class MultiplyObjectsJob(Job): root = scene.getRoot() - processed_nodes = [] # type: List[SceneNode] + processed_nodes: List[SceneNode] = [] nodes = [] fixed_nodes = [] @@ -76,12 +78,12 @@ class MultiplyObjectsJob(Job): found_solution_for_all = True group_operation = GroupedOperation() if nodes: - group_operation, not_fit_count = createGroupOperationForArrange(nodes, - Application.getInstance().getBuildVolume(), - fixed_nodes, - factor = 10000, - add_new_nodes_in_scene = True) - found_solution_for_all = not_fit_count == 0 + if self._grid_arrange: + arranger = GridArrange(nodes, Application.getInstance().getBuildVolume(), fixed_nodes) + else: + arranger = Nest2DArrange(nodes, Application.getInstance().getBuildVolume(), fixed_nodes, factor=1000) + + group_operation, not_fit_count = arranger.createGroupOperationForArrange(add_new_nodes_in_scene=True) if nodes_to_add_without_arrange: for nested_node in nodes_to_add_without_arrange: diff --git a/resources/qml/Actions.qml b/resources/qml/Actions.qml index f945b1c11d..65888b3493 100644 --- a/resources/qml/Actions.qml +++ b/resources/qml/Actions.qml @@ -41,7 +41,7 @@ Item property alias deleteAll: deleteAllAction property alias reloadAll: reloadAllAction property alias arrangeAll: arrangeAllAction - property alias arrangeSelection: arrangeSelectionAction + property alias arrangeAllGrid: arrangeAllGridAction property alias resetAllTranslation: resetAllTranslationAction property alias resetAll: resetAllAction @@ -462,9 +462,10 @@ Item Action { - id: arrangeSelectionAction - text: catalog.i18nc("@action:inmenu menubar:edit","Arrange Selection") - onTriggered: Printer.arrangeSelection() + id: arrangeAllGridAction + text: catalog.i18nc("@action:inmenu menubar:edit","Arrange All Models in a grid") + onTriggered: Printer.arrangeAllInGrid() + shortcut: "Shift+Ctrl+R" } Action diff --git a/resources/qml/Menus/ContextMenu.qml b/resources/qml/Menus/ContextMenu.qml index d85703451f..2de2795a74 100644 --- a/resources/qml/Menus/ContextMenu.qml +++ b/resources/qml/Menus/ContextMenu.qml @@ -66,6 +66,7 @@ Cura.Menu Cura.MenuSeparator {} Cura.MenuItem { action: Cura.Actions.selectAll } Cura.MenuItem { action: Cura.Actions.arrangeAll } + Cura.MenuItem { action: Cura.Actions.arrangeAllGrid } Cura.MenuItem { action: Cura.Actions.deleteAll } Cura.MenuItem { action: Cura.Actions.reloadAll } Cura.MenuItem { action: Cura.Actions.resetAllTranslation } @@ -108,9 +109,7 @@ Cura.Menu height: UM.Theme.getSize("small_popup_dialog").height minimumWidth: UM.Theme.getSize("small_popup_dialog").width minimumHeight: UM.Theme.getSize("small_popup_dialog").height - - onAccepted: CuraActions.multiplySelection(copiesField.value) - + onAccepted: gridPlacementSelected.checked? CuraActions.multiplySelectionToGrid(copiesField.value) : CuraActions.multiplySelection(copiesField.value) buttonSpacing: UM.Theme.getSize("thin_margin").width rightButtons: @@ -127,28 +126,49 @@ Cura.Menu } ] - Row + Column { - spacing: UM.Theme.getSize("default_margin").width + spacing: UM.Theme.getSize("default_margin").height - UM.Label + Row { - text: catalog.i18nc("@label", "Number of Copies") - anchors.verticalCenter: copiesField.verticalCenter - width: contentWidth - wrapMode: Text.NoWrap + spacing: UM.Theme.getSize("default_margin").width + + UM.Label + { + text: catalog.i18nc("@label", "Number of Copies") + anchors.verticalCenter: copiesField.verticalCenter + width: contentWidth + wrapMode: Text.NoWrap + } + + Cura.SpinBox + { + id: copiesField + editable: true + focus: true + from: 1 + to: 99 + width: 2 * UM.Theme.getSize("button").width + value: 1 + } } - Cura.SpinBox + UM.CheckBox { - id: copiesField - editable: true - focus: true - from: 1 - to: 99 - width: 2 * UM.Theme.getSize("button").width - value: 1 + id: gridPlacementSelected + text: catalog.i18nc("@label", "Grid Placement") + + UM.ToolTip + { + visible: parent.hovered + targetPoint: Qt.point(parent.x + Math.round(parent.width / 2), parent.y) + x: 0 + y: parent.y + parent.height + UM.Theme.getSize("default_margin").height + tooltipText: catalog.i18nc("@info", "Multiply selected item and place them in a grid of build plate.") + } } + } } }