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 0d3ada2d97..caf2ee74d3 100644 --- a/.github/workflows/linux.yml +++ b/.github/workflows/linux.yml @@ -2,285 +2,277 @@ 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 - - - name: Write the run info - shell: python - run: | - import os - with open("run_info.sh", "w") as f: - f.writelines(f'echo "CURA_VERSION_FULL={os.environ["CURA_VERSION_FULL"]}" >> $GITHUB_ENV\n') - - - name: Upload the run info - uses: actions/upload-artifact@v3 - with: - name: linux-run-info - path: | - run_info.sh - 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 + + - name: Write the run info + shell: python + run: | + import os + with open("run_info.sh", "w") as f: + f.writelines(f'echo "CURA_VERSION_FULL={os.environ["CURA_VERSION_FULL"]}" >> $GITHUB_ENV\n') + - name: Upload the run info + uses: actions/upload-artifact@v3 + with: + name: linux-run-info + path: | + run_info.sh + 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..4caf472b5d --- /dev/null +++ b/cura/Arranging/GridArrange.py @@ -0,0 +1,347 @@ +import math +from typing import List, TYPE_CHECKING, Tuple, Set, Union + +if TYPE_CHECKING: + from UM.Scene.SceneNode import SceneNode + from cura.BuildVolume import BuildVolume + +from UM.Application import Application +from UM.Math.AxisAlignedBox import AxisAlignedBox +from UM.Math.Polygon import Polygon +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, this assures that new objects will + # be placed on integer offsets from each other + grid_precision = 10 # 1cm + rounded_grid_width = math.ceil(self._grid_width / grid_precision) * grid_precision + rounded_grid_height = math.ceil(self._grid_height / grid_precision) * grid_precision + + # The space added by the "grid precision rounding up" of the grid size + self._grid_round_margin_x = rounded_grid_width - self._grid_width + self._grid_round_margin_y = rounded_grid_height - self._grid_height + + self._grid_width = rounded_grid_width + self._grid_height = rounded_grid_height + + 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._intersectingGridIdxInclusive(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, + bounds: Union[AxisAlignedBox, Polygon], + *, + margin_x: float = 0.0, + margin_y: float = 0.0 + ) -> Tuple[float, float, float, float]: + if isinstance(bounds, AxisAlignedBox): + coord_x1 = bounds.left - margin_x + coord_x2 = bounds.right + margin_x + coord_y1 = bounds.back - margin_y + coord_y2 = bounds.front + margin_y + elif isinstance(bounds, Polygon): + coord_x1 = float('inf') + coord_y1 = float('inf') + coord_x2 = float('-inf') + coord_y2 = float('-inf') + for x, y in bounds.getPoints(): + coord_x1 = min(coord_x1, x) + coord_y1 = min(coord_y1, y) + coord_x2 = max(coord_x2, x) + coord_y2 = max(coord_y2, y) + else: + raise TypeError("bounds must be either an AxisAlignedBox or a Polygon") + + coord_x1 -= margin_x + coord_x2 += margin_x + coord_y1 -= margin_y + coord_y2 += margin_y + + 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 _intersectingGridIdxInclusive(self, bounds: Union[AxisAlignedBox, Polygon]) -> Set[Tuple[int, int]]: + grid_x1, grid_y1, grid_x2, grid_y2 = self._getGridCornerPoints( + bounds, + margin_x=-(self._margin_x + self._grid_round_margin_x) * 0.5, + margin_y=-(self._margin_y + self._grid_round_margin_y) * 0.5, + ) + 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, bounds: Union[AxisAlignedBox, Polygon]) -> Set[Tuple[int, int]]: + grid_x1, grid_y1, grid_x2, grid_y2 = self._getGridCornerPoints( + bounds, + margin_x=(self._margin_x + self._grid_round_margin_x) * 0.5, + margin_y=(self._margin_y + self._grid_round_margin_y) * 0.5, + ) + 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 0b5ccab439..783c6230b8 100755 --- a/cura/CuraApplication.py +++ b/cura/CuraApplication.py @@ -55,7 +55,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 @@ -116,6 +115,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 @@ -1455,6 +1455,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 = [] @@ -1484,17 +1491,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() @@ -1981,7 +1988,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/packaging/MacOS/cura_background_dmg.png b/packaging/MacOS/cura_background_dmg.png index 8f2fb50b05..a293d94bd2 100644 Binary files a/packaging/MacOS/cura_background_dmg.png and b/packaging/MacOS/cura_background_dmg.png differ diff --git a/plugins/3MFWriter/ThreeMFWorkspaceWriter.py b/plugins/3MFWriter/ThreeMFWorkspaceWriter.py index 1bc1432b67..e89af5c70a 100644 --- a/plugins/3MFWriter/ThreeMFWorkspaceWriter.py +++ b/plugins/3MFWriter/ThreeMFWorkspaceWriter.py @@ -40,7 +40,9 @@ class ThreeMFWorkspaceWriter(WorkspaceWriter): # Indicate that the 3mf mesh writer should not close the archive just yet (we still need to add stuff to it). mesh_writer.setStoreArchive(True) - mesh_writer.write(stream, nodes, mode) + if not mesh_writer.write(stream, nodes, mode): + self.setInformation(mesh_writer.getInformation()) + return False archive = mesh_writer.getArchive() if archive is None: # This happens if there was no mesh data to write. @@ -98,7 +100,7 @@ class ThreeMFWorkspaceWriter(WorkspaceWriter): Logger.error("No permission to write workspace to this stream.") return False except EnvironmentError as e: - self.setInformation(catalog.i18nc("@error:zip", "The operating system does not allow saving a project file to this location or with this file name.")) + self.setInformation(catalog.i18nc("@error:zip", str(e))) Logger.error("EnvironmentError when writing workspace to this stream: {err}".format(err = str(e))) return False mesh_writer.setStoreArchive(False) diff --git a/plugins/3MFWriter/ThreeMFWriter.py b/plugins/3MFWriter/ThreeMFWriter.py index 3f6fef7201..1ecfd87aa8 100644 --- a/plugins/3MFWriter/ThreeMFWriter.py +++ b/plugins/3MFWriter/ThreeMFWriter.py @@ -237,9 +237,9 @@ class ThreeMFWriter(MeshWriter): archive.writestr(model_file, scene_string) archive.writestr(content_types_file, b' \n' + ET.tostring(content_types)) archive.writestr(relations_file, b' \n' + ET.tostring(relations_element)) - except Exception as e: + except Exception as error: Logger.logException("e", "Error writing zip file") - self.setInformation(catalog.i18nc("@error:zip", "Error writing 3mf file.")) + self.setInformation(str(error)) return False finally: if not self._store_archive: diff --git a/plugins/PostProcessingPlugin/scripts/LimitXYAccelJerk.py b/plugins/PostProcessingPlugin/scripts/LimitXYAccelJerk.py new file mode 100644 index 0000000000..43aceb7793 --- /dev/null +++ b/plugins/PostProcessingPlugin/scripts/LimitXYAccelJerk.py @@ -0,0 +1,326 @@ +# Limit XY Accel: Authored by: Greg Foresi (GregValiant) +# July 2023 +# Sometimes bed-slinger printers need different Accel and Jerk values for the Y but Cura always makes them the same. +# This script changes the Accel and/or Jerk from the beginning of the 'Start Layer' to the end of the 'End Layer'. +# The existing M201 Max Accel will be changed to limit the Y (and/or X) accel at the printer. If you have Accel enabled in Cura and the XY Accel is set to 3000 then setting the Y limit to 1000 will result in the printer limiting the Y to 1000. This can keep tall skinny prints from breaking loose of the bed and failing. The script was not tested with Junction Deviation. +# If enabled - the Jerk setting is changed line-by-line within the gcode as there is no "limit" on Jerk. +# if 'Gradual ACCEL change' is enabled then the Accel is changed gradually from the Start to the End layer and that will be the final Accel setting in the file. If 'Gradual' is enabled then the Jerk settings will continue to be changed to the end of the file (rather than ending at the End layer). +# This post is intended for printers with moving beds (bed slingers) so UltiMaker printers are excluded. +# When setting an accel limit on multi-extruder printers ALL extruders are effected. +# This post does not distinguish between Print Accel and Travel Accel. The limit is the limit for all regardless. Example: Skin Accel = 1000 and Outer Wall accel = 500. If the limit is set to 300 then both Skin and Outer Wall will be Accel = 300. + +from ..Script import Script +from cura.CuraApplication import CuraApplication +import re +from UM.Message import Message + +class LimitXYAccelJerk(Script): + + def initialize(self) -> None: + super().initialize() + # Get the Accel and Jerk and set the values in the setting boxes-- + mycura = CuraApplication.getInstance().getGlobalContainerStack() + extruder = mycura.extruderList + accel_print = extruder[0].getProperty("acceleration_print", "value") + accel_travel = extruder[0].getProperty("acceleration_travel", "value") + jerk_print_old = extruder[0].getProperty("jerk_print", "value") + jerk_travel_old = extruder[0].getProperty("jerk_travel", "value") + self._instance.setProperty("x_accel_limit", "value", round(accel_print)) + self._instance.setProperty("y_accel_limit", "value", round(accel_print)) + self._instance.setProperty("x_jerk", "value", jerk_print_old) + self._instance.setProperty("y_jerk", "value", jerk_print_old) + ext_count = int(mycura.getProperty("machine_extruder_count", "value")) + machine_name = str(mycura.getProperty("machine_name", "value")) + + # Warn the user if the printer is an Ultimaker------------------------- + if "Ultimaker" in machine_name: + Message(text = " [Limit the X-Y Accel/Jerk] DID NOT RUN because Ultimaker printers don't have sliding beds.").show() + + # Warn the user if the printer is multi-extruder------------------ + if ext_count > 1: + Message(text = " 'Limit the X-Y Accel/Jerk': The post processor treats all extruders the same. If you have multiple extruders they will all be subject to the same Accel and Jerk limits imposed. If you have different Travel and Print Accel they will also be subject to the same limits. If that is not acceptable then you should not use this Post Processor.").show() + + def getSettingDataString(self): + return """{ + "name": "Limit the X-Y Accel/Jerk (all extruders equal)", + "key": "LimitXYAccelJerk", + "metadata": {}, + "version": 2, + "settings": + { + "type_of_change": + { + "label": "Immediate or Gradual change", + "description": "An 'Immediate' change will insert the new numbers immediately at the Start Layer. A 'Gradual' change will transition from the starting Accel to the new Accel limit across a range of layers.", + "type": "enum", + "options": { + "immediate_change": "Immediate", + "gradual_change": "Gradual"}, + "default_value": "immediate_change" + }, + "x_accel_limit": + { + "label": "X MAX Acceleration", + "description": "If this number is lower than the 'X Print Accel' in Cura then this will limit the Accel on the X axis. Enter the Maximum Acceleration value for the X axis. This will affect both Print and Travel Accel. If you enable an End Layer then at the end of that layer the Accel Limit will be reset (unless you choose 'Gradual' in which case the new limit goes to the top layer).", + "type": "int", + "enabled": true, + "minimum_value": 50, + "unit": "mm/sec² ", + "default_value": 500 + }, + "y_accel_limit": + { + "label": "Y MAX Acceleration", + "description": "If this number is lower than the Y accel in Cura then this will limit the Accel on the Y axis. Enter the Maximum Acceleration value for the Y axis. This will affect both Print and Travel Accel. If you enable an End Layer then at the end of that layer the Accel Limit will be reset (unless you choose 'Gradual' in which case the new limit goes to the top layer).", + "type": "int", + "enabled": true, + "minimum_value": 50, + "unit": "mm/sec² ", + "default_value": 500 + }, + "jerk_enable": + { + "label": "Change the Jerk", + "description": "Whether to change the Jerk values.", + "type": "bool", + "enabled": true, + "default_value": false + }, + "x_jerk": + { + "label": " X jerk", + "description": "Enter the Jerk value for the X axis. Enter '0' to use the existing X Jerk. This setting will affect both the Print and Travel jerk.", + "type": "int", + "enabled": "jerk_enable", + "unit": "mm/sec ", + "default_value": 8 + }, + "y_jerk": + { + "label": " Y jerk", + "description": "Enter the Jerk value for the Y axis. Enter '0' to use the existing Y Jerk. This setting will affect both the Print and Travel jerk.", + "type": "int", + "enabled": "jerk_enable", + "unit": "mm/sec ", + "default_value": 8 + }, + "start_layer": + { + "label": "From Start of Layer:", + "description": "Use the Cura Preview numbers. Enter the Layer to start the changes at. The minimum is Layer 1.", + "type": "int", + "default_value": 1, + "minimum_value": 1, + "unit": "Lay# ", + "enabled": "type_of_change == 'immediate_change'" + }, + "end_layer": + { + "label": "To End of Layer", + "description": "Use the Cura Preview numbers. Enter '-1' for the entire file or enter a layer number. The changes will end at your 'End Layer' and revert back to the original numbers.", + "type": "int", + "default_value": -1, + "minimum_value": -1, + "unit": "Lay# ", + "enabled": "type_of_change == 'immediate_change'" + }, + "gradient_start_layer": + { + "label": " Gradual From Layer:", + "description": "Use the Cura Preview numbers. Enter the Layer to start the changes at. The minimum is Layer 1.", + "type": "int", + "default_value": 1, + "minimum_value": 1, + "unit": "Lay# ", + "enabled": "type_of_change == 'gradual_change'" + }, + "gradient_end_layer": + { + "label": " Gradual To Layer", + "description": "Use the Cura Preview numbers. Enter '-1' for the top layer or enter a layer number. The last 'Gradual' change will continue to the end of the file.", + "type": "int", + "default_value": -1, + "minimum_value": -1, + "unit": "Lay# ", + "enabled": "type_of_change == 'gradual_change'" + } + } + }""" + + def execute(self, data): + mycura = CuraApplication.getInstance().getGlobalContainerStack() + extruder = mycura.extruderList + machine_name = str(mycura.getProperty("machine_name", "value")) + print_sequence = str(mycura.getProperty("print_sequence", "value")) + + # Exit if 'one_at_a_time' is enabled------------------------- + if print_sequence == "one_at_a_time": + Message(text = " [Limit the X-Y Accel/Jerk] DID NOT RUN. This post processor is not compatible with 'One-at-a-Time' mode.").show() + data[0] += "; [LimitXYAccelJerk] DID NOT RUN because Cura is set to 'One-at-a-Time' mode.\n" + return data + + # Exit if the printer is an Ultimaker------------------------- + if "Ultimaker" in machine_name: + Message(text = " [Limit the X-Y Accel/Jerk] DID NOT RUN. This post processor is for bed slinger printers only.").show() + data[0] += "; [LimitXYAccelJerk] DID NOT RUN because the printer doesn't have a sliding bed.\n" + return data + + type_of_change = str(self.getSettingValueByKey("type_of_change")) + accel_print_enabled = bool(extruder[0].getProperty("acceleration_enabled", "value")) + accel_travel_enabled = bool(extruder[0].getProperty("acceleration_travel_enabled", "value")) + accel_print = extruder[0].getProperty("acceleration_print", "value") + accel_travel = extruder[0].getProperty("acceleration_travel", "value") + jerk_print_enabled = str(extruder[0].getProperty("jerk_enabled", "value")) + jerk_travel_enabled = str(extruder[0].getProperty("jerk_travel_enabled", "value")) + jerk_print_old = extruder[0].getProperty("jerk_print", "value") + jerk_travel_old = extruder[0].getProperty("jerk_travel", "value") + + if int(accel_print) >= int(accel_travel): + accel_old = accel_print + else: + accel_old = accel_travel + jerk_travel = str(extruder[0].getProperty("jerk_travel", "value")) + if int(jerk_print_old) >= int(jerk_travel_old): + jerk_old = jerk_print_old + else: + jerk_old = jerk_travel_old + + #Set the new Accel values---------------------------------------------------------- + x_accel = str(self.getSettingValueByKey("x_accel_limit")) + y_accel = str(self.getSettingValueByKey("y_accel_limit")) + x_jerk = int(self.getSettingValueByKey("x_jerk")) + y_jerk = int(self.getSettingValueByKey("y_jerk")) + + # Put the strings together------------------------------------------- + m201_limit_new = "M201 X" + x_accel + " Y" + y_accel + m201_limit_old = "M201 X" + str(round(accel_old)) + " Y" + str(round(accel_old)) + if x_jerk == 0: + m205_jerk_pattern = "Y(\d*)" + m205_jerk_new = "Y" + str(y_jerk) + if y_jerk == 0: + m205_jerk_pattern = "X(\d*)" + m205_jerk_new = "X" + str(x_jerk) + if x_jerk != 0 and y_jerk != 0: + m205_jerk_pattern = "M205 X(\d*) Y(\d*)" + m205_jerk_new = "M205 X" + str(x_jerk) + " Y" + str(y_jerk) + m205_jerk_old = "M205 X" + str(jerk_old) + " Y" + str(jerk_old) + type_of_change = self.getSettingValueByKey("type_of_change") + + #Get the indexes of the start and end layers---------------------------------------- + if type_of_change == 'immediate_change': + start_layer = int(self.getSettingValueByKey("start_layer"))-1 + end_layer = int(self.getSettingValueByKey("end_layer")) + else: + start_layer = int(self.getSettingValueByKey("gradient_start_layer"))-1 + end_layer = int(self.getSettingValueByKey("gradient_end_layer")) + start_index = 2 + end_index = len(data)-2 + for num in range(2,len(data)-1): + if ";LAYER:" + str(start_layer) + "\n" in data[num]: + start_index = num + break + if int(end_layer) > 0: + for num in range(3,len(data)-1): + try: + if ";LAYER:" + str(end_layer) + "\n" in data[num]: + end_index = num + break + except: + end_index = len(data)-2 + + #Add Accel limit and new Jerk at start layer----------------------------------------------------- + if type_of_change == "immediate_change": + layer = data[start_index] + lines = layer.split("\n") + for index, line in enumerate(lines): + if lines[index].startswith(";LAYER:"): + lines.insert(index+1,m201_limit_new) + if self.getSettingValueByKey("jerk_enable"): + lines.insert(index+2,m205_jerk_new) + data[start_index] = "\n".join(lines) + break + + #Alter any existing jerk lines. Accel lines can be ignored----------------------------------- + for num in range(start_index,end_index,1): + layer = data[num] + lines = layer.split("\n") + for index, line in enumerate(lines): + if line.startswith("M205"): + lines[index] = re.sub(m205_jerk_pattern, m205_jerk_new, line) + data[num] = "\n".join(lines) + if end_layer != -1: + try: + layer = data[end_index-1] + lines = layer.split("\n") + lines.insert(len(lines)-2,m201_limit_old) + lines.insert(len(lines)-2,m205_jerk_old) + data[end_index-1] = "\n".join(lines) + except: + pass + else: + data[len(data)-1] = m201_limit_old + "\n" + m205_jerk_old + "\n" + data[len(data)-1] + return data + + elif type_of_change == "gradual_change": + layer_spread = end_index - start_index + if accel_old >= int(x_accel): + x_accel_hyst = round((accel_old - int(x_accel)) / layer_spread) + else: + x_accel_hyst = round((int(x_accel) - accel_old) / layer_spread) + if accel_old >= int(y_accel): + y_accel_hyst = round((accel_old - int(y_accel)) / layer_spread) + else: + y_accel_hyst = round((int(y_accel) - accel_old) / layer_spread) + + if accel_old >= int(x_accel): + x_accel_start = round(round((accel_old - x_accel_hyst)/25)*25) + else: + x_accel_start = round(round((x_accel_hyst + accel_old)/25)*25) + if accel_old >= int(y_accel): + y_accel_start = round(round((accel_old - y_accel_hyst)/25)*25) + else: + y_accel_start = round(round((y_accel_hyst + accel_old)/25)*25) + m201_limit_new = "M201 X" + str(x_accel_start) + " Y" + str(y_accel_start) + #Add Accel limit and new Jerk at start layer------------------------------------------------------------- + layer = data[start_index] + lines = layer.split("\n") + for index, line in enumerate(lines): + if lines[index].startswith(";LAYER:"): + lines.insert(index+1,m201_limit_new) + if self.getSettingValueByKey("jerk_enable"): + lines.insert(index+2,m205_jerk_new) + data[start_index] = "\n".join(lines) + break + for num in range(start_index + 1, end_index,1): + layer = data[num] + lines = layer.split("\n") + if accel_old >= int(x_accel): + x_accel_start -= x_accel_hyst + if x_accel_start < int(x_accel): x_accel_start = int(x_accel) + else: + x_accel_start += x_accel_hyst + if x_accel_start > int(x_accel): x_accel_start = int(x_accel) + if accel_old >= int(y_accel): + y_accel_start -= y_accel_hyst + if y_accel_start < int(y_accel): y_accel_start = int(y_accel) + else: + y_accel_start += y_accel_hyst + if y_accel_start > int(y_accel): y_accel_start = int(y_accel) + m201_limit_new = "M201 X" + str(round(round(x_accel_start/25)*25)) + " Y" + str(round(round(y_accel_start/25)*25)) + for index, line in enumerate(lines): + if line.startswith(";LAYER:"): + lines.insert(index+1, m201_limit_new) + continue + data[num] = "\n".join(lines) + + #Alter any existing jerk lines. Accel lines can be ignored--------------- + if self.getSettingValueByKey("jerk_enable"): + for num in range(start_index,len(data)-1,1): + layer = data[num] + lines = layer.split("\n") + for index, line in enumerate(lines): + if line.startswith("M205"): + lines[index] = re.sub(m205_jerk_pattern, m205_jerk_new, line) + data[num] = "\n".join(lines) + data[len(data)-1] = m201_limit_old + "\n" + m205_jerk_old + "\n" + data[len(data)-1] + return data \ No newline at end of file diff --git a/resources/definitions/fdmprinter.def.json b/resources/definitions/fdmprinter.def.json index 40d1eb3b1c..680dd3b6f8 100644 --- a/resources/definitions/fdmprinter.def.json +++ b/resources/definitions/fdmprinter.def.json @@ -5514,7 +5514,9 @@ "unit": "mm", "type": "float", "default_value": 0.0, - "maximum_value": "extruderValue(support_extruder_nr, 'support_offset')", + "maximum_value": "extruderValue(support_extruder_nr, 'support_offset') if support_structure == 'normal' else None", + "minimum_value_warning": "-1 * machine_nozzle_size", + "maximum_value_warning": "10 * machine_nozzle_size", "limit_to_extruder": "support_interface_extruder_nr", "enabled": "support_interface_enable and (support_enable or support_meshes_present)", "settable_per_mesh": false, @@ -5529,7 +5531,9 @@ "type": "float", "default_value": 0.0, "value": "extruderValue(support_roof_extruder_nr, 'support_interface_offset')", - "maximum_value": "extruderValue(support_extruder_nr, 'support_offset')", + "maximum_value": "extruderValue(support_extruder_nr, 'support_offset') if support_structure == 'normal' else None", + "minimum_value_warning": "-1 * machine_nozzle_size", + "maximum_value_warning": "10 * machine_nozzle_size", "limit_to_extruder": "support_roof_extruder_nr", "enabled": "support_roof_enable and (support_enable or support_meshes_present)", "settable_per_mesh": false, @@ -5543,7 +5547,9 @@ "type": "float", "default_value": 0.0, "value": "extruderValue(support_bottom_extruder_nr, 'support_interface_offset')", - "maximum_value": "extruderValue(support_extruder_nr, 'support_offset')", + "maximum_value": "extruderValue(support_extruder_nr, 'support_offset') if support_structure == 'normal' else None", + "minimum_value_warning": "-1 * machine_nozzle_size", + "maximum_value_warning": "10 * machine_nozzle_size", "limit_to_extruder": "support_bottom_extruder_nr", "enabled": "support_bottom_enable and (support_enable or support_meshes_present)", "settable_per_mesh": false, 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/Cura.qml b/resources/qml/Cura.qml index 31066f8f46..4983363946 100644 --- a/resources/qml/Cura.qml +++ b/resources/qml/Cura.qml @@ -822,12 +822,26 @@ UM.MainWindow } } - Cura.WizardDialog + property var wizardDialog + Component { - id: addMachineDialog - title: catalog.i18nc("@title:window", "Add Printer") - model: CuraApplication.getAddPrinterPagesModel() - progressBarVisible: false + id: addMachineDialogLoader + + Cura.WizardDialog + { + title: catalog.i18nc("@title:window", "Add Printer") + maximumWidth: Screen.width * 2 + maximumHeight: Screen.height * 2 + model: CuraApplication.getAddPrinterPagesModel() + progressBarVisible: false + onVisibleChanged: + { + if(!visible) + { + wizardDialog = null + } + } + } } Cura.WizardDialog @@ -852,9 +866,8 @@ UM.MainWindow target: Cura.Actions.addMachine function onTriggered() { - // Make sure to show from the first page when the dialog shows up. - addMachineDialog.resetModelState() - addMachineDialog.show() + wizardDialog = addMachineDialogLoader.createObject() + wizardDialog.show() } } diff --git a/resources/qml/MachineSettings/GcodeTextArea.qml b/resources/qml/MachineSettings/GcodeTextArea.qml index d4bc58cdc4..2538cd9f65 100644 --- a/resources/qml/MachineSettings/GcodeTextArea.qml +++ b/resources/qml/MachineSettings/GcodeTextArea.qml @@ -55,6 +55,7 @@ UM.TooltipArea } ScrollBar.vertical: UM.ScrollBar {} + clip: true TextArea.flickable: TextArea { @@ -70,6 +71,7 @@ UM.TooltipArea selectionColor: UM.Theme.getColor("text_selection") selectedTextColor: UM.Theme.getColor("text") wrapMode: TextEdit.NoWrap + padding: UM.Theme.getSize("narrow_margin").height + backgroundRectangle.border.width onActiveFocusChanged: { @@ -81,8 +83,9 @@ UM.TooltipArea background: Rectangle { + id: backgroundRectangle + anchors.fill: parent - anchors.margins: -border.width //Wrap the border around the parent. color: UM.Theme.getColor("detail_background") border.color: 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.") + } } + } } } diff --git a/resources/qml/Preferences/SettingVisibilityItem.qml b/resources/qml/Preferences/SettingVisibilityItem.qml index 8905c15124..07255306a5 100644 --- a/resources/qml/Preferences/SettingVisibilityItem.qml +++ b/resources/qml/Preferences/SettingVisibilityItem.qml @@ -20,6 +20,7 @@ Item width: childrenRect.width; height: childrenRect.height; id: checkboxTooltipArea + x: check.height UM.CheckBox { id: check @@ -40,7 +41,7 @@ Item { width: height height: check.height - anchors.left: checkboxTooltipArea.right + anchors.right: checkboxTooltipArea.left anchors.leftMargin: 2 * screenScaleFactor text: @@ -82,7 +83,7 @@ Item source: UM.Theme.getIcon("Information") - color: UM.Theme.getColor("primary_button_text") + color: UM.Theme.getColor("small_button_text") } visible: provider.properties.enabled == "False" diff --git a/resources/qml/Settings/SettingView.qml b/resources/qml/Settings/SettingView.qml index 934e19030d..c5fed795d5 100644 --- a/resources/qml/Settings/SettingView.qml +++ b/resources/qml/Settings/SettingView.qml @@ -40,7 +40,7 @@ Item Cura.TextField { id: filter - height: parent.height + implicitHeight: parent.height anchors.left: parent.left anchors.right: parent.right topPadding: height / 4 @@ -337,7 +337,7 @@ Item } function onShowTooltip(text) { base.showTooltip(delegate, Qt.point(-settingsView.x - UM.Theme.getSize("default_margin").width, 0), text) } function onHideTooltip() { base.hideTooltip() } - function onShowAllHiddenInheritedSettings() + function onShowAllHiddenInheritedSettings(category_id) { var children_with_override = Cura.SettingInheritanceManager.getChildrenKeysWithOverride(category_id) for(var i = 0; i < children_with_override.length; i++) diff --git a/resources/qml/WelcomePages/AddNetworkPrinterScrollView.qml b/resources/qml/WelcomePages/AddNetworkPrinterScrollView.qml index 3d138e3d2e..d65bd63550 100644 --- a/resources/qml/WelcomePages/AddNetworkPrinterScrollView.qml +++ b/resources/qml/WelcomePages/AddNetworkPrinterScrollView.qml @@ -3,6 +3,7 @@ import QtQuick 2.10 import QtQuick.Controls 2.3 +import QtQuick.Layouts 2.3 import UM 1.5 as UM import Cura 1.1 as Cura @@ -15,9 +16,7 @@ import Cura 1.1 as Cura Item { id: base - height: networkPrinterInfo.height + controlsRectangle.height - property alias maxItemCountAtOnce: networkPrinterListView.maxItemCountAtOnce property var currentItem: (networkPrinterListView.currentIndex >= 0) ? networkPrinterListView.model[networkPrinterListView.currentIndex] : null @@ -29,35 +28,32 @@ Item Item { id: networkPrinterInfo - height: networkPrinterListView.visible ? networkPrinterListView.height : noPrinterLabel.height anchors.left: parent.left anchors.right: parent.right anchors.top: parent.top + anchors.bottom: separator.top UM.Label { id: noPrinterLabel height: UM.Theme.getSize("setting_control").height + UM.Theme.getSize("default_margin").height - anchors.left: parent.left - anchors.leftMargin: UM.Theme.getSize("default_margin").width + anchors.fill: parent + anchors.margins: UM.Theme.getSize("default_margin").width text: catalog.i18nc("@label", "There is no printer found over your network.") visible: networkPrinterListView.count == 0 // Do not show if there are discovered devices. + verticalAlignment: Text.AlignTop } ListView { id: networkPrinterListView - anchors.top: parent.top - anchors.left: parent.left - anchors.right: parent.right - height: Math.min(contentHeight, (maxItemCountAtOnce * UM.Theme.getSize("action_button").height) - UM.Theme.getSize("default_margin").height) + anchors.fill: parent ScrollBar.vertical: UM.ScrollBar { id: networkPrinterScrollBar } clip: true - property int maxItemCountAtOnce: 8 // show at max 8 items at once, otherwise you need to scroll. visible: networkPrinterListView.count > 0 model: contentLoader.enabled ? CuraApplication.getDiscoveredPrintersModel().discoveredPrinters: undefined @@ -138,7 +134,7 @@ Item { id: separator anchors.left: parent.left - anchors.top: networkPrinterInfo.bottom + anchors.bottom: controlsRectangle.top anchors.right: parent.right height: UM.Theme.getSize("default_lining").height color: UM.Theme.getColor("lining") @@ -149,7 +145,7 @@ Item id: controlsRectangle anchors.left: parent.left anchors.right: parent.right - anchors.top: separator.bottom + anchors.bottom: parent.bottom height: UM.Theme.getSize("message_action_button").height + UM.Theme.getSize("default_margin").height diff --git a/resources/qml/WelcomePages/AddThirdPartyPrinter.qml b/resources/qml/WelcomePages/AddThirdPartyPrinter.qml index 2c6c3a19bf..9229715db0 100644 --- a/resources/qml/WelcomePages/AddThirdPartyPrinter.qml +++ b/resources/qml/WelcomePages/AddThirdPartyPrinter.qml @@ -3,6 +3,7 @@ import QtQuick 2.10 import QtQuick.Controls 2.3 +import QtQuick.Layouts 2.3 import UM 1.5 as UM import Cura 1.1 as Cura @@ -17,79 +18,84 @@ Item property var goToUltimakerPrinter - DropDownWidget + ColumnLayout { - id: addNetworkPrinterDropDown - anchors.top: parent.top + anchors.topMargin: UM.Theme.getSize("wide_margin").height + anchors.bottom: backButton.top + anchors.bottomMargin: UM.Theme.getSize("default_margin").height anchors.left: parent.left anchors.right: parent.right - anchors.topMargin: UM.Theme.getSize("wide_margin").height - title: catalog.i18nc("@label", "Add a networked printer") - contentShown: true // by default expand the network printer list + spacing: UM.Theme.getSize("default_margin").height - onClicked: + DropDownWidget { - addLocalPrinterDropDown.contentShown = !contentShown - } + id: addNetworkPrinterDropDown - contentComponent: networkPrinterListComponent - Component - { - id: networkPrinterListComponent - AddNetworkPrinterScrollView + Layout.fillWidth: true + Layout.fillHeight: contentShown + + title: catalog.i18nc("@label", "Add a networked printer") + contentShown: true // by default expand the network printer list + + onClicked: { - id: networkPrinterScrollView + addLocalPrinterDropDown.contentShown = !contentShown + } - maxItemCountAtOnce: 9 // show at max 9 items at once, otherwise you need to scroll. - - onRefreshButtonClicked: + contentComponent: networkPrinterListComponent + Component + { + id: networkPrinterListComponent + AddNetworkPrinterScrollView { - UM.OutputDeviceManager.startDiscovery() - } + id: networkPrinterScrollView - onAddByIpButtonClicked: - { - base.goToPage("add_printer_by_ip") - } - - onAddCloudPrinterButtonClicked: - { - base.goToPage("add_cloud_printers") - if (!Cura.API.account.isLoggedIn) + onRefreshButtonClicked: { - Cura.API.account.login() + UM.OutputDeviceManager.startDiscovery() + } + + onAddByIpButtonClicked: + { + base.goToPage("add_printer_by_ip") + } + + onAddCloudPrinterButtonClicked: + { + base.goToPage("add_cloud_printers") + if (!Cura.API.account.isLoggedIn) + { + Cura.API.account.login() + } } } } } - } - DropDownWidget - { - id: addLocalPrinterDropDown - - anchors.top: addNetworkPrinterDropDown.bottom - anchors.left: parent.left - anchors.right: parent.right - anchors.topMargin: UM.Theme.getSize("default_margin").height - - title: catalog.i18nc("@label", "Add a non-networked printer") - - onClicked: + DropDownWidget { - addNetworkPrinterDropDown.contentShown = !contentShown - } + id: addLocalPrinterDropDown - contentComponent: localPrinterListComponent - Component - { - id: localPrinterListComponent - AddLocalPrinterScrollView + Layout.fillWidth: true + Layout.fillHeight: contentShown + + title: catalog.i18nc("@label", "Add a non-networked printer") + + onClicked: { - id: localPrinterView - height: backButton.y - addLocalPrinterDropDown.y - UM.Theme.getSize("expandable_component_content_header").height - UM.Theme.getSize("default_margin").height + addNetworkPrinterDropDown.contentShown = !contentShown + } + + contentComponent: localPrinterListComponent + Component + { + id: localPrinterListComponent + AddLocalPrinterScrollView + { + id: localPrinterView + } } } } diff --git a/resources/qml/WelcomePages/DropDownWidget.qml b/resources/qml/WelcomePages/DropDownWidget.qml index 90e1900d35..3db9ae4bf3 100644 --- a/resources/qml/WelcomePages/DropDownWidget.qml +++ b/resources/qml/WelcomePages/DropDownWidget.qml @@ -22,7 +22,7 @@ Item id: base implicitWidth: 200 * screenScaleFactor - height: header.contentShown ? (header.height + contentRectangle.height) : header.height + implicitHeight: contentShown ? (header.height + contentRectangle.implicitHeight) : header.height property var contentComponent: null property alias contentItem: contentLoader.item @@ -56,12 +56,14 @@ Item Cura.RoundedRectangle { id: contentRectangle + anchors.top: header.bottom // Move up a bit (exactly the width of the border) to avoid double line - y: header.height - UM.Theme.getSize("default_lining").width + anchors.topMargin: -UM.Theme.getSize("default_lining").width anchors.left: header.left anchors.right: header.right + anchors.bottom: parent.bottom // Add 2x lining, because it needs a bit of space on the top and the bottom. - height: contentLoader.item ? contentLoader.item.height + 2 * UM.Theme.getSize("thick_lining").height : 0 + anchors.bottomMargin: UM.Theme.getSize("thick_lining").height border.width: UM.Theme.getSize("default_lining").width border.color: UM.Theme.getColor("lining") @@ -73,9 +75,7 @@ Item Loader { id: contentLoader - anchors.top: parent.top - anchors.left: parent.left - anchors.right: parent.right + anchors.fill: parent // Keep a small margin with the Rectangle container so its content will not overlap with the Rectangle // border. anchors.margins: UM.Theme.getSize("default_lining").width diff --git a/resources/qml/WelcomePages/WizardDialog.qml b/resources/qml/WelcomePages/WizardDialog.qml index 8629f47115..387289052b 100644 --- a/resources/qml/WelcomePages/WizardDialog.qml +++ b/resources/qml/WelcomePages/WizardDialog.qml @@ -32,11 +32,6 @@ Window property var model: null // Needs to be set by whoever is using this dialog. property alias progressBarVisible: wizardPanel.progressBarVisible - function resetModelState() - { - model.resetState() - } - WizardPanel { id: wizardPanel