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