Merge branch 'main' into CURA-9494_nightly

This commit is contained in:
Remco Burema 2023-09-07 13:09:49 +02:00
commit c716ff8dfe
30 changed files with 1730 additions and 962 deletions

View file

@ -1,158 +1,153 @@
name: Create and Upload Conan package name: Create and Upload Conan package
on: on:
workflow_call: workflow_call:
inputs: inputs:
project_name: project_name:
required: true required: true
type: string type: string
recipe_id_full: recipe_id_full:
required: true required: true
type: string type: string
build_id: build_id:
required: true required: true
type: number type: number
build_info: build_info:
required: false required: false
default: true default: true
type: boolean type: boolean
recipe_id_latest: recipe_id_latest:
required: false required: false
type: string type: string
runs_on: runs_on:
required: true required: true
type: string type: string
python_version: python_version:
required: true required: true
type: string type: string
conan_config_branch: conan_config_branch:
required: false required: false
type: string type: string
conan_logging_level: conan_logging_level:
required: false required: false
type: string type: string
conan_clean_local_cache: conan_clean_local_cache:
required: false required: false
type: boolean type: boolean
default: false default: false
conan_upload_community: conan_upload_community:
required: false required: false
default: true default: true
type: boolean type: boolean
env: env:
CONAN_LOGIN_USERNAME: ${{ secrets.CONAN_USER }} CONAN_LOGIN_USERNAME: ${{ secrets.CONAN_USER }}
CONAN_PASSWORD: ${{ secrets.CONAN_PASS }} CONAN_PASSWORD: ${{ secrets.CONAN_PASS }}
CONAN_LOG_RUN_TO_OUTPUT: 1 CONAN_LOG_RUN_TO_OUTPUT: 1
CONAN_LOGGING_LEVEL: ${{ inputs.conan_logging_level }} CONAN_LOGGING_LEVEL: ${{ inputs.conan_logging_level }}
CONAN_NON_INTERACTIVE: 1 CONAN_NON_INTERACTIVE: 1
jobs: jobs:
conan-package-create: conan-package-create:
runs-on: ${{ inputs.runs_on }} runs-on: ${{ inputs.runs_on }}
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v3
- name: Setup Python and pip - name: Setup Python and pip
uses: actions/setup-python@v4 uses: actions/setup-python@v4
with: with:
python-version: ${{ inputs.python_version }} python-version: ${{ inputs.python_version }}
cache: 'pip' cache: 'pip'
cache-dependency-path: .github/workflows/requirements-conan-package.txt cache-dependency-path: .github/workflows/requirements-conan-package.txt
- name: Install Python requirements for runner - name: Install Python requirements for runner
run: pip install -r https://raw.githubusercontent.com/Ultimaker/Cura/main/.github/workflows/requirements-conan-package.txt 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 # Note the runner requirements are always installed from the main branch in the Ultimaker/Cura repo
- name: Use Conan download cache (Bash) - name: Use Conan download cache (Bash)
if: ${{ runner.os != 'Windows' }} if: ${{ runner.os != 'Windows' }}
run: conan config set storage.download_cache="$HOME/.conan/conan_download_cache" run: conan config set storage.download_cache="$HOME/.conan/conan_download_cache"
- name: Use Conan download cache (Powershell) - name: Use Conan download cache (Powershell)
if: ${{ runner.os == 'Windows' }} if: ${{ runner.os == 'Windows' }}
run: conan config set storage.download_cache="C:\Users\runneradmin\.conan\conan_download_cache" run: conan config set storage.download_cache="C:\Users\runneradmin\.conan\conan_download_cache"
- name: Cache Conan local repository packages (Bash) - name: Cache Conan local repository packages (Bash)
uses: actions/cache@v3 uses: actions/cache@v3
if: ${{ runner.os != 'Windows' }} if: ${{ runner.os != 'Windows' }}
with: with:
path: | path: |
$HOME/.conan/data $HOME/.conan/data
$HOME/.conan/conan_download_cache $HOME/.conan/conan_download_cache
key: conan-${{ inputs.runs_on }}-${{ runner.arch }}-create-cache key: conan-${{ inputs.runs_on }}-${{ runner.arch }}-create-cache
- name: Cache Conan local repository packages (Powershell) - name: Cache Conan local repository packages (Powershell)
uses: actions/cache@v3 uses: actions/cache@v3
if: ${{ runner.os == 'Windows' }} if: ${{ runner.os == 'Windows' }}
with: with:
path: | path: |
C:\Users\runneradmin\.conan\data C:\Users\runneradmin\.conan\data
C:\.conan C:\.conan
C:\Users\runneradmin\.conan\conan_download_cache C:\Users\runneradmin\.conan\conan_download_cache
key: conan-${{ inputs.runs_on }}-${{ runner.arch }}-create-cache key: conan-${{ inputs.runs_on }}-${{ runner.arch }}-create-cache
- name: Install MacOS system requirements - name: Install MacOS system requirements
if: ${{ runner.os == 'Macos' }} if: ${{ runner.os == 'Macos' }}
run: brew install autoconf automake ninja 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. # 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. # This is maybe because grub caches the disk it uses last time, which is recreated each time.
- name: Install Linux system requirements - name: Install Linux system requirements
if: ${{ runner.os == 'Linux' }} if: ${{ runner.os == 'Linux' }}
run: | run: |
sudo rm /var/cache/debconf/config.dat sudo rm /var/cache/debconf/config.dat
sudo dpkg --configure -a sudo dpkg --configure -a
sudo add-apt-repository ppa:ubuntu-toolchain-r/test -y sudo add-apt-repository ppa:ubuntu-toolchain-r/test -y
sudo apt update sudo apt update
sudo apt upgrade 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 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 - name: Install GCC-132 on ubuntu
if: ${{ startsWith(inputs.runs_on, 'ubuntu-22.04') }} if: ${{ startsWith(inputs.runs_on, 'ubuntu') }}
run: | run: |
sudo apt install g++-12 gcc-12 -y sudo apt install g++-13 gcc-13 -y
sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-12 12 sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-13 13
sudo update-alternatives --install /usr/bin/g++ g++ /usr/bin/g++-12 12 sudo update-alternatives --install /usr/bin/g++ g++ /usr/bin/g++-13 13
- name: Use GCC-10 on ubuntu-20.04 - name: Create the default Conan profile
if: ${{ startsWith(inputs.runs_on, 'ubuntu-20.04') }} run: conan profile new default --detect
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 - name: Get Conan configuration from branch
run: conan profile new default --detect 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 - name: Get Conan configuration
if: ${{ inputs.conan_config_branch != '' }} run: |
run: conan config install https://github.com/Ultimaker/conan-config.git -a "-b ${{ inputs.conan_config_branch }}" 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 - name: Add Cura private Artifactory remote
if: ${{ inputs.conan_config_branch == '' }} run: conan remote add cura-private https://ultimaker.jfrog.io/artifactory/api/conan/cura-private True
run: conan config install https://github.com/Ultimaker/conan-config.git
- name: Add Cura private Artifactory remote - name: Create the Packages
run: conan remote add cura-private https://ultimaker.jfrog.io/artifactory/api/conan/cura-private True run: conan install ${{ inputs.recipe_id_full }} --build=missing --update -c tools.build:skip_test=True
- name: Create the Packages - name: Upload the Package(s)
run: conan install ${{ inputs.recipe_id_full }} --build=missing --update if: ${{ always() && inputs.conan_upload_community }}
run: conan upload ${{ inputs.recipe_id_full }} -r cura --all -c
- name: Upload the Package(s) - name: Upload the Package(s) to the private Artifactory
if: ${{ always() && inputs.conan_upload_community }} if: ${{ always() && ! inputs.conan_upload_community }}
run: conan upload ${{ inputs.recipe_id_full }} -r cura --all -c 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

View file

@ -49,15 +49,15 @@ on:
- '[1-9].[0-9][0-9].[0-9]*' - '[1-9].[0-9][0-9].[0-9]*'
env: env:
CONAN_LOGIN_USERNAME_CURA: ${{ secrets.CONAN_USER }} CONAN_LOGIN_USERNAME_CURA: ${{ secrets.CONAN_USER }}
CONAN_PASSWORD_CURA: ${{ secrets.CONAN_PASS }} CONAN_PASSWORD_CURA: ${{ secrets.CONAN_PASS }}
CONAN_LOGIN_USERNAME_CURA_CE: ${{ secrets.CONAN_USER }} CONAN_LOGIN_USERNAME_CURA_CE: ${{ secrets.CONAN_USER }}
CONAN_PASSWORD_CURA_CE: ${{ secrets.CONAN_PASS }} CONAN_PASSWORD_CURA_CE: ${{ secrets.CONAN_PASS }}
CONAN_LOG_RUN_TO_OUTPUT: 1 CONAN_LOG_RUN_TO_OUTPUT: 1
CONAN_LOGGING_LEVEL: ${{ inputs.conan_logging_level }} CONAN_LOGGING_LEVEL: ${{ inputs.conan_logging_level }}
CONAN_NON_INTERACTIVE: 1 CONAN_NON_INTERACTIVE: 1
permissions: {} permissions: { }
jobs: jobs:
conan-recipe-version: conan-recipe-version:
permissions: permissions:
@ -103,18 +103,23 @@ jobs:
sudo apt update sudo apt update
sudo apt upgrade 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 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 - name: Install GCC-13
sudo update-alternatives --install /usr/bin/g++ g++ /usr/bin/g++-12 12 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 - name: Create the default Conan profile
run: conan profile new default --detect --force run: conan profile new default --detect --force
- name: Get Conan configuration - 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 - 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 - name: Create the latest alias
if: always() if: always()

View file

@ -1,106 +1,107 @@
name: Export Conan Recipe to server name: Export Conan Recipe to server
on: on:
workflow_call: workflow_call:
inputs: inputs:
recipe_id_full: recipe_id_full:
required: true required: true
type: string type: string
recipe_id_latest: recipe_id_latest:
required: false required: false
type: string type: string
runs_on: runs_on:
required: true required: true
type: string type: string
python_version: python_version:
required: true required: true
type: string type: string
conan_config_branch: conan_config_branch:
required: false required: false
type: string type: string
conan_logging_level: conan_logging_level:
required: false required: false
type: string type: string
conan_export_binaries: conan_export_binaries:
required: false required: false
type: boolean type: boolean
conan_upload_community: conan_upload_community:
required: false required: false
default: true default: true
type: boolean type: boolean
env: env:
CONAN_LOGIN_USERNAME: ${{ secrets.CONAN_USER }} CONAN_LOGIN_USERNAME: ${{ secrets.CONAN_USER }}
CONAN_PASSWORD: ${{ secrets.CONAN_PASS }} CONAN_PASSWORD: ${{ secrets.CONAN_PASS }}
CONAN_LOG_RUN_TO_OUTPUT: 1 CONAN_LOG_RUN_TO_OUTPUT: 1
CONAN_LOGGING_LEVEL: ${{ inputs.conan_logging_level }} CONAN_LOGGING_LEVEL: ${{ inputs.conan_logging_level }}
CONAN_NON_INTERACTIVE: 1 CONAN_NON_INTERACTIVE: 1
jobs: jobs:
package-export: package-export:
runs-on: ${{ inputs.runs_on }} runs-on: ${{ inputs.runs_on }}
steps: steps:
- name: Checkout project - name: Checkout project
uses: actions/checkout@v3 uses: actions/checkout@v3
- name: Setup Python and pip - name: Setup Python and pip
uses: actions/setup-python@v4 uses: actions/setup-python@v4
with: with:
python-version: ${{ inputs.python_version }} python-version: ${{ inputs.python_version }}
cache: 'pip' cache: 'pip'
cache-dependency-path: .github/workflows/requirements-conan-package.txt cache-dependency-path: .github/workflows/requirements-conan-package.txt
- name: Install Python requirements and Create default Conan profile - name: Install Python requirements and Create default Conan profile
run: | run: |
pip install -r https://raw.githubusercontent.com/Ultimaker/Cura/main/.github/workflows/requirements-conan-package.txt pip install -r https://raw.githubusercontent.com/Ultimaker/Cura/main/.github/workflows/requirements-conan-package.txt
conan profile new default --detect conan profile new default --detect
# Note the runner requirements are always installed from the main branch in the Ultimaker/Cura repo # Note the runner requirements are always installed from the main branch in the Ultimaker/Cura repo
- name: Cache Conan local repository packages - name: Cache Conan local repository packages
uses: actions/cache@v3 uses: actions/cache@v3
with: with:
path: $HOME/.conan/data path: $HOME/.conan/data
key: ${{ runner.os }}-conan-export-cache key: ${{ runner.os }}-conan-export-cache
- name: Get Conan configuration from branch - name: Get Conan configuration from branch
if: ${{ inputs.conan_config_branch != '' }} if: ${{ inputs.conan_config_branch != '' }}
run: conan config install https://github.com/Ultimaker/conan-config.git -a "-b ${{ 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 - name: Get Conan configuration
if: ${{ inputs.conan_config_branch == '' }} run: |
run: conan config install https://github.com/Ultimaker/conan-config.git 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 - name: Add Cura private Artifactory remote
run: conan remote add cura-private https://ultimaker.jfrog.io/artifactory/api/conan/cura-private True run: conan remote add cura-private https://ultimaker.jfrog.io/artifactory/api/conan/cura-private True
- name: Export the Package (binaries) - name: Export the Package (binaries)
if: ${{ inputs.conan_export_binaries }} if: ${{ inputs.conan_export_binaries }}
run: conan create . ${{ inputs.recipe_id_full }} --build=missing --update run: conan create . ${{ inputs.recipe_id_full }} --build=missing --update -c tools.build:skip_test=True
- name: Export the Package - name: Export the Package
if: ${{ !inputs.conan_export_binaries }} if: ${{ !inputs.conan_export_binaries }}
run: conan export . ${{ inputs.recipe_id_full }} run: conan export . ${{ inputs.recipe_id_full }}
- name: Create the latest alias - name: Create the latest alias
if: always() if: always()
run: conan alias ${{ inputs.recipe_id_latest }} ${{ inputs.recipe_id_full }} run: conan alias ${{ inputs.recipe_id_latest }} ${{ inputs.recipe_id_full }}
- name: Upload the Package(s) - name: Upload the Package(s)
if: ${{ always() && inputs.conan_upload_community }} if: ${{ always() && inputs.conan_upload_community }}
run: | run: |
conan upload ${{ inputs.recipe_id_full }} -r cura --all -c conan upload ${{ inputs.recipe_id_full }} -r cura --all -c
conan upload ${{ inputs.recipe_id_latest }} -r cura -c conan upload ${{ inputs.recipe_id_latest }} -r cura -c
- name: Upload the Package(s) to the private Artifactory - name: Upload the Package(s) to the private Artifactory
if: ${{ always() && ! inputs.conan_upload_community }} if: ${{ always() && ! inputs.conan_upload_community }}
run: | run: |
conan upload ${{ inputs.recipe_id_full }} -r cura-private --all -c conan upload ${{ inputs.recipe_id_full }} -r cura-private --all -c
conan upload ${{ inputs.recipe_id_latest }} -r cura-private -c conan upload ${{ inputs.recipe_id_latest }} -r cura-private -c

View file

@ -2,285 +2,277 @@ name: Linux Installer
run-name: ${{ inputs.cura_conan_version }} for Linux-${{ inputs.architecture }} by @${{ github.actor }} run-name: ${{ inputs.cura_conan_version }} for Linux-${{ inputs.architecture }} by @${{ github.actor }}
on: on:
workflow_dispatch: workflow_dispatch:
inputs: inputs:
cura_conan_version: cura_conan_version:
description: 'Cura Conan Version' description: 'Cura Conan Version'
default: 'cura/latest@ultimaker/testing' default: 'cura/latest@ultimaker/testing'
required: true required: true
type: string type: string
conan_args: conan_args:
description: 'Conan args: eq.: --require-override' description: 'Conan args: eq.: --require-override'
default: '' default: ''
required: false required: false
type: string type: string
enterprise: enterprise:
description: 'Build Cura as an Enterprise edition' description: 'Build Cura as an Enterprise edition'
default: false default: false
required: true required: true
type: boolean type: boolean
staging: staging:
description: 'Use staging API' description: 'Use staging API'
default: false default: false
required: true required: true
type: boolean type: boolean
architecture: architecture:
description: 'Architecture' description: 'Architecture'
required: true required: true
default: 'X64' default: 'X64'
type: choice type: choice
options: options:
- X64 - X64
operating_system: operating_system:
description: 'OS' description: 'OS'
required: true required: true
default: 'ubuntu-22.04' default: 'ubuntu-22.04'
type: choice type: choice
options: options:
- ubuntu-22.04 - ubuntu-22.04
- ubuntu-20.04 - ubuntu-20.04
workflow_call: workflow_call:
inputs: inputs:
cura_conan_version: cura_conan_version:
description: 'Cura Conan Version' description: 'Cura Conan Version'
default: 'cura/latest@ultimaker/testing' default: 'cura/latest@ultimaker/testing'
required: true required: true
type: string type: string
conan_args: conan_args:
description: 'Conan args: eq.: --require-override' description: 'Conan args: eq.: --require-override'
default: '' default: ''
required: false required: false
type: string type: string
enterprise: enterprise:
description: 'Build Cura as an Enterprise edition' description: 'Build Cura as an Enterprise edition'
default: false default: false
required: true required: true
type: boolean type: boolean
staging: staging:
description: 'Use staging API' description: 'Use staging API'
default: false default: false
required: true required: true
type: boolean type: boolean
architecture: architecture:
description: 'Architecture' description: 'Architecture'
required: true required: true
default: 'X64' default: 'X64'
type: string type: string
operating_system: operating_system:
description: 'OS' description: 'OS'
required: true required: true
default: 'ubuntu-22.04' default: 'ubuntu-22.04'
type: string type: string
env: env:
CONAN_LOGIN_USERNAME_CURA: ${{ secrets.CONAN_USER }} CONAN_LOGIN_USERNAME_CURA: ${{ secrets.CONAN_USER }}
CONAN_PASSWORD_CURA: ${{ secrets.CONAN_PASS }} CONAN_PASSWORD_CURA: ${{ secrets.CONAN_PASS }}
GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }} GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }}
CURA_CONAN_VERSION: ${{ inputs.cura_conan_version }} CURA_CONAN_VERSION: ${{ inputs.cura_conan_version }}
ENTERPRISE: ${{ inputs.enterprise }} ENTERPRISE: ${{ inputs.enterprise }}
STAGING: ${{ inputs.staging }} STAGING: ${{ inputs.staging }}
jobs: jobs:
cura-installer-create: cura-installer-create:
runs-on: ${{ inputs.operating_system }} runs-on: ${{ inputs.operating_system }}
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v3
- name: Setup Python and pip - name: Setup Python and pip
uses: actions/setup-python@v4 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
with: with:
success: ${{ contains(join(needs.*.result, ','), 'success') }} python-version: '3.10.x'
success_title: "Create the Cura distributions" cache: 'pip'
success_body: "Installers for ${{ inputs.cura_conan_version }}" cache-dependency-path: .github/workflows/requirements-conan-package.txt
failure_title: "Failed to create the Cura distributions"
failure_body: "Failed to create at least 1 installer for ${{ inputs.cura_conan_version }}" - name: Install Python requirements for runner
secrets: inherit 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

View file

@ -1,2 +1,2 @@
conan==1.60.2 conan>=1.60.2,<2.0.0
sip sip

View file

@ -2,163 +2,165 @@
name: unit-test name: unit-test
on: on:
push: push:
paths: paths:
- 'plugins/**' - 'plugins/**'
- 'resources/**' - 'resources/**'
- 'cura/**' - 'cura/**'
- 'icons/**' - 'icons/**'
- 'tests/**' - 'tests/**'
- 'packaging/**' - 'packaging/**'
- '.github/workflows/conan-*.yml' - '.github/workflows/conan-*.yml'
- '.github/workflows/unit-test.yml' - '.github/workflows/unit-test.yml'
- '.github/workflows/notify.yml' - '.github/workflows/notify.yml'
- '.github/workflows/requirements-conan-package.txt' - '.github/workflows/requirements-conan-package.txt'
- 'requirements*.txt' - 'requirements*.txt'
- 'conanfile.py' - 'conanfile.py'
- 'conandata.yml' - 'conandata.yml'
- 'GitVersion.yml' - 'GitVersion.yml'
- '*.jinja' - '*.jinja'
branches: branches:
- main - main
- 'CURA-*' - 'CURA-*'
- '[1-9]+.[0-9]+' - '[1-9]+.[0-9]+'
tags: tags:
- '[0-9]+.[0-9]+.[0-9]+' - '[0-9]+.[0-9]+.[0-9]+'
- '[0-9]+.[0-9]+-beta' - '[0-9]+.[0-9]+-beta'
pull_request: pull_request:
paths: paths:
- 'plugins/**' - 'plugins/**'
- 'resources/**' - 'resources/**'
- 'cura/**' - 'cura/**'
- 'icons/**' - 'icons/**'
- 'tests/**' - 'tests/**'
- 'packaging/**' - 'packaging/**'
- '.github/workflows/conan-*.yml' - '.github/workflows/conan-*.yml'
- '.github/workflows/unit-test.yml' - '.github/workflows/unit-test.yml'
- '.github/workflows/notify.yml' - '.github/workflows/notify.yml'
- '.github/workflows/requirements-conan-package.txt' - '.github/workflows/requirements-conan-package.txt'
- 'requirements*.txt' - 'requirements*.txt'
- 'conanfile.py' - 'conanfile.py'
- 'conandata.yml' - 'conandata.yml'
- 'GitVersion.yml' - 'GitVersion.yml'
- '*.jinja' - '*.jinja'
branches: branches:
- main - main
- '[1-9]+.[0-9]+' - '[1-9]+.[0-9]+'
tags: tags:
- '[0-9]+.[0-9]+.[0-9]+' - '[0-9]+.[0-9]+.[0-9]+'
- '[0-9]+.[0-9]+-beta' - '[0-9]+.[0-9]+-beta'
env: env:
CONAN_LOGIN_USERNAME_CURA: ${{ secrets.CONAN_USER }} CONAN_LOGIN_USERNAME_CURA: ${{ secrets.CONAN_USER }}
CONAN_PASSWORD_CURA: ${{ secrets.CONAN_PASS }} CONAN_PASSWORD_CURA: ${{ secrets.CONAN_PASS }}
CONAN_LOGIN_USERNAME_CURA_CE: ${{ secrets.CONAN_USER }} CONAN_LOGIN_USERNAME_CURA_CE: ${{ secrets.CONAN_USER }}
CONAN_PASSWORD_CURA_CE: ${{ secrets.CONAN_PASS }} CONAN_PASSWORD_CURA_CE: ${{ secrets.CONAN_PASS }}
CONAN_LOG_RUN_TO_OUTPUT: 1 CONAN_LOG_RUN_TO_OUTPUT: 1
CONAN_LOGGING_LEVEL: info CONAN_LOGGING_LEVEL: info
CONAN_NON_INTERACTIVE: 1 CONAN_NON_INTERACTIVE: 1
permissions: permissions:
contents: read contents: read
jobs: jobs:
conan-recipe-version: conan-recipe-version:
uses: ultimaker/cura/.github/workflows/conan-recipe-version.yml@main 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: with:
project_name: cura fetch-depth: 2
testing: - name: Setup Python and pip
runs-on: ubuntu-22.04 uses: actions/setup-python@v4
needs: [ conan-recipe-version ] with:
python-version: '3.11.x'
architecture: 'x64'
cache: 'pip'
cache-dependency-path: .github/workflows/requirements-conan-package.txt
steps: - name: Install Python requirements and Create default Conan profile
- name: Checkout run: pip install -r requirements-conan-package.txt
uses: actions/checkout@v3 working-directory: .github/workflows/
with:
fetch-depth: 2
- name: Setup Python and pip - name: Use Conan download cache (Bash)
uses: actions/setup-python@v4 if: ${{ runner.os != 'Windows' }}
with: run: conan config set storage.download_cache="$HOME/.conan/conan_download_cache"
python-version: '3.11.x'
architecture: 'x64'
cache: 'pip'
cache-dependency-path: .github/workflows/requirements-conan-package.txt
- name: Install Python requirements and Create default Conan profile - name: Cache Conan local repository packages (Bash)
run: pip install -r requirements-conan-package.txt uses: actions/cache@v3
working-directory: .github/workflows/ 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) # NOTE: Due to what are probably github issues, we have to remove the cache and reconfigure before the rest.
if: ${{ runner.os != 'Windows' }} # This is maybe because grub caches the disk it uses last time, which is recreated each time.
run: conan config set storage.download_cache="$HOME/.conan/conan_download_cache" - 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) - name: Install GCC-13
uses: actions/cache@v3 run: |
if: ${{ runner.os != 'Windows' }} sudo apt install g++-13 gcc-13 -y
with: sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-13 13
path: | sudo update-alternatives --install /usr/bin/g++ g++ /usr/bin/g++-13 13
$HOME/.conan/data
$HOME/.conan/conan_download_cache
key: conan-${{ runner.os }}-${{ runner.arch }}-unit-cache
# NOTE: Due to what are probably github issues, we have to remove the cache and reconfigure before the rest. - name: Get Conan configuration
# This is maybe because grub caches the disk it uses last time, which is recreated each time. run: |
- name: Install Linux system requirements conan config install https://github.com/Ultimaker/conan-config.git
if: ${{ runner.os == 'Linux' }} conan config install https://github.com/Ultimaker/conan-config.git -a "-b runner/${{ runner.os }}/${{ runner.arch }}"
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: Install GCC-12 on ubuntu-22.04 - name: Get Conan profile
run: | run: conan profile new default --detect --force
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 - name: Install dependencies
run: conan profile new default --detect --force 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 - name: Upload the Dependency package(s)
run: conan install . ${{ needs.conan-recipe-version.outputs.recipe_id_full }} --build=missing --update -o cura:devtools=True -g VirtualPythonEnv -if venv run: conan upload "*" -r cura --all -c
- name: Upload the Dependency package(s) - name: Set Environment variables for Cura (bash)
run: conan upload "*" -r cura --all -c if: ${{ runner.os != 'Windows' }}
run: |
. ./venv/bin/activate_github_actions_env.sh
- name: Set Environment variables for Cura (bash) - name: Run Unit Test
if: ${{ runner.os != 'Windows' }} id: run-test
run: | run: |
. ./venv/bin/activate_github_actions_env.sh pytest --junitxml=junit_cura.xml
working-directory: tests
- name: Run Unit Test - name: Save PR metadata
id: run-test if: always()
run: | run: |
pytest --junitxml=junit_cura.xml echo ${{ github.event.number }} > pr-id.txt
working-directory: tests 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 - name: Upload Test Results
if: always() if: always()
run: | uses: actions/upload-artifact@v3
echo ${{ github.event.number }} > pr-id.txt with:
echo ${{ github.event.pull_request.head.repo.full_name }} > pr-head-repo.txt name: test-result
echo ${{ github.event.pull_request.head.ref }} > pr-head-ref.txt path: |
working-directory: tests tests/**/*.xml
tests/pr-id.txt
- name: Upload Test Results tests/pr-head-repo.txt
if: always() tests/pr-head-ref.txt
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

View file

@ -1,82 +1,87 @@
name: update-translations name: update-translations
on: on:
push: push:
paths: paths:
- 'plugins/**' - 'plugins/**'
- 'resources/**' - 'resources/**'
- 'cura/**' - 'cura/**'
- 'icons/**' - 'icons/**'
- 'tests/**' - 'tests/**'
- 'packaging/**' - 'packaging/**'
- '.github/workflows/conan-*.yml' - '.github/workflows/conan-*.yml'
- '.github/workflows/notify.yml' - '.github/workflows/notify.yml'
- '.github/workflows/requirements-conan-package.txt' - '.github/workflows/requirements-conan-package.txt'
- 'requirements*.txt' - 'requirements*.txt'
- 'conanfile.py' - 'conanfile.py'
- 'conandata.yml' - 'conandata.yml'
- 'GitVersion.yml' - 'GitVersion.yml'
- '*.jinja' - '*.jinja'
branches: branches:
- '[1-9].[0-9]' - '[1-9].[0-9]'
- '[1-9].[0-9][0-9]' - '[1-9].[0-9][0-9]'
tags: tags:
- '[1-9].[0-9].[0-9]*' - '[1-9].[0-9].[0-9]*'
- '[1-9].[0-9].[0-9]' - '[1-9].[0-9].[0-9]'
- '[1-9].[0-9][0-9].[0-9]*' - '[1-9].[0-9][0-9].[0-9]*'
jobs: jobs:
update-translations: update-translations:
name: Update translations name: Update translations
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v3
- name: Cache Conan data - name: Cache Conan data
id: cache-conan id: cache-conan
uses: actions/cache@v3 uses: actions/cache@v3
with: with:
path: ~/.conan path: ~/.conan
key: ${{ runner.os }}-conan key: ${{ runner.os }}-conan
- name: Setup Python and pip - name: Setup Python and pip
uses: actions/setup-python@v4 uses: actions/setup-python@v4
with: with:
python-version: 3.10.x python-version: 3.11.x
cache: pip cache: pip
cache-dependency-path: .github/workflows/requirements-conan-package.txt cache-dependency-path: .github/workflows/requirements-conan-package.txt
- name: Install Python requirements for runner - name: Install Python requirements for runner
run: pip install -r .github/workflows/requirements-conan-package.txt 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. # 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. # This is maybe because grub caches the disk it uses last time, which is recreated each time.
- name: Install Linux system requirements - name: Install Linux system requirements
if: ${{ runner.os == 'Linux' }} if: ${{ runner.os == 'Linux' }}
run: | run: |
sudo rm /var/cache/debconf/config.dat sudo rm /var/cache/debconf/config.dat
sudo dpkg --configure -a sudo dpkg --configure -a
sudo add-apt-repository ppa:ubuntu-toolchain-r/test -y sudo add-apt-repository ppa:ubuntu-toolchain-r/test -y
sudo apt update sudo apt update
sudo apt upgrade 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 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: Create the default Conan profile - name: Install GCC-13
run: conan profile new default --detect --force 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 - name: Create the default Conan profile
run: conan config install https://github.com/Ultimaker/conan-config.git run: conan profile new default --detect --force
- name: generate the files using Conan install - name: Get Conan configuration
run: conan install . --build=missing --update -o cura:devtools=True 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 - name: generate the files using Conan install
with: run: conan install . --build=missing --update -o cura:devtools=True
file_pattern: resources/i18n/*.po resources/i18n/*.pot
status_options: --untracked-files=no - uses: stefanzweifel/git-auto-commit-action@v4
commit_message: update translations with:
file_pattern: resources/i18n/*.po resources/i18n/*.pot
status_options: --untracked-files=no
commit_message: update translations

View file

@ -8,17 +8,20 @@ from UM.Logger import Logger
from UM.Message import Message from UM.Message import Message
from UM.Scene.SceneNode import SceneNode from UM.Scene.SceneNode import SceneNode
from UM.i18n import i18nCatalog 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") i18n_catalog = i18nCatalog("cura")
class ArrangeObjectsJob(Job): 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__() super().__init__()
self._nodes = nodes self._nodes = nodes
self._fixed_nodes = fixed_nodes self._fixed_nodes = fixed_nodes
self._min_offset = min_offset self._min_offset = min_offset
self._grid_arrange = grid_arrange
def run(self): def run(self):
found_solution_for_all = False found_solution_for_all = False
@ -29,10 +32,18 @@ class ArrangeObjectsJob(Job):
title = i18n_catalog.i18nc("@info:title", "Finding Location")) title = i18n_catalog.i18nc("@info:title", "Finding Location"))
status_message.show() 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: 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 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() status_message.hide()

View file

@ -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

View file

@ -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

View file

@ -15,149 +15,137 @@ from UM.Operations.AddSceneNodeOperation import AddSceneNodeOperation
from UM.Operations.GroupedOperation import GroupedOperation from UM.Operations.GroupedOperation import GroupedOperation
from UM.Operations.RotateOperation import RotateOperation from UM.Operations.RotateOperation import RotateOperation
from UM.Operations.TranslateOperation import TranslateOperation from UM.Operations.TranslateOperation import TranslateOperation
from cura.Arranging.Arranger import Arranger
if TYPE_CHECKING: if TYPE_CHECKING:
from UM.Scene.SceneNode import SceneNode from UM.Scene.SceneNode import SceneNode
from cura.BuildVolume import BuildVolume 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]]: class Nest2DArrange(Arranger):
""" def __init__(self,
Find placement for a set of scene nodes, but don't actually move them just yet. nodes_to_arrange: List["SceneNode"],
:param nodes_to_arrange: The list of nodes that need to be moved. build_volume: "BuildVolume",
:param build_volume: The build volume that we want to place the nodes in. It gets size & disallowed areas from this. fixed_nodes: Optional[List["SceneNode"]] = None,
:param fixed_nodes: List of nods that should not be moved, but should be used when deciding where the others nodes *,
are placed. factor: int = 10000,
:param factor: The library that we use is int based. This factor defines how accurate we want it to be. 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) def findNodePlacement(self) -> Tuple[bool, List[Item]]:
WHERE spacing = int(1.5 * self._factor) # 1.5mm spacing.
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.
machine_width = build_volume.getWidth() machine_width = self._build_volume.getWidth()
machine_depth = build_volume.getDepth() machine_depth = self._build_volume.getDepth()
build_plate_bounding_box = Box(int(machine_width * factor), int(machine_depth * factor)) build_plate_bounding_box = Box(int(machine_width * self._factor), int(machine_depth * self._factor))
if fixed_nodes is None: if self._fixed_nodes is None:
fixed_nodes = [] self._fixed_nodes = []
# Add all the items we want to arrange # Add all the items we want to arrange
node_items = [] node_items = []
for node in nodes_to_arrange: for node in self._nodes_to_arrange:
hull_polygon = node.callDecoration("getConvexHull") hull_polygon = node.callDecoration("getConvexHull")
if not hull_polygon or hull_polygon.getPoints is None: 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())) Logger.log("w", "Object {} cannot be arranged because it has no convex hull.".format(node.getName()))
continue continue
converted_points = [] 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
for point in hull_polygon.getPoints(): 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 = Item(converted_points)
item.markAsFixedInBin(0)
node_items.append(item) node_items.append(item)
num_disallowed_areas_added += 1
config = NfpConfig() # Use a tiny margin for the build_plate_polygon (the nesting doesn't like overlapping disallowed areas)
config.accuracy = 1.0 half_machine_width = 0.5 * machine_width - 1
config.alignment = NfpConfig.Alignment.DONT_ALIGN 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. # Clip the disallowed areas so that they don't overlap the bounding box (The arranger chokes otherwise)
node_items = list(filter(lambda item: not item.isFixed(), node_items)) 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"], if hull_polygon is not None and hull_polygon.getPoints() is not None and len(
build_volume: "BuildVolume", hull_polygon.getPoints()) > 2: # numpy array has to be explicitly checked against None
fixed_nodes: Optional[List["SceneNode"]] = None, for point in hull_polygon.getPoints():
factor = 10000, converted_points.append(Point(int(point[0] * self._factor), int(point[1] * self._factor)))
add_new_nodes_in_scene: bool = False) -> Tuple[GroupedOperation, int]: item = Item(converted_points)
scene_root = Application.getInstance().getController().getScene().getRoot() item.markAsFixedInBin(0)
found_solution_for_all, node_items = findNodePlacement(nodes_to_arrange, build_volume, fixed_nodes, factor) node_items.append(item)
num_disallowed_areas_added += 1
not_fit_count = 0 config = NfpConfig()
grouped_operation = GroupedOperation() config.accuracy = 1.0
for node, node_item in zip(nodes_to_arrange, node_items): config.alignment = NfpConfig.Alignment.DONT_ALIGN
if add_new_nodes_in_scene: if self._lock_rotation:
grouped_operation.addOperation(AddSceneNodeOperation(node, scene_root)) config.rotations = [0.0]
if node_item.binId() == 0: num_bins = nest(node_items, build_plate_bounding_box, spacing, config)
# 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
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"], return found_solution_for_all, node_items
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: 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) not_fit_count = 0
grouped_operation.push() grouped_operation = GroupedOperation()
return not_fit_count == 0 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

View file

@ -203,6 +203,9 @@ class BuildVolume(SceneNode):
if shape: if shape:
self._shape = shape self._shape = shape
def getShape(self) -> str:
return self._shape
def getDiagonalSize(self) -> float: def getDiagonalSize(self) -> float:
"""Get the length of the 3D diagonal through the build volume. """Get the length of the 3D diagonal through the build volume.

View file

@ -22,7 +22,10 @@ from cura.Operations.SetParentOperation import SetParentOperation
from cura.MultiplyObjectsJob import MultiplyObjectsJob from cura.MultiplyObjectsJob import MultiplyObjectsJob
from cura.Settings.SetObjectExtruderOperation import SetObjectExtruderOperation from cura.Settings.SetObjectExtruderOperation import SetObjectExtruderOperation
from cura.Settings.ExtruderManager import ExtruderManager 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 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) center_operation = TranslateOperation(current_node, Vector(0, center_y, 0), set_position = True)
operation.addOperation(center_operation) operation.addOperation(center_operation)
operation.push() operation.push()
@pyqtSlot(int) @pyqtSlot(int)
def multiplySelection(self, count: int) -> None: def multiplySelection(self, count: int) -> None:
"""Multiply all objects in the selection """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. :param count: The number of times to multiply the selection.
""" """
min_offset = cura.CuraApplication.CuraApplication.getInstance().getBuildVolume().getEdgeDisallowedSize() + 2 # Allow for some rounding errors 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() job.start()
@pyqtSlot() @pyqtSlot()
@ -229,9 +241,9 @@ class CuraActions(QObject):
if node.callDecoration("isSliceable"): if node.callDecoration("isSliceable"):
fixed_nodes.append(node) fixed_nodes.append(node)
# Add the new nodes to the scene, and arrange them # Add the new nodes to the scene, and arrange them
group_operation, not_fit_count = createGroupOperationForArrange(nodes, application.getBuildVolume(),
fixed_nodes, factor=10000, arranger = GridArrange(nodes, application.getBuildVolume(), fixed_nodes)
add_new_nodes_in_scene=True) group_operation, not_fit_count = arranger.createGroupOperationForArrange(add_new_nodes_in_scene = True)
group_operation.push() group_operation.push()
# deselect currently selected nodes, and select the new nodes # deselect currently selected nodes, and select the new nodes

View file

@ -55,7 +55,6 @@ from cura import ApplicationMetadata
from cura.API import CuraAPI from cura.API import CuraAPI
from cura.API.Account import Account from cura.API.Account import Account
from cura.Arranging.ArrangeObjectsJob import ArrangeObjectsJob from cura.Arranging.ArrangeObjectsJob import ArrangeObjectsJob
from cura.Arranging.Nest2DArrange import arrange
from cura.Machines.MachineErrorChecker import MachineErrorChecker from cura.Machines.MachineErrorChecker import MachineErrorChecker
from cura.Machines.Models.BuildPlateModel import BuildPlateModel from cura.Machines.Models.BuildPlateModel import BuildPlateModel
from cura.Machines.Models.CustomQualityProfilesDropDownMenuModel import CustomQualityProfilesDropDownMenuModel from cura.Machines.Models.CustomQualityProfilesDropDownMenuModel import CustomQualityProfilesDropDownMenuModel
@ -116,6 +115,7 @@ from . import CameraAnimation
from . import CuraActions from . import CuraActions
from . import PlatformPhysics from . import PlatformPhysics
from . import PrintJobPreviewImageProvider from . import PrintJobPreviewImageProvider
from .Arranging.Nest2DArrange import Nest2DArrange
from .AutoSave import AutoSave from .AutoSave import AutoSave
from .Machines.Models.CompatibleMachineModel import CompatibleMachineModel from .Machines.Models.CompatibleMachineModel import CompatibleMachineModel
from .Machines.Models.MachineListModel import MachineListModel from .Machines.Models.MachineListModel import MachineListModel
@ -1455,6 +1455,13 @@ class CuraApplication(QtApplication):
# Single build plate # Single build plate
@pyqtSlot() @pyqtSlot()
def arrangeAll(self) -> None: 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 = [] nodes_to_arrange = []
active_build_plate = self.getMultiBuildPlateModel().activeBuildPlate active_build_plate = self.getMultiBuildPlateModel().activeBuildPlate
locked_nodes = [] locked_nodes = []
@ -1484,17 +1491,17 @@ class CuraApplication(QtApplication):
locked_nodes.append(node) locked_nodes.append(node)
else: else:
nodes_to_arrange.append(node) 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 """Arrange a set of nodes given a set of fixed nodes
:param nodes: nodes that we have to place :param nodes: nodes that we have to place
:param fixed_nodes: nodes that are placed in the arranger before finding spots for nodes :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 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() job.start()
@pyqtSlot() @pyqtSlot()
@ -1981,7 +1988,8 @@ class CuraApplication(QtApplication):
if select_models_on_load: if select_models_on_load:
Selection.add(node) Selection.add(node)
try: try:
arrange(nodes_to_arrange, self.getBuildVolume(), fixed_nodes) arranger = Nest2DArrange(nodes_to_arrange, self.getBuildVolume(), fixed_nodes)
arranger.arrange()
except: except:
Logger.logException("e", "Failed to arrange the models") Logger.logException("e", "Failed to arrange the models")

View file

@ -14,17 +14,19 @@ from UM.Operations.TranslateOperation import TranslateOperation
from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
from UM.Scene.SceneNode import SceneNode from UM.Scene.SceneNode import SceneNode
from UM.i18n import i18nCatalog 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") i18n_catalog = i18nCatalog("cura")
class MultiplyObjectsJob(Job): 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__() super().__init__()
self._objects = objects self._objects = objects
self._count = count self._count: int = count
self._min_offset = min_offset self._min_offset: int = min_offset
self._grid_arrange: bool = grid_arrange
def run(self) -> None: def run(self) -> None:
status_message = Message(i18n_catalog.i18nc("@info:status", "Multiplying and placing objects"), lifetime = 0, status_message = Message(i18n_catalog.i18nc("@info:status", "Multiplying and placing objects"), lifetime = 0,
@ -39,7 +41,7 @@ class MultiplyObjectsJob(Job):
root = scene.getRoot() root = scene.getRoot()
processed_nodes = [] # type: List[SceneNode] processed_nodes: List[SceneNode] = []
nodes = [] nodes = []
fixed_nodes = [] fixed_nodes = []
@ -76,12 +78,12 @@ class MultiplyObjectsJob(Job):
found_solution_for_all = True found_solution_for_all = True
group_operation = GroupedOperation() group_operation = GroupedOperation()
if nodes: if nodes:
group_operation, not_fit_count = createGroupOperationForArrange(nodes, if self._grid_arrange:
Application.getInstance().getBuildVolume(), arranger = GridArrange(nodes, Application.getInstance().getBuildVolume(), fixed_nodes)
fixed_nodes, else:
factor = 10000, arranger = Nest2DArrange(nodes, Application.getInstance().getBuildVolume(), fixed_nodes, factor=1000)
add_new_nodes_in_scene = True)
found_solution_for_all = not_fit_count == 0 group_operation, not_fit_count = arranger.createGroupOperationForArrange(add_new_nodes_in_scene=True)
if nodes_to_add_without_arrange: if nodes_to_add_without_arrange:
for nested_node in nodes_to_add_without_arrange: for nested_node in nodes_to_add_without_arrange:

Binary file not shown.

Before

Width:  |  Height:  |  Size: 381 KiB

After

Width:  |  Height:  |  Size: 417 KiB

Before After
Before After

View file

@ -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). # 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.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() archive = mesh_writer.getArchive()
if archive is None: # This happens if there was no mesh data to write. 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.") Logger.error("No permission to write workspace to this stream.")
return False return False
except EnvironmentError as e: 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))) Logger.error("EnvironmentError when writing workspace to this stream: {err}".format(err = str(e)))
return False return False
mesh_writer.setStoreArchive(False) mesh_writer.setStoreArchive(False)

View file

@ -237,9 +237,9 @@ class ThreeMFWriter(MeshWriter):
archive.writestr(model_file, scene_string) archive.writestr(model_file, scene_string)
archive.writestr(content_types_file, b'<?xml version="1.0" encoding="UTF-8"?> \n' + ET.tostring(content_types)) archive.writestr(content_types_file, b'<?xml version="1.0" encoding="UTF-8"?> \n' + ET.tostring(content_types))
archive.writestr(relations_file, b'<?xml version="1.0" encoding="UTF-8"?> \n' + ET.tostring(relations_element)) archive.writestr(relations_file, b'<?xml version="1.0" encoding="UTF-8"?> \n' + ET.tostring(relations_element))
except Exception as e: except Exception as error:
Logger.logException("e", "Error writing zip file") Logger.logException("e", "Error writing zip file")
self.setInformation(catalog.i18nc("@error:zip", "Error writing 3mf file.")) self.setInformation(str(error))
return False return False
finally: finally:
if not self._store_archive: if not self._store_archive:

View file

@ -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 = "<NOTICE> [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 = "<NOTICE> '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 = "<NOTICE> [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 = "<NOTICE> [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

View file

@ -5514,7 +5514,9 @@
"unit": "mm", "unit": "mm",
"type": "float", "type": "float",
"default_value": 0.0, "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", "limit_to_extruder": "support_interface_extruder_nr",
"enabled": "support_interface_enable and (support_enable or support_meshes_present)", "enabled": "support_interface_enable and (support_enable or support_meshes_present)",
"settable_per_mesh": false, "settable_per_mesh": false,
@ -5529,7 +5531,9 @@
"type": "float", "type": "float",
"default_value": 0.0, "default_value": 0.0,
"value": "extruderValue(support_roof_extruder_nr, 'support_interface_offset')", "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", "limit_to_extruder": "support_roof_extruder_nr",
"enabled": "support_roof_enable and (support_enable or support_meshes_present)", "enabled": "support_roof_enable and (support_enable or support_meshes_present)",
"settable_per_mesh": false, "settable_per_mesh": false,
@ -5543,7 +5547,9 @@
"type": "float", "type": "float",
"default_value": 0.0, "default_value": 0.0,
"value": "extruderValue(support_bottom_extruder_nr, 'support_interface_offset')", "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", "limit_to_extruder": "support_bottom_extruder_nr",
"enabled": "support_bottom_enable and (support_enable or support_meshes_present)", "enabled": "support_bottom_enable and (support_enable or support_meshes_present)",
"settable_per_mesh": false, "settable_per_mesh": false,

View file

@ -41,7 +41,7 @@ Item
property alias deleteAll: deleteAllAction property alias deleteAll: deleteAllAction
property alias reloadAll: reloadAllAction property alias reloadAll: reloadAllAction
property alias arrangeAll: arrangeAllAction property alias arrangeAll: arrangeAllAction
property alias arrangeSelection: arrangeSelectionAction property alias arrangeAllGrid: arrangeAllGridAction
property alias resetAllTranslation: resetAllTranslationAction property alias resetAllTranslation: resetAllTranslationAction
property alias resetAll: resetAllAction property alias resetAll: resetAllAction
@ -462,9 +462,10 @@ Item
Action Action
{ {
id: arrangeSelectionAction id: arrangeAllGridAction
text: catalog.i18nc("@action:inmenu menubar:edit","Arrange Selection") text: catalog.i18nc("@action:inmenu menubar:edit","Arrange All Models in a grid")
onTriggered: Printer.arrangeSelection() onTriggered: Printer.arrangeAllInGrid()
shortcut: "Shift+Ctrl+R"
} }
Action Action

View file

@ -822,12 +822,26 @@ UM.MainWindow
} }
} }
Cura.WizardDialog property var wizardDialog
Component
{ {
id: addMachineDialog id: addMachineDialogLoader
title: catalog.i18nc("@title:window", "Add Printer")
model: CuraApplication.getAddPrinterPagesModel() Cura.WizardDialog
progressBarVisible: false {
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 Cura.WizardDialog
@ -852,9 +866,8 @@ UM.MainWindow
target: Cura.Actions.addMachine target: Cura.Actions.addMachine
function onTriggered() function onTriggered()
{ {
// Make sure to show from the first page when the dialog shows up. wizardDialog = addMachineDialogLoader.createObject()
addMachineDialog.resetModelState() wizardDialog.show()
addMachineDialog.show()
} }
} }

View file

@ -55,6 +55,7 @@ UM.TooltipArea
} }
ScrollBar.vertical: UM.ScrollBar {} ScrollBar.vertical: UM.ScrollBar {}
clip: true
TextArea.flickable: TextArea TextArea.flickable: TextArea
{ {
@ -70,6 +71,7 @@ UM.TooltipArea
selectionColor: UM.Theme.getColor("text_selection") selectionColor: UM.Theme.getColor("text_selection")
selectedTextColor: UM.Theme.getColor("text") selectedTextColor: UM.Theme.getColor("text")
wrapMode: TextEdit.NoWrap wrapMode: TextEdit.NoWrap
padding: UM.Theme.getSize("narrow_margin").height + backgroundRectangle.border.width
onActiveFocusChanged: onActiveFocusChanged:
{ {
@ -81,8 +83,9 @@ UM.TooltipArea
background: Rectangle background: Rectangle
{ {
id: backgroundRectangle
anchors.fill: parent anchors.fill: parent
anchors.margins: -border.width //Wrap the border around the parent.
color: UM.Theme.getColor("detail_background") color: UM.Theme.getColor("detail_background")
border.color: border.color:

View file

@ -66,6 +66,7 @@ Cura.Menu
Cura.MenuSeparator {} Cura.MenuSeparator {}
Cura.MenuItem { action: Cura.Actions.selectAll } Cura.MenuItem { action: Cura.Actions.selectAll }
Cura.MenuItem { action: Cura.Actions.arrangeAll } Cura.MenuItem { action: Cura.Actions.arrangeAll }
Cura.MenuItem { action: Cura.Actions.arrangeAllGrid }
Cura.MenuItem { action: Cura.Actions.deleteAll } Cura.MenuItem { action: Cura.Actions.deleteAll }
Cura.MenuItem { action: Cura.Actions.reloadAll } Cura.MenuItem { action: Cura.Actions.reloadAll }
Cura.MenuItem { action: Cura.Actions.resetAllTranslation } Cura.MenuItem { action: Cura.Actions.resetAllTranslation }
@ -108,9 +109,7 @@ Cura.Menu
height: UM.Theme.getSize("small_popup_dialog").height height: UM.Theme.getSize("small_popup_dialog").height
minimumWidth: UM.Theme.getSize("small_popup_dialog").width minimumWidth: UM.Theme.getSize("small_popup_dialog").width
minimumHeight: UM.Theme.getSize("small_popup_dialog").height minimumHeight: UM.Theme.getSize("small_popup_dialog").height
onAccepted: gridPlacementSelected.checked? CuraActions.multiplySelectionToGrid(copiesField.value) : CuraActions.multiplySelection(copiesField.value)
onAccepted: CuraActions.multiplySelection(copiesField.value)
buttonSpacing: UM.Theme.getSize("thin_margin").width buttonSpacing: UM.Theme.getSize("thin_margin").width
rightButtons: 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") spacing: UM.Theme.getSize("default_margin").width
anchors.verticalCenter: copiesField.verticalCenter
width: contentWidth UM.Label
wrapMode: Text.NoWrap {
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 id: gridPlacementSelected
editable: true text: catalog.i18nc("@label", "Grid Placement")
focus: true
from: 1 UM.ToolTip
to: 99 {
width: 2 * UM.Theme.getSize("button").width visible: parent.hovered
value: 1 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.")
}
} }
} }
} }
} }

View file

@ -20,6 +20,7 @@ Item
width: childrenRect.width; width: childrenRect.width;
height: childrenRect.height; height: childrenRect.height;
id: checkboxTooltipArea id: checkboxTooltipArea
x: check.height
UM.CheckBox UM.CheckBox
{ {
id: check id: check
@ -40,7 +41,7 @@ Item
{ {
width: height width: height
height: check.height height: check.height
anchors.left: checkboxTooltipArea.right anchors.right: checkboxTooltipArea.left
anchors.leftMargin: 2 * screenScaleFactor anchors.leftMargin: 2 * screenScaleFactor
text: text:
@ -82,7 +83,7 @@ Item
source: UM.Theme.getIcon("Information") source: UM.Theme.getIcon("Information")
color: UM.Theme.getColor("primary_button_text") color: UM.Theme.getColor("small_button_text")
} }
visible: provider.properties.enabled == "False" visible: provider.properties.enabled == "False"

View file

@ -40,7 +40,7 @@ Item
Cura.TextField Cura.TextField
{ {
id: filter id: filter
height: parent.height implicitHeight: parent.height
anchors.left: parent.left anchors.left: parent.left
anchors.right: parent.right anchors.right: parent.right
topPadding: height / 4 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 onShowTooltip(text) { base.showTooltip(delegate, Qt.point(-settingsView.x - UM.Theme.getSize("default_margin").width, 0), text) }
function onHideTooltip() { base.hideTooltip() } function onHideTooltip() { base.hideTooltip() }
function onShowAllHiddenInheritedSettings() function onShowAllHiddenInheritedSettings(category_id)
{ {
var children_with_override = Cura.SettingInheritanceManager.getChildrenKeysWithOverride(category_id) var children_with_override = Cura.SettingInheritanceManager.getChildrenKeysWithOverride(category_id)
for(var i = 0; i < children_with_override.length; i++) for(var i = 0; i < children_with_override.length; i++)

View file

@ -3,6 +3,7 @@
import QtQuick 2.10 import QtQuick 2.10
import QtQuick.Controls 2.3 import QtQuick.Controls 2.3
import QtQuick.Layouts 2.3
import UM 1.5 as UM import UM 1.5 as UM
import Cura 1.1 as Cura import Cura 1.1 as Cura
@ -15,9 +16,7 @@ import Cura 1.1 as Cura
Item Item
{ {
id: base id: base
height: networkPrinterInfo.height + controlsRectangle.height
property alias maxItemCountAtOnce: networkPrinterListView.maxItemCountAtOnce
property var currentItem: (networkPrinterListView.currentIndex >= 0) property var currentItem: (networkPrinterListView.currentIndex >= 0)
? networkPrinterListView.model[networkPrinterListView.currentIndex] ? networkPrinterListView.model[networkPrinterListView.currentIndex]
: null : null
@ -29,35 +28,32 @@ Item
Item Item
{ {
id: networkPrinterInfo id: networkPrinterInfo
height: networkPrinterListView.visible ? networkPrinterListView.height : noPrinterLabel.height
anchors.left: parent.left anchors.left: parent.left
anchors.right: parent.right anchors.right: parent.right
anchors.top: parent.top anchors.top: parent.top
anchors.bottom: separator.top
UM.Label UM.Label
{ {
id: noPrinterLabel id: noPrinterLabel
height: UM.Theme.getSize("setting_control").height + UM.Theme.getSize("default_margin").height height: UM.Theme.getSize("setting_control").height + UM.Theme.getSize("default_margin").height
anchors.left: parent.left anchors.fill: parent
anchors.leftMargin: UM.Theme.getSize("default_margin").width anchors.margins: UM.Theme.getSize("default_margin").width
text: catalog.i18nc("@label", "There is no printer found over your network.") text: catalog.i18nc("@label", "There is no printer found over your network.")
visible: networkPrinterListView.count == 0 // Do not show if there are discovered devices. visible: networkPrinterListView.count == 0 // Do not show if there are discovered devices.
verticalAlignment: Text.AlignTop
} }
ListView ListView
{ {
id: networkPrinterListView id: networkPrinterListView
anchors.top: parent.top anchors.fill: parent
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)
ScrollBar.vertical: UM.ScrollBar ScrollBar.vertical: UM.ScrollBar
{ {
id: networkPrinterScrollBar id: networkPrinterScrollBar
} }
clip: true clip: true
property int maxItemCountAtOnce: 8 // show at max 8 items at once, otherwise you need to scroll.
visible: networkPrinterListView.count > 0 visible: networkPrinterListView.count > 0
model: contentLoader.enabled ? CuraApplication.getDiscoveredPrintersModel().discoveredPrinters: undefined model: contentLoader.enabled ? CuraApplication.getDiscoveredPrintersModel().discoveredPrinters: undefined
@ -138,7 +134,7 @@ Item
{ {
id: separator id: separator
anchors.left: parent.left anchors.left: parent.left
anchors.top: networkPrinterInfo.bottom anchors.bottom: controlsRectangle.top
anchors.right: parent.right anchors.right: parent.right
height: UM.Theme.getSize("default_lining").height height: UM.Theme.getSize("default_lining").height
color: UM.Theme.getColor("lining") color: UM.Theme.getColor("lining")
@ -149,7 +145,7 @@ Item
id: controlsRectangle id: controlsRectangle
anchors.left: parent.left anchors.left: parent.left
anchors.right: parent.right 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 height: UM.Theme.getSize("message_action_button").height + UM.Theme.getSize("default_margin").height

View file

@ -3,6 +3,7 @@
import QtQuick 2.10 import QtQuick 2.10
import QtQuick.Controls 2.3 import QtQuick.Controls 2.3
import QtQuick.Layouts 2.3
import UM 1.5 as UM import UM 1.5 as UM
import Cura 1.1 as Cura import Cura 1.1 as Cura
@ -17,79 +18,84 @@ Item
property var goToUltimakerPrinter property var goToUltimakerPrinter
DropDownWidget ColumnLayout
{ {
id: addNetworkPrinterDropDown
anchors.top: parent.top 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.left: parent.left
anchors.right: parent.right anchors.right: parent.right
anchors.topMargin: UM.Theme.getSize("wide_margin").height
title: catalog.i18nc("@label", "Add a networked printer") spacing: UM.Theme.getSize("default_margin").height
contentShown: true // by default expand the network printer list
onClicked: DropDownWidget
{ {
addLocalPrinterDropDown.contentShown = !contentShown id: addNetworkPrinterDropDown
}
contentComponent: networkPrinterListComponent Layout.fillWidth: true
Component Layout.fillHeight: contentShown
{
id: networkPrinterListComponent title: catalog.i18nc("@label", "Add a networked printer")
AddNetworkPrinterScrollView 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. contentComponent: networkPrinterListComponent
Component
onRefreshButtonClicked: {
id: networkPrinterListComponent
AddNetworkPrinterScrollView
{ {
UM.OutputDeviceManager.startDiscovery() id: networkPrinterScrollView
}
onAddByIpButtonClicked: onRefreshButtonClicked:
{
base.goToPage("add_printer_by_ip")
}
onAddCloudPrinterButtonClicked:
{
base.goToPage("add_cloud_printers")
if (!Cura.API.account.isLoggedIn)
{ {
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 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:
{ {
addNetworkPrinterDropDown.contentShown = !contentShown id: addLocalPrinterDropDown
}
contentComponent: localPrinterListComponent Layout.fillWidth: true
Component Layout.fillHeight: contentShown
{
id: localPrinterListComponent title: catalog.i18nc("@label", "Add a non-networked printer")
AddLocalPrinterScrollView
onClicked:
{ {
id: localPrinterView addNetworkPrinterDropDown.contentShown = !contentShown
height: backButton.y - addLocalPrinterDropDown.y - UM.Theme.getSize("expandable_component_content_header").height - UM.Theme.getSize("default_margin").height }
contentComponent: localPrinterListComponent
Component
{
id: localPrinterListComponent
AddLocalPrinterScrollView
{
id: localPrinterView
}
} }
} }
} }

View file

@ -22,7 +22,7 @@ Item
id: base id: base
implicitWidth: 200 * screenScaleFactor 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 var contentComponent: null
property alias contentItem: contentLoader.item property alias contentItem: contentLoader.item
@ -56,12 +56,14 @@ Item
Cura.RoundedRectangle Cura.RoundedRectangle
{ {
id: contentRectangle id: contentRectangle
anchors.top: header.bottom
// Move up a bit (exactly the width of the border) to avoid double line // 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.left: header.left
anchors.right: header.right anchors.right: header.right
anchors.bottom: parent.bottom
// Add 2x lining, because it needs a bit of space on the top and the 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.width: UM.Theme.getSize("default_lining").width
border.color: UM.Theme.getColor("lining") border.color: UM.Theme.getColor("lining")
@ -73,9 +75,7 @@ Item
Loader Loader
{ {
id: contentLoader id: contentLoader
anchors.top: parent.top anchors.fill: parent
anchors.left: parent.left
anchors.right: parent.right
// Keep a small margin with the Rectangle container so its content will not overlap with the Rectangle // Keep a small margin with the Rectangle container so its content will not overlap with the Rectangle
// border. // border.
anchors.margins: UM.Theme.getSize("default_lining").width anchors.margins: UM.Theme.getSize("default_lining").width

View file

@ -32,11 +32,6 @@ Window
property var model: null // Needs to be set by whoever is using this dialog. property var model: null // Needs to be set by whoever is using this dialog.
property alias progressBarVisible: wizardPanel.progressBarVisible property alias progressBarVisible: wizardPanel.progressBarVisible
function resetModelState()
{
model.resetState()
}
WizardPanel WizardPanel
{ {
id: wizardPanel id: wizardPanel