mirror of
https://git.mirrors.martin98.com/https://github.com/Ultimaker/Cura
synced 2025-08-12 16:59:01 +08:00
Merge branch 'main' into CURA-10896_improve_add_printer_wizard
This commit is contained in:
commit
4a8a8d35f6
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
|
name: Create and Upload Conan package
|
||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_call:
|
workflow_call:
|
||||||
inputs:
|
inputs:
|
||||||
project_name:
|
project_name:
|
||||||
required: true
|
required: true
|
||||||
type: string
|
type: string
|
||||||
|
|
||||||
recipe_id_full:
|
recipe_id_full:
|
||||||
required: true
|
required: true
|
||||||
type: string
|
type: string
|
||||||
|
|
||||||
build_id:
|
build_id:
|
||||||
required: true
|
required: true
|
||||||
type: number
|
type: number
|
||||||
|
|
||||||
build_info:
|
build_info:
|
||||||
required: false
|
required: false
|
||||||
default: true
|
default: true
|
||||||
type: boolean
|
type: boolean
|
||||||
|
|
||||||
recipe_id_latest:
|
recipe_id_latest:
|
||||||
required: false
|
required: false
|
||||||
type: string
|
type: string
|
||||||
|
|
||||||
runs_on:
|
runs_on:
|
||||||
required: true
|
required: true
|
||||||
type: string
|
type: string
|
||||||
|
|
||||||
python_version:
|
python_version:
|
||||||
required: true
|
required: true
|
||||||
type: string
|
type: string
|
||||||
|
|
||||||
conan_config_branch:
|
conan_config_branch:
|
||||||
required: false
|
required: false
|
||||||
type: string
|
type: string
|
||||||
|
|
||||||
conan_logging_level:
|
conan_logging_level:
|
||||||
required: false
|
required: false
|
||||||
type: string
|
type: string
|
||||||
|
|
||||||
conan_clean_local_cache:
|
conan_clean_local_cache:
|
||||||
required: false
|
required: false
|
||||||
type: boolean
|
type: boolean
|
||||||
default: false
|
default: false
|
||||||
|
|
||||||
conan_upload_community:
|
conan_upload_community:
|
||||||
required: false
|
required: false
|
||||||
default: true
|
default: true
|
||||||
type: boolean
|
type: boolean
|
||||||
|
|
||||||
env:
|
env:
|
||||||
CONAN_LOGIN_USERNAME: ${{ secrets.CONAN_USER }}
|
CONAN_LOGIN_USERNAME: ${{ secrets.CONAN_USER }}
|
||||||
CONAN_PASSWORD: ${{ secrets.CONAN_PASS }}
|
CONAN_PASSWORD: ${{ secrets.CONAN_PASS }}
|
||||||
CONAN_LOG_RUN_TO_OUTPUT: 1
|
CONAN_LOG_RUN_TO_OUTPUT: 1
|
||||||
CONAN_LOGGING_LEVEL: ${{ inputs.conan_logging_level }}
|
CONAN_LOGGING_LEVEL: ${{ inputs.conan_logging_level }}
|
||||||
CONAN_NON_INTERACTIVE: 1
|
CONAN_NON_INTERACTIVE: 1
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
conan-package-create:
|
conan-package-create:
|
||||||
runs-on: ${{ inputs.runs_on }}
|
runs-on: ${{ inputs.runs_on }}
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: Setup Python and pip
|
- name: Setup Python and pip
|
||||||
uses: actions/setup-python@v4
|
uses: actions/setup-python@v4
|
||||||
with:
|
with:
|
||||||
python-version: ${{ inputs.python_version }}
|
python-version: ${{ inputs.python_version }}
|
||||||
cache: 'pip'
|
cache: 'pip'
|
||||||
cache-dependency-path: .github/workflows/requirements-conan-package.txt
|
cache-dependency-path: .github/workflows/requirements-conan-package.txt
|
||||||
|
|
||||||
- name: Install Python requirements for runner
|
- name: Install Python requirements for runner
|
||||||
run: pip install -r https://raw.githubusercontent.com/Ultimaker/Cura/main/.github/workflows/requirements-conan-package.txt
|
run: pip install -r https://raw.githubusercontent.com/Ultimaker/Cura/main/.github/workflows/requirements-conan-package.txt
|
||||||
# Note the runner requirements are always installed from the main branch in the Ultimaker/Cura repo
|
# Note the runner requirements are always installed from the main branch in the Ultimaker/Cura repo
|
||||||
|
|
||||||
- name: Use Conan download cache (Bash)
|
- name: Use Conan download cache (Bash)
|
||||||
if: ${{ runner.os != 'Windows' }}
|
if: ${{ runner.os != 'Windows' }}
|
||||||
run: conan config set storage.download_cache="$HOME/.conan/conan_download_cache"
|
run: conan config set storage.download_cache="$HOME/.conan/conan_download_cache"
|
||||||
|
|
||||||
- name: Use Conan download cache (Powershell)
|
- name: Use Conan download cache (Powershell)
|
||||||
if: ${{ runner.os == 'Windows' }}
|
if: ${{ runner.os == 'Windows' }}
|
||||||
run: conan config set storage.download_cache="C:\Users\runneradmin\.conan\conan_download_cache"
|
run: conan config set storage.download_cache="C:\Users\runneradmin\.conan\conan_download_cache"
|
||||||
|
|
||||||
- name: Cache Conan local repository packages (Bash)
|
- name: Cache Conan local repository packages (Bash)
|
||||||
uses: actions/cache@v3
|
uses: actions/cache@v3
|
||||||
if: ${{ runner.os != 'Windows' }}
|
if: ${{ runner.os != 'Windows' }}
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
$HOME/.conan/data
|
$HOME/.conan/data
|
||||||
$HOME/.conan/conan_download_cache
|
$HOME/.conan/conan_download_cache
|
||||||
key: conan-${{ inputs.runs_on }}-${{ runner.arch }}-create-cache
|
key: conan-${{ inputs.runs_on }}-${{ runner.arch }}-create-cache
|
||||||
|
|
||||||
- name: Cache Conan local repository packages (Powershell)
|
- name: Cache Conan local repository packages (Powershell)
|
||||||
uses: actions/cache@v3
|
uses: actions/cache@v3
|
||||||
if: ${{ runner.os == 'Windows' }}
|
if: ${{ runner.os == 'Windows' }}
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
C:\Users\runneradmin\.conan\data
|
C:\Users\runneradmin\.conan\data
|
||||||
C:\.conan
|
C:\.conan
|
||||||
C:\Users\runneradmin\.conan\conan_download_cache
|
C:\Users\runneradmin\.conan\conan_download_cache
|
||||||
key: conan-${{ inputs.runs_on }}-${{ runner.arch }}-create-cache
|
key: conan-${{ inputs.runs_on }}-${{ runner.arch }}-create-cache
|
||||||
|
|
||||||
- name: Install MacOS system requirements
|
- name: Install MacOS system requirements
|
||||||
if: ${{ runner.os == 'Macos' }}
|
if: ${{ runner.os == 'Macos' }}
|
||||||
run: brew install autoconf automake ninja
|
run: brew install autoconf automake ninja
|
||||||
|
|
||||||
# NOTE: Due to what are probably github issues, we have to remove the cache and reconfigure before the rest.
|
# NOTE: Due to what are probably github issues, we have to remove the cache and reconfigure before the rest.
|
||||||
# This is maybe because grub caches the disk it uses last time, which is recreated each time.
|
# This is maybe because grub caches the disk it uses last time, which is recreated each time.
|
||||||
- name: Install Linux system requirements
|
- name: Install Linux system requirements
|
||||||
if: ${{ runner.os == 'Linux' }}
|
if: ${{ runner.os == 'Linux' }}
|
||||||
run: |
|
run: |
|
||||||
sudo rm /var/cache/debconf/config.dat
|
sudo rm /var/cache/debconf/config.dat
|
||||||
sudo dpkg --configure -a
|
sudo dpkg --configure -a
|
||||||
sudo add-apt-repository ppa:ubuntu-toolchain-r/test -y
|
sudo add-apt-repository ppa:ubuntu-toolchain-r/test -y
|
||||||
sudo apt update
|
sudo apt update
|
||||||
sudo apt upgrade
|
sudo apt upgrade
|
||||||
sudo apt install build-essential checkinstall libegl-dev zlib1g-dev libssl-dev ninja-build autoconf libx11-dev libx11-xcb-dev libfontenc-dev libice-dev libsm-dev libxau-dev libxaw7-dev libxcomposite-dev libxcursor-dev libxdamage-dev libxdmcp-dev libxext-dev libxfixes-dev libxi-dev libxinerama-dev libxkbfile-dev libxmu-dev libxmuu-dev libxpm-dev libxrandr-dev libxrender-dev libxres-dev libxss-dev libxt-dev libxtst-dev libxv-dev libxvmc-dev libxxf86vm-dev xtrans-dev libxcb-render0-dev libxcb-render-util0-dev libxcb-xkb-dev libxcb-icccm4-dev libxcb-image0-dev libxcb-keysyms1-dev libxcb-randr0-dev libxcb-shape0-dev libxcb-sync-dev libxcb-xfixes0-dev libxcb-xinerama0-dev xkb-data libxcb-dri3-dev uuid-dev libxcb-util-dev libxkbcommon-x11-dev pkg-config flex bison -y
|
sudo apt install build-essential checkinstall libegl-dev zlib1g-dev libssl-dev ninja-build autoconf libx11-dev libx11-xcb-dev libfontenc-dev libice-dev libsm-dev libxau-dev libxaw7-dev libxcomposite-dev libxcursor-dev libxdamage-dev libxdmcp-dev libxext-dev libxfixes-dev libxi-dev libxinerama-dev libxkbfile-dev libxmu-dev libxmuu-dev libxpm-dev libxrandr-dev libxrender-dev libxres-dev libxss-dev libxt-dev libxtst-dev libxv-dev libxvmc-dev libxxf86vm-dev xtrans-dev libxcb-render0-dev libxcb-render-util0-dev libxcb-xkb-dev libxcb-icccm4-dev libxcb-image0-dev libxcb-keysyms1-dev libxcb-randr0-dev libxcb-shape0-dev libxcb-sync-dev libxcb-xfixes0-dev libxcb-xinerama0-dev xkb-data libxcb-dri3-dev uuid-dev libxcb-util-dev libxkbcommon-x11-dev pkg-config flex bison -y
|
||||||
|
|
||||||
- name: Install GCC-12 on ubuntu-22.04
|
- name: Install GCC-132 on ubuntu
|
||||||
if: ${{ startsWith(inputs.runs_on, 'ubuntu-22.04') }}
|
if: ${{ startsWith(inputs.runs_on, 'ubuntu') }}
|
||||||
run: |
|
run: |
|
||||||
sudo apt install g++-12 gcc-12 -y
|
sudo apt install g++-13 gcc-13 -y
|
||||||
sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-12 12
|
sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-13 13
|
||||||
sudo update-alternatives --install /usr/bin/g++ g++ /usr/bin/g++-12 12
|
sudo update-alternatives --install /usr/bin/g++ g++ /usr/bin/g++-13 13
|
||||||
|
|
||||||
- name: Use GCC-10 on ubuntu-20.04
|
- name: Create the default Conan profile
|
||||||
if: ${{ startsWith(inputs.runs_on, 'ubuntu-20.04') }}
|
run: conan profile new default --detect
|
||||||
run: |
|
|
||||||
sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-10 10
|
|
||||||
sudo update-alternatives --install /usr/bin/g++ g++ /usr/bin/g++-10 10
|
|
||||||
|
|
||||||
- name: Create the default Conan profile
|
- name: Get Conan configuration from branch
|
||||||
run: conan profile new default --detect
|
if: ${{ inputs.conan_config_branch != '' }}
|
||||||
|
run: conan config install https://github.com/Ultimaker/conan-config.git -a "-b ${{ inputs.conan_config_branch }}"
|
||||||
|
|
||||||
- name: Get Conan configuration from branch
|
- name: Get Conan configuration
|
||||||
if: ${{ inputs.conan_config_branch != '' }}
|
run: |
|
||||||
run: conan config install https://github.com/Ultimaker/conan-config.git -a "-b ${{ inputs.conan_config_branch }}"
|
conan config install https://github.com/Ultimaker/conan-config.git
|
||||||
|
conan config install https://github.com/Ultimaker/conan-config.git -a "-b runner/${{ runner.os }}/${{ runner.arch }}"
|
||||||
|
|
||||||
- name: Get Conan configuration
|
- name: Add Cura private Artifactory remote
|
||||||
if: ${{ inputs.conan_config_branch == '' }}
|
run: conan remote add cura-private https://ultimaker.jfrog.io/artifactory/api/conan/cura-private True
|
||||||
run: conan config install https://github.com/Ultimaker/conan-config.git
|
|
||||||
|
|
||||||
- name: Add Cura private Artifactory remote
|
- name: Create the Packages
|
||||||
run: conan remote add cura-private https://ultimaker.jfrog.io/artifactory/api/conan/cura-private True
|
run: conan install ${{ inputs.recipe_id_full }} --build=missing --update -c tools.build:skip_test=True
|
||||||
|
|
||||||
- name: Create the Packages
|
- name: Upload the Package(s)
|
||||||
run: conan install ${{ inputs.recipe_id_full }} --build=missing --update
|
if: ${{ always() && inputs.conan_upload_community }}
|
||||||
|
run: conan upload ${{ inputs.recipe_id_full }} -r cura --all -c
|
||||||
|
|
||||||
- name: Upload the Package(s)
|
- name: Upload the Package(s) to the private Artifactory
|
||||||
if: ${{ always() && inputs.conan_upload_community }}
|
if: ${{ always() && ! inputs.conan_upload_community }}
|
||||||
run: conan upload ${{ inputs.recipe_id_full }} -r cura --all -c
|
run: conan upload ${{ inputs.recipe_id_full }} -r cura-private --all -c
|
||||||
|
|
||||||
- name: Upload the Package(s) to the private Artifactory
|
|
||||||
if: ${{ always() && ! inputs.conan_upload_community }}
|
|
||||||
run: conan upload ${{ inputs.recipe_id_full }} -r cura-private --all -c
|
|
||||||
|
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]*'
|
- '[1-9].[0-9][0-9].[0-9]*'
|
||||||
|
|
||||||
env:
|
env:
|
||||||
CONAN_LOGIN_USERNAME_CURA: ${{ secrets.CONAN_USER }}
|
CONAN_LOGIN_USERNAME_CURA: ${{ secrets.CONAN_USER }}
|
||||||
CONAN_PASSWORD_CURA: ${{ secrets.CONAN_PASS }}
|
CONAN_PASSWORD_CURA: ${{ secrets.CONAN_PASS }}
|
||||||
CONAN_LOGIN_USERNAME_CURA_CE: ${{ secrets.CONAN_USER }}
|
CONAN_LOGIN_USERNAME_CURA_CE: ${{ secrets.CONAN_USER }}
|
||||||
CONAN_PASSWORD_CURA_CE: ${{ secrets.CONAN_PASS }}
|
CONAN_PASSWORD_CURA_CE: ${{ secrets.CONAN_PASS }}
|
||||||
CONAN_LOG_RUN_TO_OUTPUT: 1
|
CONAN_LOG_RUN_TO_OUTPUT: 1
|
||||||
CONAN_LOGGING_LEVEL: ${{ inputs.conan_logging_level }}
|
CONAN_LOGGING_LEVEL: ${{ inputs.conan_logging_level }}
|
||||||
CONAN_NON_INTERACTIVE: 1
|
CONAN_NON_INTERACTIVE: 1
|
||||||
|
|
||||||
permissions: {}
|
permissions: { }
|
||||||
jobs:
|
jobs:
|
||||||
conan-recipe-version:
|
conan-recipe-version:
|
||||||
permissions:
|
permissions:
|
||||||
@ -103,18 +103,23 @@ jobs:
|
|||||||
sudo apt update
|
sudo apt update
|
||||||
sudo apt upgrade
|
sudo apt upgrade
|
||||||
sudo apt install efibootmgr build-essential checkinstall libegl-dev zlib1g-dev libssl-dev ninja-build autoconf libx11-dev libx11-xcb-dev libfontenc-dev libice-dev libsm-dev libxau-dev libxaw7-dev libxcomposite-dev libxcursor-dev libxdamage-dev libxdmcp-dev libxext-dev libxfixes-dev libxi-dev libxinerama-dev libxkbfile-dev libxmu-dev libxmuu-dev libxpm-dev libxrandr-dev libxrender-dev libxres-dev libxss-dev libxt-dev libxtst-dev libxv-dev libxvmc-dev libxxf86vm-dev xtrans-dev libxcb-render0-dev libxcb-render-util0-dev libxcb-xkb-dev libxcb-icccm4-dev libxcb-image0-dev libxcb-keysyms1-dev libxcb-randr0-dev libxcb-shape0-dev libxcb-sync-dev libxcb-xfixes0-dev libxcb-xinerama0-dev xkb-data libxcb-dri3-dev uuid-dev libxcb-util-dev libxkbcommon-x11-dev pkg-config flex bison g++-12 gcc-12 -y
|
sudo apt install efibootmgr build-essential checkinstall libegl-dev zlib1g-dev libssl-dev ninja-build autoconf libx11-dev libx11-xcb-dev libfontenc-dev libice-dev libsm-dev libxau-dev libxaw7-dev libxcomposite-dev libxcursor-dev libxdamage-dev libxdmcp-dev libxext-dev libxfixes-dev libxi-dev libxinerama-dev libxkbfile-dev libxmu-dev libxmuu-dev libxpm-dev libxrandr-dev libxrender-dev libxres-dev libxss-dev libxt-dev libxtst-dev libxv-dev libxvmc-dev libxxf86vm-dev xtrans-dev libxcb-render0-dev libxcb-render-util0-dev libxcb-xkb-dev libxcb-icccm4-dev libxcb-image0-dev libxcb-keysyms1-dev libxcb-randr0-dev libxcb-shape0-dev libxcb-sync-dev libxcb-xfixes0-dev libxcb-xinerama0-dev xkb-data libxcb-dri3-dev uuid-dev libxcb-util-dev libxkbcommon-x11-dev pkg-config flex bison g++-12 gcc-12 -y
|
||||||
sudo apt install g++-12 gcc-12 -y
|
|
||||||
sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-12 12
|
- name: Install GCC-13
|
||||||
sudo update-alternatives --install /usr/bin/g++ g++ /usr/bin/g++-12 12
|
run: |
|
||||||
|
sudo apt install g++-13 gcc-13 -y
|
||||||
|
sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-13 13
|
||||||
|
sudo update-alternatives --install /usr/bin/g++ g++ /usr/bin/g++-13 13
|
||||||
|
|
||||||
- name: Create the default Conan profile
|
- name: Create the default Conan profile
|
||||||
run: conan profile new default --detect --force
|
run: conan profile new default --detect --force
|
||||||
|
|
||||||
- name: Get Conan configuration
|
- name: Get Conan configuration
|
||||||
run: conan config install https://github.com/Ultimaker/conan-config.git
|
run: |
|
||||||
|
conan config install https://github.com/Ultimaker/conan-config.git
|
||||||
|
conan config install https://github.com/Ultimaker/conan-config.git -a "-b runner/${{ runner.os }}/${{ runner.arch }}"
|
||||||
|
|
||||||
- name: Create the Packages
|
- name: Create the Packages
|
||||||
run: conan create . ${{ needs.conan-recipe-version.outputs.recipe_id_full }} --build=missing --update -o ${{ needs.conan-recipe-version.outputs.project_name }}:devtools=True
|
run: conan create . ${{ needs.conan-recipe-version.outputs.recipe_id_full }} --build=missing --update -o ${{ needs.conan-recipe-version.outputs.project_name }}:devtools=True -c tools.build:skip_test=True
|
||||||
|
|
||||||
- name: Create the latest alias
|
- name: Create the latest alias
|
||||||
if: always()
|
if: always()
|
||||||
|
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
|
name: Export Conan Recipe to server
|
||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_call:
|
workflow_call:
|
||||||
inputs:
|
inputs:
|
||||||
recipe_id_full:
|
recipe_id_full:
|
||||||
required: true
|
required: true
|
||||||
type: string
|
type: string
|
||||||
|
|
||||||
recipe_id_latest:
|
recipe_id_latest:
|
||||||
required: false
|
required: false
|
||||||
type: string
|
type: string
|
||||||
|
|
||||||
runs_on:
|
runs_on:
|
||||||
required: true
|
required: true
|
||||||
type: string
|
type: string
|
||||||
|
|
||||||
python_version:
|
python_version:
|
||||||
required: true
|
required: true
|
||||||
type: string
|
type: string
|
||||||
|
|
||||||
conan_config_branch:
|
conan_config_branch:
|
||||||
required: false
|
required: false
|
||||||
type: string
|
type: string
|
||||||
|
|
||||||
conan_logging_level:
|
conan_logging_level:
|
||||||
required: false
|
required: false
|
||||||
type: string
|
type: string
|
||||||
|
|
||||||
conan_export_binaries:
|
conan_export_binaries:
|
||||||
required: false
|
required: false
|
||||||
type: boolean
|
type: boolean
|
||||||
|
|
||||||
conan_upload_community:
|
conan_upload_community:
|
||||||
required: false
|
required: false
|
||||||
default: true
|
default: true
|
||||||
type: boolean
|
type: boolean
|
||||||
|
|
||||||
env:
|
env:
|
||||||
CONAN_LOGIN_USERNAME: ${{ secrets.CONAN_USER }}
|
CONAN_LOGIN_USERNAME: ${{ secrets.CONAN_USER }}
|
||||||
CONAN_PASSWORD: ${{ secrets.CONAN_PASS }}
|
CONAN_PASSWORD: ${{ secrets.CONAN_PASS }}
|
||||||
CONAN_LOG_RUN_TO_OUTPUT: 1
|
CONAN_LOG_RUN_TO_OUTPUT: 1
|
||||||
CONAN_LOGGING_LEVEL: ${{ inputs.conan_logging_level }}
|
CONAN_LOGGING_LEVEL: ${{ inputs.conan_logging_level }}
|
||||||
CONAN_NON_INTERACTIVE: 1
|
CONAN_NON_INTERACTIVE: 1
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
package-export:
|
package-export:
|
||||||
runs-on: ${{ inputs.runs_on }}
|
runs-on: ${{ inputs.runs_on }}
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout project
|
- name: Checkout project
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: Setup Python and pip
|
- name: Setup Python and pip
|
||||||
uses: actions/setup-python@v4
|
uses: actions/setup-python@v4
|
||||||
with:
|
with:
|
||||||
python-version: ${{ inputs.python_version }}
|
python-version: ${{ inputs.python_version }}
|
||||||
cache: 'pip'
|
cache: 'pip'
|
||||||
cache-dependency-path: .github/workflows/requirements-conan-package.txt
|
cache-dependency-path: .github/workflows/requirements-conan-package.txt
|
||||||
|
|
||||||
- name: Install Python requirements and Create default Conan profile
|
- name: Install Python requirements and Create default Conan profile
|
||||||
run: |
|
run: |
|
||||||
pip install -r https://raw.githubusercontent.com/Ultimaker/Cura/main/.github/workflows/requirements-conan-package.txt
|
pip install -r https://raw.githubusercontent.com/Ultimaker/Cura/main/.github/workflows/requirements-conan-package.txt
|
||||||
conan profile new default --detect
|
conan profile new default --detect
|
||||||
# Note the runner requirements are always installed from the main branch in the Ultimaker/Cura repo
|
# Note the runner requirements are always installed from the main branch in the Ultimaker/Cura repo
|
||||||
|
|
||||||
- name: Cache Conan local repository packages
|
- name: Cache Conan local repository packages
|
||||||
uses: actions/cache@v3
|
uses: actions/cache@v3
|
||||||
with:
|
with:
|
||||||
path: $HOME/.conan/data
|
path: $HOME/.conan/data
|
||||||
key: ${{ runner.os }}-conan-export-cache
|
key: ${{ runner.os }}-conan-export-cache
|
||||||
|
|
||||||
- name: Get Conan configuration from branch
|
- name: Get Conan configuration from branch
|
||||||
if: ${{ inputs.conan_config_branch != '' }}
|
if: ${{ inputs.conan_config_branch != '' }}
|
||||||
run: conan config install https://github.com/Ultimaker/conan-config.git -a "-b ${{ inputs.conan_config_branch }}"
|
run: conan config install https://github.com/Ultimaker/conan-config.git -a "-b ${{ inputs.conan_config_branch }}"
|
||||||
|
|
||||||
- name: Get Conan configuration
|
- name: Get Conan configuration
|
||||||
if: ${{ inputs.conan_config_branch == '' }}
|
run: |
|
||||||
run: conan config install https://github.com/Ultimaker/conan-config.git
|
conan config install https://github.com/Ultimaker/conan-config.git
|
||||||
|
conan config install https://github.com/Ultimaker/conan-config.git -a "-b runner/${{ runner.os }}/${{ runner.arch }}"
|
||||||
|
|
||||||
- name: Add Cura private Artifactory remote
|
- name: Add Cura private Artifactory remote
|
||||||
run: conan remote add cura-private https://ultimaker.jfrog.io/artifactory/api/conan/cura-private True
|
run: conan remote add cura-private https://ultimaker.jfrog.io/artifactory/api/conan/cura-private True
|
||||||
|
|
||||||
- name: Export the Package (binaries)
|
- name: Export the Package (binaries)
|
||||||
if: ${{ inputs.conan_export_binaries }}
|
if: ${{ inputs.conan_export_binaries }}
|
||||||
run: conan create . ${{ inputs.recipe_id_full }} --build=missing --update
|
run: conan create . ${{ inputs.recipe_id_full }} --build=missing --update -c tools.build:skip_test=True
|
||||||
|
|
||||||
- name: Export the Package
|
- name: Export the Package
|
||||||
if: ${{ !inputs.conan_export_binaries }}
|
if: ${{ !inputs.conan_export_binaries }}
|
||||||
run: conan export . ${{ inputs.recipe_id_full }}
|
run: conan export . ${{ inputs.recipe_id_full }}
|
||||||
|
|
||||||
- name: Create the latest alias
|
- name: Create the latest alias
|
||||||
if: always()
|
if: always()
|
||||||
run: conan alias ${{ inputs.recipe_id_latest }} ${{ inputs.recipe_id_full }}
|
run: conan alias ${{ inputs.recipe_id_latest }} ${{ inputs.recipe_id_full }}
|
||||||
|
|
||||||
- name: Upload the Package(s)
|
- name: Upload the Package(s)
|
||||||
if: ${{ always() && inputs.conan_upload_community }}
|
if: ${{ always() && inputs.conan_upload_community }}
|
||||||
run: |
|
run: |
|
||||||
conan upload ${{ inputs.recipe_id_full }} -r cura --all -c
|
conan upload ${{ inputs.recipe_id_full }} -r cura --all -c
|
||||||
conan upload ${{ inputs.recipe_id_latest }} -r cura -c
|
conan upload ${{ inputs.recipe_id_latest }} -r cura -c
|
||||||
|
|
||||||
- name: Upload the Package(s) to the private Artifactory
|
- name: Upload the Package(s) to the private Artifactory
|
||||||
if: ${{ always() && ! inputs.conan_upload_community }}
|
if: ${{ always() && ! inputs.conan_upload_community }}
|
||||||
run: |
|
run: |
|
||||||
conan upload ${{ inputs.recipe_id_full }} -r cura-private --all -c
|
conan upload ${{ inputs.recipe_id_full }} -r cura-private --all -c
|
||||||
conan upload ${{ inputs.recipe_id_latest }} -r cura-private -c
|
conan upload ${{ inputs.recipe_id_latest }} -r cura-private -c
|
||||||
|
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 }}
|
run-name: ${{ inputs.cura_conan_version }} for Linux-${{ inputs.architecture }} by @${{ github.actor }}
|
||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs:
|
inputs:
|
||||||
cura_conan_version:
|
cura_conan_version:
|
||||||
description: 'Cura Conan Version'
|
description: 'Cura Conan Version'
|
||||||
default: 'cura/latest@ultimaker/testing'
|
default: 'cura/latest@ultimaker/testing'
|
||||||
required: true
|
required: true
|
||||||
type: string
|
type: string
|
||||||
conan_args:
|
conan_args:
|
||||||
description: 'Conan args: eq.: --require-override'
|
description: 'Conan args: eq.: --require-override'
|
||||||
default: ''
|
default: ''
|
||||||
required: false
|
required: false
|
||||||
type: string
|
type: string
|
||||||
enterprise:
|
enterprise:
|
||||||
description: 'Build Cura as an Enterprise edition'
|
description: 'Build Cura as an Enterprise edition'
|
||||||
default: false
|
default: false
|
||||||
required: true
|
required: true
|
||||||
type: boolean
|
type: boolean
|
||||||
staging:
|
staging:
|
||||||
description: 'Use staging API'
|
description: 'Use staging API'
|
||||||
default: false
|
default: false
|
||||||
required: true
|
required: true
|
||||||
type: boolean
|
type: boolean
|
||||||
architecture:
|
architecture:
|
||||||
description: 'Architecture'
|
description: 'Architecture'
|
||||||
required: true
|
required: true
|
||||||
default: 'X64'
|
default: 'X64'
|
||||||
type: choice
|
type: choice
|
||||||
options:
|
options:
|
||||||
- X64
|
- X64
|
||||||
operating_system:
|
operating_system:
|
||||||
description: 'OS'
|
description: 'OS'
|
||||||
required: true
|
required: true
|
||||||
default: 'ubuntu-22.04'
|
default: 'ubuntu-22.04'
|
||||||
type: choice
|
type: choice
|
||||||
options:
|
options:
|
||||||
- ubuntu-22.04
|
- ubuntu-22.04
|
||||||
- ubuntu-20.04
|
- ubuntu-20.04
|
||||||
workflow_call:
|
workflow_call:
|
||||||
inputs:
|
inputs:
|
||||||
cura_conan_version:
|
cura_conan_version:
|
||||||
description: 'Cura Conan Version'
|
description: 'Cura Conan Version'
|
||||||
default: 'cura/latest@ultimaker/testing'
|
default: 'cura/latest@ultimaker/testing'
|
||||||
required: true
|
required: true
|
||||||
type: string
|
type: string
|
||||||
conan_args:
|
conan_args:
|
||||||
description: 'Conan args: eq.: --require-override'
|
description: 'Conan args: eq.: --require-override'
|
||||||
default: ''
|
default: ''
|
||||||
required: false
|
required: false
|
||||||
type: string
|
type: string
|
||||||
enterprise:
|
enterprise:
|
||||||
description: 'Build Cura as an Enterprise edition'
|
description: 'Build Cura as an Enterprise edition'
|
||||||
default: false
|
default: false
|
||||||
required: true
|
required: true
|
||||||
type: boolean
|
type: boolean
|
||||||
staging:
|
staging:
|
||||||
description: 'Use staging API'
|
description: 'Use staging API'
|
||||||
default: false
|
default: false
|
||||||
required: true
|
required: true
|
||||||
type: boolean
|
type: boolean
|
||||||
architecture:
|
architecture:
|
||||||
description: 'Architecture'
|
description: 'Architecture'
|
||||||
required: true
|
required: true
|
||||||
default: 'X64'
|
default: 'X64'
|
||||||
type: string
|
type: string
|
||||||
operating_system:
|
operating_system:
|
||||||
description: 'OS'
|
description: 'OS'
|
||||||
required: true
|
required: true
|
||||||
default: 'ubuntu-22.04'
|
default: 'ubuntu-22.04'
|
||||||
type: string
|
type: string
|
||||||
|
|
||||||
env:
|
env:
|
||||||
CONAN_LOGIN_USERNAME_CURA: ${{ secrets.CONAN_USER }}
|
CONAN_LOGIN_USERNAME_CURA: ${{ secrets.CONAN_USER }}
|
||||||
CONAN_PASSWORD_CURA: ${{ secrets.CONAN_PASS }}
|
CONAN_PASSWORD_CURA: ${{ secrets.CONAN_PASS }}
|
||||||
GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }}
|
GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }}
|
||||||
CURA_CONAN_VERSION: ${{ inputs.cura_conan_version }}
|
CURA_CONAN_VERSION: ${{ inputs.cura_conan_version }}
|
||||||
ENTERPRISE: ${{ inputs.enterprise }}
|
ENTERPRISE: ${{ inputs.enterprise }}
|
||||||
STAGING: ${{ inputs.staging }}
|
STAGING: ${{ inputs.staging }}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
cura-installer-create:
|
cura-installer-create:
|
||||||
runs-on: ${{ inputs.operating_system }}
|
runs-on: ${{ inputs.operating_system }}
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: Setup Python and pip
|
- name: Setup Python and pip
|
||||||
uses: actions/setup-python@v4
|
uses: actions/setup-python@v4
|
||||||
with:
|
|
||||||
python-version: '3.10.x'
|
|
||||||
cache: 'pip'
|
|
||||||
cache-dependency-path: .github/workflows/requirements-conan-package.txt
|
|
||||||
|
|
||||||
- name: Install Python requirements for runner
|
|
||||||
run: pip install -r .github/workflows/requirements-conan-package.txt
|
|
||||||
|
|
||||||
- name: Cache Conan local repository packages (Bash)
|
|
||||||
uses: actions/cache@v3
|
|
||||||
with:
|
|
||||||
path: |
|
|
||||||
$HOME/.conan/data
|
|
||||||
$HOME/.conan/conan_download_cache
|
|
||||||
key: conan-${{ runner.os }}-${{ runner.arch }}-installer-cache
|
|
||||||
|
|
||||||
- name: Hack needed specifically for ubuntu-22.04 from mid-Feb 2023 onwards
|
|
||||||
if: ${{ startsWith(inputs.operating_system, 'ubuntu-22.04') }}
|
|
||||||
run: sudo apt remove libodbc2 libodbcinst2 unixodbc-common -y
|
|
||||||
|
|
||||||
# NOTE: Due to what are probably github issues, we have to remove the cache and reconfigure before the rest.
|
|
||||||
# This is maybe because grub caches the disk it uses last time, which is recreated each time.
|
|
||||||
- name: Install Linux system requirements
|
|
||||||
run: |
|
|
||||||
sudo rm /var/cache/debconf/config.dat
|
|
||||||
sudo dpkg --configure -a
|
|
||||||
sudo add-apt-repository ppa:ubuntu-toolchain-r/test -y
|
|
||||||
sudo apt update
|
|
||||||
sudo apt upgrade
|
|
||||||
sudo apt install build-essential checkinstall libegl-dev zlib1g-dev libssl-dev ninja-build autoconf libx11-dev libx11-xcb-dev libfontenc-dev libice-dev libsm-dev libxau-dev libxaw7-dev libxcomposite-dev libxcursor-dev libxdamage-dev libxdmcp-dev libxext-dev libxfixes-dev libxi-dev libxinerama-dev libxkbfile-dev libxmu-dev libxmuu-dev libxpm-dev libxrandr-dev libxrender-dev libxres-dev libxss-dev libxt-dev libxtst-dev libxv-dev libxvmc-dev libxxf86vm-dev xtrans-dev libxcb-render0-dev libxcb-render-util0-dev libxcb-xkb-dev libxcb-icccm4-dev libxcb-image0-dev libxcb-keysyms1-dev libxcb-randr0-dev libxcb-shape0-dev libxcb-sync-dev libxcb-xfixes0-dev libxcb-xinerama0-dev xkb-data libxcb-dri3-dev uuid-dev libxcb-util-dev libxkbcommon-x11-dev pkg-config -y
|
|
||||||
wget --no-check-certificate --quiet https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage -O $GITHUB_WORKSPACE/appimagetool
|
|
||||||
chmod +x $GITHUB_WORKSPACE/appimagetool
|
|
||||||
echo "APPIMAGETOOL_LOCATION=$GITHUB_WORKSPACE/appimagetool" >> $GITHUB_ENV
|
|
||||||
|
|
||||||
- name: Install GCC-12 on ubuntu-22.04
|
|
||||||
if: ${{ startsWith(inputs.operating_system, 'ubuntu-22.04') }}
|
|
||||||
run: |
|
|
||||||
sudo apt install g++-12 gcc-12 -y
|
|
||||||
sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-12 12
|
|
||||||
sudo update-alternatives --install /usr/bin/g++ g++ /usr/bin/g++-12 12
|
|
||||||
|
|
||||||
- name: Use GCC-10 on ubuntu-20.04
|
|
||||||
if: ${{ startsWith(inputs.operating_system, 'ubuntu-20.04') }}
|
|
||||||
run: |
|
|
||||||
sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-10 10
|
|
||||||
sudo update-alternatives --install /usr/bin/g++ g++ /usr/bin/g++-10 10
|
|
||||||
|
|
||||||
- name: Create the default Conan profile
|
|
||||||
run: conan profile new default --detect --force
|
|
||||||
|
|
||||||
- name: Configure GPG Key Linux (Bash)
|
|
||||||
run: echo -n "$GPG_PRIVATE_KEY" | base64 --decode | gpg --import
|
|
||||||
|
|
||||||
- name: Get Conan configuration
|
|
||||||
run: |
|
|
||||||
conan config install https://github.com/Ultimaker/conan-config.git
|
|
||||||
conan config install https://github.com/Ultimaker/conan-config.git -a "-b runner/${{ runner.os }}/${{ runner.arch }}"
|
|
||||||
|
|
||||||
- name: Use Conan download cache (Bash)
|
|
||||||
run: conan config set storage.download_cache="$HOME/.conan/conan_download_cache"
|
|
||||||
|
|
||||||
- name: Create the Packages (Bash)
|
|
||||||
run: conan install $CURA_CONAN_VERSION ${{ inputs.conan_args }} --build=missing --update -if cura_inst -g VirtualPythonEnv -o cura:enterprise=$ENTERPRISE -o cura:staging=$STAGING --json "cura_inst/conan_install_info.json"
|
|
||||||
|
|
||||||
- name: Upload the Package(s)
|
|
||||||
if: always()
|
|
||||||
run: |
|
|
||||||
conan upload "*" -r cura --all -c
|
|
||||||
|
|
||||||
- name: Set Environment variables for Cura (bash)
|
|
||||||
run: |
|
|
||||||
. ./cura_inst/bin/activate_github_actions_env.sh
|
|
||||||
. ./cura_inst/bin/activate_github_actions_version_env.sh
|
|
||||||
|
|
||||||
# FIXME: This is a workaround to ensure that we use and pack a shared library for OpenSSL 1.1.1l. We currently compile
|
|
||||||
# OpenSSL statically for CPython, but our Python Dependenies (such as PyQt6) require a shared library.
|
|
||||||
# Because Conan won't allow for building the same library with two different options (easily) we need to install it explicitly
|
|
||||||
# and do a manual copy to the VirtualEnv, such that Pyinstaller can find it.
|
|
||||||
|
|
||||||
- name: Install OpenSSL shared
|
|
||||||
run: conan install openssl/1.1.1l@_/_ --build=missing --update -o openssl:shared=True -g deploy
|
|
||||||
|
|
||||||
- name: Copy OpenSSL shared (Bash)
|
|
||||||
run: |
|
|
||||||
cp ./openssl/lib/*.so* ./cura_inst/bin/ || true
|
|
||||||
cp ./openssl/lib/*.dylib* ./cura_inst/bin/ || true
|
|
||||||
|
|
||||||
- name: Create the Cura dist
|
|
||||||
run: pyinstaller ./cura_inst/UltiMaker-Cura.spec
|
|
||||||
|
|
||||||
- name: Output the name file name and extension
|
|
||||||
id: filename
|
|
||||||
shell: python
|
|
||||||
run: |
|
|
||||||
import os
|
|
||||||
enterprise = "-Enterprise" if "${{ inputs.enterprise }}" == "true" else ""
|
|
||||||
if "${{ inputs.operating_system }}" == "ubuntu-22.04":
|
|
||||||
installer_filename = f"UltiMaker-Cura-{os.getenv('CURA_VERSION_FULL')}{enterprise}-linux-modern-${{ inputs.architecture }}"
|
|
||||||
else:
|
|
||||||
installer_filename = f"UltiMaker-Cura-{os.getenv('CURA_VERSION_FULL')}{enterprise}-linux-${{ inputs.architecture }}"
|
|
||||||
output_env = os.environ["GITHUB_OUTPUT"]
|
|
||||||
content = ""
|
|
||||||
if os.path.exists(output_env):
|
|
||||||
with open(output_env, "r") as f:
|
|
||||||
content = f.read()
|
|
||||||
with open(output_env, "w") as f:
|
|
||||||
f.write(content)
|
|
||||||
f.writelines(f"INSTALLER_FILENAME={installer_filename}\n")
|
|
||||||
|
|
||||||
- name: Summarize the used Conan dependencies
|
|
||||||
shell: python
|
|
||||||
run: |
|
|
||||||
import os
|
|
||||||
import json
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
conan_install_info_path = Path("cura_inst/conan_install_info.json")
|
|
||||||
conan_info = {"installed": []}
|
|
||||||
if os.path.exists(conan_install_info_path):
|
|
||||||
with open(conan_install_info_path, "r") as f:
|
|
||||||
conan_info = json.load(f)
|
|
||||||
sorted_deps = sorted([dep["recipe"]["id"].replace('#', r' rev: ') for dep in conan_info["installed"]])
|
|
||||||
|
|
||||||
summary_env = os.environ["GITHUB_STEP_SUMMARY"]
|
|
||||||
content = ""
|
|
||||||
if os.path.exists(summary_env):
|
|
||||||
with open(summary_env, "r") as f:
|
|
||||||
content = f.read()
|
|
||||||
|
|
||||||
with open(summary_env, "w") as f:
|
|
||||||
f.write(content)
|
|
||||||
f.writelines("# ${{ steps.filename.outputs.INSTALLER_FILENAME }}\n")
|
|
||||||
f.writelines("## Conan packages:\n")
|
|
||||||
for dep in sorted_deps:
|
|
||||||
f.writelines(f"`{dep}`\n")
|
|
||||||
|
|
||||||
- name: Summarize the used Python modules
|
|
||||||
shell: python
|
|
||||||
run: |
|
|
||||||
import os
|
|
||||||
import pkg_resources
|
|
||||||
summary_env = os.environ["GITHUB_STEP_SUMMARY"]
|
|
||||||
content = ""
|
|
||||||
if os.path.exists(summary_env):
|
|
||||||
with open(summary_env, "r") as f:
|
|
||||||
content = f.read()
|
|
||||||
|
|
||||||
with open(summary_env, "w") as f:
|
|
||||||
f.write(content)
|
|
||||||
f.writelines("## Python modules:\n")
|
|
||||||
for package in pkg_resources.working_set:
|
|
||||||
f.writelines(f"`{package.key}/{package.version}`\n")
|
|
||||||
|
|
||||||
- name: Create the Linux AppImage (Bash)
|
|
||||||
run: |
|
|
||||||
python ../cura_inst/packaging/AppImage/create_appimage.py ./UltiMaker-Cura $CURA_VERSION_FULL "${{ steps.filename.outputs.INSTALLER_FILENAME }}.AppImage"
|
|
||||||
chmod +x "${{ steps.filename.outputs.INSTALLER_FILENAME }}.AppImage"
|
|
||||||
working-directory: dist
|
|
||||||
|
|
||||||
- name: Upload the AppImage
|
|
||||||
uses: actions/upload-artifact@v3
|
|
||||||
with:
|
|
||||||
name: ${{ steps.filename.outputs.INSTALLER_FILENAME }}-AppImage
|
|
||||||
path: |
|
|
||||||
dist/${{ steps.filename.outputs.INSTALLER_FILENAME }}.AppImage
|
|
||||||
retention-days: 5
|
|
||||||
|
|
||||||
notify-export:
|
|
||||||
if: ${{ always() }}
|
|
||||||
needs: [ cura-installer-create ]
|
|
||||||
|
|
||||||
uses: ultimaker/cura/.github/workflows/notify.yml@main
|
|
||||||
with:
|
with:
|
||||||
success: ${{ contains(join(needs.*.result, ','), 'success') }}
|
python-version: '3.10.x'
|
||||||
success_title: "Create the Cura distributions"
|
cache: 'pip'
|
||||||
success_body: "Installers for ${{ inputs.cura_conan_version }}"
|
cache-dependency-path: .github/workflows/requirements-conan-package.txt
|
||||||
failure_title: "Failed to create the Cura distributions"
|
|
||||||
failure_body: "Failed to create at least 1 installer for ${{ inputs.cura_conan_version }}"
|
- name: Install Python requirements for runner
|
||||||
secrets: inherit
|
run: pip install -r .github/workflows/requirements-conan-package.txt
|
||||||
|
|
||||||
|
- name: Cache Conan local repository packages (Bash)
|
||||||
|
uses: actions/cache@v3
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
$HOME/.conan/data
|
||||||
|
$HOME/.conan/conan_download_cache
|
||||||
|
key: conan-${{ runner.os }}-${{ runner.arch }}-installer-cache
|
||||||
|
|
||||||
|
- name: Hack needed specifically for ubuntu-22.04 from mid-Feb 2023 onwards
|
||||||
|
if: ${{ startsWith(inputs.operating_system, 'ubuntu-22.04') }}
|
||||||
|
run: sudo apt remove libodbc2 libodbcinst2 unixodbc-common -y
|
||||||
|
|
||||||
|
# NOTE: Due to what are probably github issues, we have to remove the cache and reconfigure before the rest.
|
||||||
|
# This is maybe because grub caches the disk it uses last time, which is recreated each time.
|
||||||
|
- name: Install Linux system requirements
|
||||||
|
run: |
|
||||||
|
sudo rm /var/cache/debconf/config.dat
|
||||||
|
sudo dpkg --configure -a
|
||||||
|
sudo add-apt-repository ppa:ubuntu-toolchain-r/test -y
|
||||||
|
sudo apt update
|
||||||
|
sudo apt upgrade
|
||||||
|
sudo apt install build-essential checkinstall libegl-dev zlib1g-dev libssl-dev ninja-build autoconf libx11-dev libx11-xcb-dev libfontenc-dev libice-dev libsm-dev libxau-dev libxaw7-dev libxcomposite-dev libxcursor-dev libxdamage-dev libxdmcp-dev libxext-dev libxfixes-dev libxi-dev libxinerama-dev libxkbfile-dev libxmu-dev libxmuu-dev libxpm-dev libxrandr-dev libxrender-dev libxres-dev libxss-dev libxt-dev libxtst-dev libxv-dev libxvmc-dev libxxf86vm-dev xtrans-dev libxcb-render0-dev libxcb-render-util0-dev libxcb-xkb-dev libxcb-icccm4-dev libxcb-image0-dev libxcb-keysyms1-dev libxcb-randr0-dev libxcb-shape0-dev libxcb-sync-dev libxcb-xfixes0-dev libxcb-xinerama0-dev xkb-data libxcb-dri3-dev uuid-dev libxcb-util-dev libxkbcommon-x11-dev pkg-config -y
|
||||||
|
wget --no-check-certificate --quiet https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage -O $GITHUB_WORKSPACE/appimagetool
|
||||||
|
chmod +x $GITHUB_WORKSPACE/appimagetool
|
||||||
|
echo "APPIMAGETOOL_LOCATION=$GITHUB_WORKSPACE/appimagetool" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Install GCC-13
|
||||||
|
run: |
|
||||||
|
sudo apt install g++-13 gcc-13 -y
|
||||||
|
sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-13 13
|
||||||
|
sudo update-alternatives --install /usr/bin/g++ g++ /usr/bin/g++-13 13
|
||||||
|
|
||||||
|
- name: Create the default Conan profile
|
||||||
|
run: conan profile new default --detect --force
|
||||||
|
|
||||||
|
- name: Configure GPG Key Linux (Bash)
|
||||||
|
run: echo -n "$GPG_PRIVATE_KEY" | base64 --decode | gpg --import
|
||||||
|
|
||||||
|
- name: Get Conan configuration
|
||||||
|
run: |
|
||||||
|
conan config install https://github.com/Ultimaker/conan-config.git
|
||||||
|
conan config install https://github.com/Ultimaker/conan-config.git -a "-b runner/${{ runner.os }}/${{ runner.arch }}"
|
||||||
|
|
||||||
|
- name: Use Conan download cache (Bash)
|
||||||
|
run: conan config set storage.download_cache="$HOME/.conan/conan_download_cache"
|
||||||
|
|
||||||
|
- name: Create the Packages (Bash)
|
||||||
|
run: conan install $CURA_CONAN_VERSION ${{ inputs.conan_args }} --build=missing --update -if cura_inst -g VirtualPythonEnv -o cura:enterprise=$ENTERPRISE -o cura:staging=$STAGING --json "cura_inst/conan_install_info.json"
|
||||||
|
|
||||||
|
- name: Upload the Package(s)
|
||||||
|
if: always()
|
||||||
|
run: |
|
||||||
|
conan upload "*" -r cura --all -c
|
||||||
|
|
||||||
|
- name: Set Environment variables for Cura (bash)
|
||||||
|
run: |
|
||||||
|
. ./cura_inst/bin/activate_github_actions_env.sh
|
||||||
|
. ./cura_inst/bin/activate_github_actions_version_env.sh
|
||||||
|
|
||||||
|
# FIXME: This is a workaround to ensure that we use and pack a shared library for OpenSSL 1.1.1l. We currently compile
|
||||||
|
# OpenSSL statically for CPython, but our Python Dependenies (such as PyQt6) require a shared library.
|
||||||
|
# Because Conan won't allow for building the same library with two different options (easily) we need to install it explicitly
|
||||||
|
# and do a manual copy to the VirtualEnv, such that Pyinstaller can find it.
|
||||||
|
|
||||||
|
- name: Install OpenSSL shared
|
||||||
|
run: conan install openssl/1.1.1l@_/_ --build=missing --update -o openssl:shared=True -g deploy
|
||||||
|
|
||||||
|
- name: Copy OpenSSL shared (Bash)
|
||||||
|
run: |
|
||||||
|
cp ./openssl/lib/*.so* ./cura_inst/bin/ || true
|
||||||
|
cp ./openssl/lib/*.dylib* ./cura_inst/bin/ || true
|
||||||
|
|
||||||
|
- name: Create the Cura dist
|
||||||
|
run: pyinstaller ./cura_inst/UltiMaker-Cura.spec
|
||||||
|
|
||||||
|
- name: Output the name file name and extension
|
||||||
|
id: filename
|
||||||
|
shell: python
|
||||||
|
run: |
|
||||||
|
import os
|
||||||
|
enterprise = "-Enterprise" if "${{ inputs.enterprise }}" == "true" else ""
|
||||||
|
if "${{ inputs.operating_system }}" == "ubuntu-22.04":
|
||||||
|
installer_filename = f"UltiMaker-Cura-{os.getenv('CURA_VERSION_FULL')}{enterprise}-linux-modern-${{ inputs.architecture }}"
|
||||||
|
else:
|
||||||
|
installer_filename = f"UltiMaker-Cura-{os.getenv('CURA_VERSION_FULL')}{enterprise}-linux-${{ inputs.architecture }}"
|
||||||
|
output_env = os.environ["GITHUB_OUTPUT"]
|
||||||
|
content = ""
|
||||||
|
if os.path.exists(output_env):
|
||||||
|
with open(output_env, "r") as f:
|
||||||
|
content = f.read()
|
||||||
|
with open(output_env, "w") as f:
|
||||||
|
f.write(content)
|
||||||
|
f.writelines(f"INSTALLER_FILENAME={installer_filename}\n")
|
||||||
|
|
||||||
|
- name: Summarize the used Conan dependencies
|
||||||
|
shell: python
|
||||||
|
run: |
|
||||||
|
import os
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
conan_install_info_path = Path("cura_inst/conan_install_info.json")
|
||||||
|
conan_info = {"installed": []}
|
||||||
|
if os.path.exists(conan_install_info_path):
|
||||||
|
with open(conan_install_info_path, "r") as f:
|
||||||
|
conan_info = json.load(f)
|
||||||
|
sorted_deps = sorted([dep["recipe"]["id"].replace('#', r' rev: ') for dep in conan_info["installed"]])
|
||||||
|
|
||||||
|
summary_env = os.environ["GITHUB_STEP_SUMMARY"]
|
||||||
|
content = ""
|
||||||
|
if os.path.exists(summary_env):
|
||||||
|
with open(summary_env, "r") as f:
|
||||||
|
content = f.read()
|
||||||
|
|
||||||
|
with open(summary_env, "w") as f:
|
||||||
|
f.write(content)
|
||||||
|
f.writelines("# ${{ steps.filename.outputs.INSTALLER_FILENAME }}\n")
|
||||||
|
f.writelines("## Conan packages:\n")
|
||||||
|
for dep in sorted_deps:
|
||||||
|
f.writelines(f"`{dep}`\n")
|
||||||
|
|
||||||
|
- name: Summarize the used Python modules
|
||||||
|
shell: python
|
||||||
|
run: |
|
||||||
|
import os
|
||||||
|
import pkg_resources
|
||||||
|
summary_env = os.environ["GITHUB_STEP_SUMMARY"]
|
||||||
|
content = ""
|
||||||
|
if os.path.exists(summary_env):
|
||||||
|
with open(summary_env, "r") as f:
|
||||||
|
content = f.read()
|
||||||
|
|
||||||
|
with open(summary_env, "w") as f:
|
||||||
|
f.write(content)
|
||||||
|
f.writelines("## Python modules:\n")
|
||||||
|
for package in pkg_resources.working_set:
|
||||||
|
f.writelines(f"`{package.key}/{package.version}`\n")
|
||||||
|
|
||||||
|
- name: Create the Linux AppImage (Bash)
|
||||||
|
run: |
|
||||||
|
python ../cura_inst/packaging/AppImage/create_appimage.py ./UltiMaker-Cura $CURA_VERSION_FULL "${{ steps.filename.outputs.INSTALLER_FILENAME }}.AppImage"
|
||||||
|
chmod +x "${{ steps.filename.outputs.INSTALLER_FILENAME }}.AppImage"
|
||||||
|
working-directory: dist
|
||||||
|
|
||||||
|
- name: Upload the AppImage
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: ${{ steps.filename.outputs.INSTALLER_FILENAME }}-AppImage
|
||||||
|
path: |
|
||||||
|
dist/${{ steps.filename.outputs.INSTALLER_FILENAME }}.AppImage
|
||||||
|
retention-days: 5
|
||||||
|
|
||||||
|
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
|
sip
|
||||||
|
278
.github/workflows/unit-test.yml
vendored
278
.github/workflows/unit-test.yml
vendored
@ -2,163 +2,165 @@
|
|||||||
name: unit-test
|
name: unit-test
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
paths:
|
paths:
|
||||||
- 'plugins/**'
|
- 'plugins/**'
|
||||||
- 'resources/**'
|
- 'resources/**'
|
||||||
- 'cura/**'
|
- 'cura/**'
|
||||||
- 'icons/**'
|
- 'icons/**'
|
||||||
- 'tests/**'
|
- 'tests/**'
|
||||||
- 'packaging/**'
|
- 'packaging/**'
|
||||||
- '.github/workflows/conan-*.yml'
|
- '.github/workflows/conan-*.yml'
|
||||||
- '.github/workflows/unit-test.yml'
|
- '.github/workflows/unit-test.yml'
|
||||||
- '.github/workflows/notify.yml'
|
- '.github/workflows/notify.yml'
|
||||||
- '.github/workflows/requirements-conan-package.txt'
|
- '.github/workflows/requirements-conan-package.txt'
|
||||||
- 'requirements*.txt'
|
- 'requirements*.txt'
|
||||||
- 'conanfile.py'
|
- 'conanfile.py'
|
||||||
- 'conandata.yml'
|
- 'conandata.yml'
|
||||||
- 'GitVersion.yml'
|
- 'GitVersion.yml'
|
||||||
- '*.jinja'
|
- '*.jinja'
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
- 'CURA-*'
|
- 'CURA-*'
|
||||||
- '[1-9]+.[0-9]+'
|
- '[1-9]+.[0-9]+'
|
||||||
tags:
|
tags:
|
||||||
- '[0-9]+.[0-9]+.[0-9]+'
|
- '[0-9]+.[0-9]+.[0-9]+'
|
||||||
- '[0-9]+.[0-9]+-beta'
|
- '[0-9]+.[0-9]+-beta'
|
||||||
pull_request:
|
pull_request:
|
||||||
paths:
|
paths:
|
||||||
- 'plugins/**'
|
- 'plugins/**'
|
||||||
- 'resources/**'
|
- 'resources/**'
|
||||||
- 'cura/**'
|
- 'cura/**'
|
||||||
- 'icons/**'
|
- 'icons/**'
|
||||||
- 'tests/**'
|
- 'tests/**'
|
||||||
- 'packaging/**'
|
- 'packaging/**'
|
||||||
- '.github/workflows/conan-*.yml'
|
- '.github/workflows/conan-*.yml'
|
||||||
- '.github/workflows/unit-test.yml'
|
- '.github/workflows/unit-test.yml'
|
||||||
- '.github/workflows/notify.yml'
|
- '.github/workflows/notify.yml'
|
||||||
- '.github/workflows/requirements-conan-package.txt'
|
- '.github/workflows/requirements-conan-package.txt'
|
||||||
- 'requirements*.txt'
|
- 'requirements*.txt'
|
||||||
- 'conanfile.py'
|
- 'conanfile.py'
|
||||||
- 'conandata.yml'
|
- 'conandata.yml'
|
||||||
- 'GitVersion.yml'
|
- 'GitVersion.yml'
|
||||||
- '*.jinja'
|
- '*.jinja'
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
- '[1-9]+.[0-9]+'
|
- '[1-9]+.[0-9]+'
|
||||||
tags:
|
tags:
|
||||||
- '[0-9]+.[0-9]+.[0-9]+'
|
- '[0-9]+.[0-9]+.[0-9]+'
|
||||||
- '[0-9]+.[0-9]+-beta'
|
- '[0-9]+.[0-9]+-beta'
|
||||||
|
|
||||||
env:
|
env:
|
||||||
CONAN_LOGIN_USERNAME_CURA: ${{ secrets.CONAN_USER }}
|
CONAN_LOGIN_USERNAME_CURA: ${{ secrets.CONAN_USER }}
|
||||||
CONAN_PASSWORD_CURA: ${{ secrets.CONAN_PASS }}
|
CONAN_PASSWORD_CURA: ${{ secrets.CONAN_PASS }}
|
||||||
CONAN_LOGIN_USERNAME_CURA_CE: ${{ secrets.CONAN_USER }}
|
CONAN_LOGIN_USERNAME_CURA_CE: ${{ secrets.CONAN_USER }}
|
||||||
CONAN_PASSWORD_CURA_CE: ${{ secrets.CONAN_PASS }}
|
CONAN_PASSWORD_CURA_CE: ${{ secrets.CONAN_PASS }}
|
||||||
CONAN_LOG_RUN_TO_OUTPUT: 1
|
CONAN_LOG_RUN_TO_OUTPUT: 1
|
||||||
CONAN_LOGGING_LEVEL: info
|
CONAN_LOGGING_LEVEL: info
|
||||||
CONAN_NON_INTERACTIVE: 1
|
CONAN_NON_INTERACTIVE: 1
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
conan-recipe-version:
|
conan-recipe-version:
|
||||||
uses: ultimaker/cura/.github/workflows/conan-recipe-version.yml@main
|
uses: ultimaker/cura/.github/workflows/conan-recipe-version.yml@main
|
||||||
|
with:
|
||||||
|
project_name: cura
|
||||||
|
|
||||||
|
testing:
|
||||||
|
runs-on: ubuntu-22.04
|
||||||
|
needs: [ conan-recipe-version ]
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v3
|
||||||
with:
|
with:
|
||||||
project_name: cura
|
fetch-depth: 2
|
||||||
|
|
||||||
testing:
|
- name: Setup Python and pip
|
||||||
runs-on: ubuntu-22.04
|
uses: actions/setup-python@v4
|
||||||
needs: [ conan-recipe-version ]
|
with:
|
||||||
|
python-version: '3.11.x'
|
||||||
|
architecture: 'x64'
|
||||||
|
cache: 'pip'
|
||||||
|
cache-dependency-path: .github/workflows/requirements-conan-package.txt
|
||||||
|
|
||||||
steps:
|
- name: Install Python requirements and Create default Conan profile
|
||||||
- name: Checkout
|
run: pip install -r requirements-conan-package.txt
|
||||||
uses: actions/checkout@v3
|
working-directory: .github/workflows/
|
||||||
with:
|
|
||||||
fetch-depth: 2
|
|
||||||
|
|
||||||
- name: Setup Python and pip
|
- name: Use Conan download cache (Bash)
|
||||||
uses: actions/setup-python@v4
|
if: ${{ runner.os != 'Windows' }}
|
||||||
with:
|
run: conan config set storage.download_cache="$HOME/.conan/conan_download_cache"
|
||||||
python-version: '3.11.x'
|
|
||||||
architecture: 'x64'
|
|
||||||
cache: 'pip'
|
|
||||||
cache-dependency-path: .github/workflows/requirements-conan-package.txt
|
|
||||||
|
|
||||||
- name: Install Python requirements and Create default Conan profile
|
- name: Cache Conan local repository packages (Bash)
|
||||||
run: pip install -r requirements-conan-package.txt
|
uses: actions/cache@v3
|
||||||
working-directory: .github/workflows/
|
if: ${{ runner.os != 'Windows' }}
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
$HOME/.conan/data
|
||||||
|
$HOME/.conan/conan_download_cache
|
||||||
|
key: conan-${{ runner.os }}-${{ runner.arch }}-unit-cache
|
||||||
|
|
||||||
- name: Use Conan download cache (Bash)
|
# NOTE: Due to what are probably github issues, we have to remove the cache and reconfigure before the rest.
|
||||||
if: ${{ runner.os != 'Windows' }}
|
# This is maybe because grub caches the disk it uses last time, which is recreated each time.
|
||||||
run: conan config set storage.download_cache="$HOME/.conan/conan_download_cache"
|
- name: Install Linux system requirements
|
||||||
|
if: ${{ runner.os == 'Linux' }}
|
||||||
|
run: |
|
||||||
|
sudo rm /var/cache/debconf/config.dat
|
||||||
|
sudo dpkg --configure -a
|
||||||
|
sudo add-apt-repository ppa:ubuntu-toolchain-r/test -y
|
||||||
|
sudo apt update
|
||||||
|
sudo apt upgrade
|
||||||
|
sudo apt install build-essential checkinstall libegl-dev zlib1g-dev libssl-dev ninja-build autoconf libx11-dev libx11-xcb-dev libfontenc-dev libice-dev libsm-dev libxau-dev libxaw7-dev libxcomposite-dev libxcursor-dev libxdamage-dev libxdmcp-dev libxext-dev libxfixes-dev libxi-dev libxinerama-dev libxkbfile-dev libxmu-dev libxmuu-dev libxpm-dev libxrandr-dev libxrender-dev libxres-dev libxss-dev libxt-dev libxtst-dev libxv-dev libxvmc-dev libxxf86vm-dev xtrans-dev libxcb-render0-dev libxcb-render-util0-dev libxcb-xkb-dev libxcb-icccm4-dev libxcb-image0-dev libxcb-keysyms1-dev libxcb-randr0-dev libxcb-shape0-dev libxcb-sync-dev libxcb-xfixes0-dev libxcb-xinerama0-dev xkb-data libxcb-dri3-dev uuid-dev libxcb-util-dev libxkbcommon-x11-dev pkg-config -y
|
||||||
|
|
||||||
- name: Cache Conan local repository packages (Bash)
|
- name: Install GCC-13
|
||||||
uses: actions/cache@v3
|
run: |
|
||||||
if: ${{ runner.os != 'Windows' }}
|
sudo apt install g++-13 gcc-13 -y
|
||||||
with:
|
sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-13 13
|
||||||
path: |
|
sudo update-alternatives --install /usr/bin/g++ g++ /usr/bin/g++-13 13
|
||||||
$HOME/.conan/data
|
|
||||||
$HOME/.conan/conan_download_cache
|
|
||||||
key: conan-${{ runner.os }}-${{ runner.arch }}-unit-cache
|
|
||||||
|
|
||||||
# NOTE: Due to what are probably github issues, we have to remove the cache and reconfigure before the rest.
|
- name: Get Conan configuration
|
||||||
# This is maybe because grub caches the disk it uses last time, which is recreated each time.
|
run: |
|
||||||
- name: Install Linux system requirements
|
conan config install https://github.com/Ultimaker/conan-config.git
|
||||||
if: ${{ runner.os == 'Linux' }}
|
conan config install https://github.com/Ultimaker/conan-config.git -a "-b runner/${{ runner.os }}/${{ runner.arch }}"
|
||||||
run: |
|
|
||||||
sudo rm /var/cache/debconf/config.dat
|
|
||||||
sudo dpkg --configure -a
|
|
||||||
sudo add-apt-repository ppa:ubuntu-toolchain-r/test -y
|
|
||||||
sudo apt update
|
|
||||||
sudo apt upgrade
|
|
||||||
sudo apt install build-essential checkinstall libegl-dev zlib1g-dev libssl-dev ninja-build autoconf libx11-dev libx11-xcb-dev libfontenc-dev libice-dev libsm-dev libxau-dev libxaw7-dev libxcomposite-dev libxcursor-dev libxdamage-dev libxdmcp-dev libxext-dev libxfixes-dev libxi-dev libxinerama-dev libxkbfile-dev libxmu-dev libxmuu-dev libxpm-dev libxrandr-dev libxrender-dev libxres-dev libxss-dev libxt-dev libxtst-dev libxv-dev libxvmc-dev libxxf86vm-dev xtrans-dev libxcb-render0-dev libxcb-render-util0-dev libxcb-xkb-dev libxcb-icccm4-dev libxcb-image0-dev libxcb-keysyms1-dev libxcb-randr0-dev libxcb-shape0-dev libxcb-sync-dev libxcb-xfixes0-dev libxcb-xinerama0-dev xkb-data libxcb-dri3-dev uuid-dev libxcb-util-dev libxkbcommon-x11-dev pkg-config -y
|
|
||||||
|
|
||||||
- name: Install GCC-12 on ubuntu-22.04
|
- name: Get Conan profile
|
||||||
run: |
|
run: conan profile new default --detect --force
|
||||||
sudo apt install g++-12 gcc-12 -y
|
|
||||||
sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-12 12
|
|
||||||
sudo update-alternatives --install /usr/bin/g++ g++ /usr/bin/g++-12 12
|
|
||||||
|
|
||||||
- name: Get Conan configuration
|
|
||||||
run: conan config install https://github.com/Ultimaker/conan-config.git
|
|
||||||
|
|
||||||
- name: Get Conan profile
|
- name: Install dependencies
|
||||||
run: conan profile new default --detect --force
|
run: conan install . ${{ needs.conan-recipe-version.outputs.recipe_id_full }} --build=missing --update -o cura:devtools=True -g VirtualPythonEnv -if venv
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Upload the Dependency package(s)
|
||||||
run: conan install . ${{ needs.conan-recipe-version.outputs.recipe_id_full }} --build=missing --update -o cura:devtools=True -g VirtualPythonEnv -if venv
|
run: conan upload "*" -r cura --all -c
|
||||||
|
|
||||||
- name: Upload the Dependency package(s)
|
- name: Set Environment variables for Cura (bash)
|
||||||
run: conan upload "*" -r cura --all -c
|
if: ${{ runner.os != 'Windows' }}
|
||||||
|
run: |
|
||||||
|
. ./venv/bin/activate_github_actions_env.sh
|
||||||
|
|
||||||
- name: Set Environment variables for Cura (bash)
|
- name: Run Unit Test
|
||||||
if: ${{ runner.os != 'Windows' }}
|
id: run-test
|
||||||
run: |
|
run: |
|
||||||
. ./venv/bin/activate_github_actions_env.sh
|
pytest --junitxml=junit_cura.xml
|
||||||
|
working-directory: tests
|
||||||
|
|
||||||
- name: Run Unit Test
|
- name: Save PR metadata
|
||||||
id: run-test
|
if: always()
|
||||||
run: |
|
run: |
|
||||||
pytest --junitxml=junit_cura.xml
|
echo ${{ github.event.number }} > pr-id.txt
|
||||||
working-directory: tests
|
echo ${{ github.event.pull_request.head.repo.full_name }} > pr-head-repo.txt
|
||||||
|
echo ${{ github.event.pull_request.head.ref }} > pr-head-ref.txt
|
||||||
|
working-directory: tests
|
||||||
|
|
||||||
- name: Save PR metadata
|
- name: Upload Test Results
|
||||||
if: always()
|
if: always()
|
||||||
run: |
|
uses: actions/upload-artifact@v3
|
||||||
echo ${{ github.event.number }} > pr-id.txt
|
with:
|
||||||
echo ${{ github.event.pull_request.head.repo.full_name }} > pr-head-repo.txt
|
name: test-result
|
||||||
echo ${{ github.event.pull_request.head.ref }} > pr-head-ref.txt
|
path: |
|
||||||
working-directory: tests
|
tests/**/*.xml
|
||||||
|
tests/pr-id.txt
|
||||||
- name: Upload Test Results
|
tests/pr-head-repo.txt
|
||||||
if: always()
|
tests/pr-head-ref.txt
|
||||||
uses: actions/upload-artifact@v3
|
|
||||||
with:
|
|
||||||
name: test-result
|
|
||||||
path: |
|
|
||||||
tests/**/*.xml
|
|
||||||
tests/pr-id.txt
|
|
||||||
tests/pr-head-repo.txt
|
|
||||||
tests/pr-head-ref.txt
|
|
||||||
|
141
.github/workflows/update-translation.yml
vendored
141
.github/workflows/update-translation.yml
vendored
@ -1,82 +1,87 @@
|
|||||||
name: update-translations
|
name: update-translations
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
paths:
|
paths:
|
||||||
- 'plugins/**'
|
- 'plugins/**'
|
||||||
- 'resources/**'
|
- 'resources/**'
|
||||||
- 'cura/**'
|
- 'cura/**'
|
||||||
- 'icons/**'
|
- 'icons/**'
|
||||||
- 'tests/**'
|
- 'tests/**'
|
||||||
- 'packaging/**'
|
- 'packaging/**'
|
||||||
- '.github/workflows/conan-*.yml'
|
- '.github/workflows/conan-*.yml'
|
||||||
- '.github/workflows/notify.yml'
|
- '.github/workflows/notify.yml'
|
||||||
- '.github/workflows/requirements-conan-package.txt'
|
- '.github/workflows/requirements-conan-package.txt'
|
||||||
- 'requirements*.txt'
|
- 'requirements*.txt'
|
||||||
- 'conanfile.py'
|
- 'conanfile.py'
|
||||||
- 'conandata.yml'
|
- 'conandata.yml'
|
||||||
- 'GitVersion.yml'
|
- 'GitVersion.yml'
|
||||||
- '*.jinja'
|
- '*.jinja'
|
||||||
branches:
|
branches:
|
||||||
- '[1-9].[0-9]'
|
- '[1-9].[0-9]'
|
||||||
- '[1-9].[0-9][0-9]'
|
- '[1-9].[0-9][0-9]'
|
||||||
tags:
|
tags:
|
||||||
- '[1-9].[0-9].[0-9]*'
|
- '[1-9].[0-9].[0-9]*'
|
||||||
- '[1-9].[0-9].[0-9]'
|
- '[1-9].[0-9].[0-9]'
|
||||||
- '[1-9].[0-9][0-9].[0-9]*'
|
- '[1-9].[0-9][0-9].[0-9]*'
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
update-translations:
|
update-translations:
|
||||||
name: Update translations
|
name: Update translations
|
||||||
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: Cache Conan data
|
- name: Cache Conan data
|
||||||
id: cache-conan
|
id: cache-conan
|
||||||
uses: actions/cache@v3
|
uses: actions/cache@v3
|
||||||
with:
|
with:
|
||||||
path: ~/.conan
|
path: ~/.conan
|
||||||
key: ${{ runner.os }}-conan
|
key: ${{ runner.os }}-conan
|
||||||
|
|
||||||
- name: Setup Python and pip
|
- name: Setup Python and pip
|
||||||
uses: actions/setup-python@v4
|
uses: actions/setup-python@v4
|
||||||
with:
|
with:
|
||||||
python-version: 3.10.x
|
python-version: 3.11.x
|
||||||
cache: pip
|
cache: pip
|
||||||
cache-dependency-path: .github/workflows/requirements-conan-package.txt
|
cache-dependency-path: .github/workflows/requirements-conan-package.txt
|
||||||
|
|
||||||
- name: Install Python requirements for runner
|
- name: Install Python requirements for runner
|
||||||
run: pip install -r .github/workflows/requirements-conan-package.txt
|
run: pip install -r .github/workflows/requirements-conan-package.txt
|
||||||
|
|
||||||
# NOTE: Due to what are probably github issues, we have to remove the cache and reconfigure before the rest.
|
# NOTE: Due to what are probably github issues, we have to remove the cache and reconfigure before the rest.
|
||||||
# This is maybe because grub caches the disk it uses last time, which is recreated each time.
|
# This is maybe because grub caches the disk it uses last time, which is recreated each time.
|
||||||
- name: Install Linux system requirements
|
- name: Install Linux system requirements
|
||||||
if: ${{ runner.os == 'Linux' }}
|
if: ${{ runner.os == 'Linux' }}
|
||||||
run: |
|
run: |
|
||||||
sudo rm /var/cache/debconf/config.dat
|
sudo rm /var/cache/debconf/config.dat
|
||||||
sudo dpkg --configure -a
|
sudo dpkg --configure -a
|
||||||
sudo add-apt-repository ppa:ubuntu-toolchain-r/test -y
|
sudo add-apt-repository ppa:ubuntu-toolchain-r/test -y
|
||||||
sudo apt update
|
sudo apt update
|
||||||
sudo apt upgrade
|
sudo apt upgrade
|
||||||
sudo apt install efibootmgr build-essential checkinstall libegl-dev zlib1g-dev libssl-dev ninja-build autoconf libx11-dev libx11-xcb-dev libfontenc-dev libice-dev libsm-dev libxau-dev libxaw7-dev libxcomposite-dev libxcursor-dev libxdamage-dev libxdmcp-dev libxext-dev libxfixes-dev libxi-dev libxinerama-dev libxkbfile-dev libxmu-dev libxmuu-dev libxpm-dev libxrandr-dev libxrender-dev libxres-dev libxss-dev libxt-dev libxtst-dev libxv-dev libxvmc-dev libxxf86vm-dev xtrans-dev libxcb-render0-dev libxcb-render-util0-dev libxcb-xkb-dev libxcb-icccm4-dev libxcb-image0-dev libxcb-keysyms1-dev libxcb-randr0-dev libxcb-shape0-dev libxcb-sync-dev libxcb-xfixes0-dev libxcb-xinerama0-dev xkb-data libxcb-dri3-dev uuid-dev libxcb-util-dev libxkbcommon-x11-dev pkg-config flex bison g++-12 gcc-12 -y
|
sudo apt install efibootmgr build-essential checkinstall libegl-dev zlib1g-dev libssl-dev ninja-build autoconf libx11-dev libx11-xcb-dev libfontenc-dev libice-dev libsm-dev libxau-dev libxaw7-dev libxcomposite-dev libxcursor-dev libxdamage-dev libxdmcp-dev libxext-dev libxfixes-dev libxi-dev libxinerama-dev libxkbfile-dev libxmu-dev libxmuu-dev libxpm-dev libxrandr-dev libxrender-dev libxres-dev libxss-dev libxt-dev libxtst-dev libxv-dev libxvmc-dev libxxf86vm-dev xtrans-dev libxcb-render0-dev libxcb-render-util0-dev libxcb-xkb-dev libxcb-icccm4-dev libxcb-image0-dev libxcb-keysyms1-dev libxcb-randr0-dev libxcb-shape0-dev libxcb-sync-dev libxcb-xfixes0-dev libxcb-xinerama0-dev xkb-data libxcb-dri3-dev uuid-dev libxcb-util-dev libxkbcommon-x11-dev pkg-config flex bison g++-12 gcc-12 -y
|
||||||
sudo apt install g++-12 gcc-12 -y
|
|
||||||
sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-12 12
|
|
||||||
sudo update-alternatives --install /usr/bin/g++ g++ /usr/bin/g++-12 12
|
|
||||||
|
|
||||||
- name: Create the default Conan profile
|
- name: Install GCC-13
|
||||||
run: conan profile new default --detect --force
|
run: |
|
||||||
|
sudo apt install g++-13 gcc-13 -y
|
||||||
|
sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-13 13
|
||||||
|
sudo update-alternatives --install /usr/bin/g++ g++ /usr/bin/g++-13 13
|
||||||
|
|
||||||
- name: Get Conan configuration
|
- name: Create the default Conan profile
|
||||||
run: conan config install https://github.com/Ultimaker/conan-config.git
|
run: conan profile new default --detect --force
|
||||||
|
|
||||||
- name: generate the files using Conan install
|
- name: Get Conan configuration
|
||||||
run: conan install . --build=missing --update -o cura:devtools=True
|
run: |
|
||||||
|
conan config install https://github.com/Ultimaker/conan-config.git
|
||||||
|
conan config install https://github.com/Ultimaker/conan-config.git -a "-b runner/${{ runner.os }}/${{ runner.arch }}"
|
||||||
|
|
||||||
- uses: stefanzweifel/git-auto-commit-action@v4
|
- name: generate the files using Conan install
|
||||||
with:
|
run: conan install . --build=missing --update -o cura:devtools=True
|
||||||
file_pattern: resources/i18n/*.po resources/i18n/*.pot
|
|
||||||
status_options: --untracked-files=no
|
- uses: stefanzweifel/git-auto-commit-action@v4
|
||||||
commit_message: update translations
|
with:
|
||||||
|
file_pattern: resources/i18n/*.po resources/i18n/*.pot
|
||||||
|
status_options: --untracked-files=no
|
||||||
|
commit_message: update translations
|
||||||
|
@ -8,17 +8,20 @@ from UM.Logger import Logger
|
|||||||
from UM.Message import Message
|
from UM.Message import Message
|
||||||
from UM.Scene.SceneNode import SceneNode
|
from UM.Scene.SceneNode import SceneNode
|
||||||
from UM.i18n import i18nCatalog
|
from UM.i18n import i18nCatalog
|
||||||
from cura.Arranging.Nest2DArrange import arrange
|
from cura.Arranging.GridArrange import GridArrange
|
||||||
|
from cura.Arranging.Nest2DArrange import Nest2DArrange
|
||||||
|
|
||||||
i18n_catalog = i18nCatalog("cura")
|
i18n_catalog = i18nCatalog("cura")
|
||||||
|
|
||||||
|
|
||||||
class ArrangeObjectsJob(Job):
|
class ArrangeObjectsJob(Job):
|
||||||
def __init__(self, nodes: List[SceneNode], fixed_nodes: List[SceneNode], min_offset = 8) -> None:
|
def __init__(self, nodes: List[SceneNode], fixed_nodes: List[SceneNode], min_offset = 8,
|
||||||
|
*, grid_arrange: bool = False) -> None:
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self._nodes = nodes
|
self._nodes = nodes
|
||||||
self._fixed_nodes = fixed_nodes
|
self._fixed_nodes = fixed_nodes
|
||||||
self._min_offset = min_offset
|
self._min_offset = min_offset
|
||||||
|
self._grid_arrange = grid_arrange
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
found_solution_for_all = False
|
found_solution_for_all = False
|
||||||
@ -29,10 +32,18 @@ class ArrangeObjectsJob(Job):
|
|||||||
title = i18n_catalog.i18nc("@info:title", "Finding Location"))
|
title = i18n_catalog.i18nc("@info:title", "Finding Location"))
|
||||||
status_message.show()
|
status_message.show()
|
||||||
|
|
||||||
|
if self._grid_arrange:
|
||||||
|
arranger = GridArrange(self._nodes, Application.getInstance().getBuildVolume(), self._fixed_nodes)
|
||||||
|
else:
|
||||||
|
arranger = Nest2DArrange(self._nodes, Application.getInstance().getBuildVolume(), self._fixed_nodes,
|
||||||
|
factor=1000)
|
||||||
|
|
||||||
|
found_solution_for_all = False
|
||||||
try:
|
try:
|
||||||
found_solution_for_all = arrange(self._nodes, Application.getInstance().getBuildVolume(), self._fixed_nodes)
|
found_solution_for_all = arranger.arrange()
|
||||||
except: # If the thread crashes, the message should still close
|
except: # If the thread crashes, the message should still close
|
||||||
Logger.logException("e", "Unable to arrange the objects on the buildplate. The arrange algorithm has crashed.")
|
Logger.logException("e",
|
||||||
|
"Unable to arrange the objects on the buildplate. The arrange algorithm has crashed.")
|
||||||
|
|
||||||
status_message.hide()
|
status_message.hide()
|
||||||
|
|
||||||
|
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.GroupedOperation import GroupedOperation
|
||||||
from UM.Operations.RotateOperation import RotateOperation
|
from UM.Operations.RotateOperation import RotateOperation
|
||||||
from UM.Operations.TranslateOperation import TranslateOperation
|
from UM.Operations.TranslateOperation import TranslateOperation
|
||||||
|
from cura.Arranging.Arranger import Arranger
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from UM.Scene.SceneNode import SceneNode
|
from UM.Scene.SceneNode import SceneNode
|
||||||
from cura.BuildVolume import BuildVolume
|
from cura.BuildVolume import BuildVolume
|
||||||
|
|
||||||
|
|
||||||
def findNodePlacement(nodes_to_arrange: List["SceneNode"], build_volume: "BuildVolume", fixed_nodes: Optional[List["SceneNode"]] = None, factor = 10000) -> Tuple[bool, List[Item]]:
|
class Nest2DArrange(Arranger):
|
||||||
"""
|
def __init__(self,
|
||||||
Find placement for a set of scene nodes, but don't actually move them just yet.
|
nodes_to_arrange: List["SceneNode"],
|
||||||
:param nodes_to_arrange: The list of nodes that need to be moved.
|
build_volume: "BuildVolume",
|
||||||
:param build_volume: The build volume that we want to place the nodes in. It gets size & disallowed areas from this.
|
fixed_nodes: Optional[List["SceneNode"]] = None,
|
||||||
:param fixed_nodes: List of nods that should not be moved, but should be used when deciding where the others nodes
|
*,
|
||||||
are placed.
|
factor: int = 10000,
|
||||||
:param factor: The library that we use is int based. This factor defines how accurate we want it to be.
|
lock_rotation: bool = False):
|
||||||
|
"""
|
||||||
|
:param nodes_to_arrange: The list of nodes that need to be moved.
|
||||||
|
:param build_volume: The build volume that we want to place the nodes in. It gets size & disallowed areas from this.
|
||||||
|
:param fixed_nodes: List of nods that should not be moved, but should be used when deciding where the others nodes
|
||||||
|
are placed.
|
||||||
|
:param factor: The library that we use is int based. This factor defines how accuracte we want it to be.
|
||||||
|
:param lock_rotation: If set to true the orientation of the object will remain the same
|
||||||
|
"""
|
||||||
|
super().__init__()
|
||||||
|
self._nodes_to_arrange = nodes_to_arrange
|
||||||
|
self._build_volume = build_volume
|
||||||
|
self._fixed_nodes = fixed_nodes
|
||||||
|
self._factor = factor
|
||||||
|
self._lock_rotation = lock_rotation
|
||||||
|
|
||||||
:return: tuple (found_solution_for_all, node_items)
|
def findNodePlacement(self) -> Tuple[bool, List[Item]]:
|
||||||
WHERE
|
spacing = int(1.5 * self._factor) # 1.5mm spacing.
|
||||||
found_solution_for_all: Whether the algorithm found a place on the buildplate for all the objects
|
|
||||||
node_items: A list of the nodes return by libnest2d, which contain the new positions on the buildplate
|
|
||||||
"""
|
|
||||||
spacing = int(1.5 * factor) # 1.5mm spacing.
|
|
||||||
|
|
||||||
machine_width = build_volume.getWidth()
|
machine_width = self._build_volume.getWidth()
|
||||||
machine_depth = build_volume.getDepth()
|
machine_depth = self._build_volume.getDepth()
|
||||||
build_plate_bounding_box = Box(int(machine_width * factor), int(machine_depth * factor))
|
build_plate_bounding_box = Box(int(machine_width * self._factor), int(machine_depth * self._factor))
|
||||||
|
|
||||||
if fixed_nodes is None:
|
if self._fixed_nodes is None:
|
||||||
fixed_nodes = []
|
self._fixed_nodes = []
|
||||||
|
|
||||||
# Add all the items we want to arrange
|
# Add all the items we want to arrange
|
||||||
node_items = []
|
node_items = []
|
||||||
for node in nodes_to_arrange:
|
for node in self._nodes_to_arrange:
|
||||||
hull_polygon = node.callDecoration("getConvexHull")
|
hull_polygon = node.callDecoration("getConvexHull")
|
||||||
if not hull_polygon or hull_polygon.getPoints is None:
|
if not hull_polygon or hull_polygon.getPoints is None:
|
||||||
Logger.log("w", "Object {} cannot be arranged because it has no convex hull.".format(node.getName()))
|
Logger.log("w", "Object {} cannot be arranged because it has no convex hull.".format(node.getName()))
|
||||||
continue
|
continue
|
||||||
converted_points = []
|
converted_points = []
|
||||||
for point in hull_polygon.getPoints():
|
|
||||||
converted_points.append(Point(int(point[0] * factor), int(point[1] * factor)))
|
|
||||||
item = Item(converted_points)
|
|
||||||
node_items.append(item)
|
|
||||||
|
|
||||||
# Use a tiny margin for the build_plate_polygon (the nesting doesn't like overlapping disallowed areas)
|
|
||||||
half_machine_width = 0.5 * machine_width - 1
|
|
||||||
half_machine_depth = 0.5 * machine_depth - 1
|
|
||||||
build_plate_polygon = Polygon(numpy.array([
|
|
||||||
[half_machine_width, -half_machine_depth],
|
|
||||||
[-half_machine_width, -half_machine_depth],
|
|
||||||
[-half_machine_width, half_machine_depth],
|
|
||||||
[half_machine_width, half_machine_depth]
|
|
||||||
], numpy.float32))
|
|
||||||
|
|
||||||
disallowed_areas = build_volume.getDisallowedAreas()
|
|
||||||
num_disallowed_areas_added = 0
|
|
||||||
for area in disallowed_areas:
|
|
||||||
converted_points = []
|
|
||||||
|
|
||||||
# Clip the disallowed areas so that they don't overlap the bounding box (The arranger chokes otherwise)
|
|
||||||
clipped_area = area.intersectionConvexHulls(build_plate_polygon)
|
|
||||||
|
|
||||||
if clipped_area.getPoints() is not None and len(clipped_area.getPoints()) > 2: # numpy array has to be explicitly checked against None
|
|
||||||
for point in clipped_area.getPoints():
|
|
||||||
converted_points.append(Point(int(point[0] * factor), int(point[1] * factor)))
|
|
||||||
|
|
||||||
disallowed_area = Item(converted_points)
|
|
||||||
disallowed_area.markAsDisallowedAreaInBin(0)
|
|
||||||
node_items.append(disallowed_area)
|
|
||||||
num_disallowed_areas_added += 1
|
|
||||||
|
|
||||||
for node in fixed_nodes:
|
|
||||||
converted_points = []
|
|
||||||
hull_polygon = node.callDecoration("getConvexHull")
|
|
||||||
|
|
||||||
if hull_polygon is not None and hull_polygon.getPoints() is not None and len(hull_polygon.getPoints()) > 2: # numpy array has to be explicitly checked against None
|
|
||||||
for point in hull_polygon.getPoints():
|
for point in hull_polygon.getPoints():
|
||||||
converted_points.append(Point(int(point[0] * factor), int(point[1] * factor)))
|
converted_points.append(Point(int(point[0] * self._factor), int(point[1] * self._factor)))
|
||||||
item = Item(converted_points)
|
item = Item(converted_points)
|
||||||
item.markAsFixedInBin(0)
|
|
||||||
node_items.append(item)
|
node_items.append(item)
|
||||||
num_disallowed_areas_added += 1
|
|
||||||
|
|
||||||
config = NfpConfig()
|
# Use a tiny margin for the build_plate_polygon (the nesting doesn't like overlapping disallowed areas)
|
||||||
config.accuracy = 1.0
|
half_machine_width = 0.5 * machine_width - 1
|
||||||
config.alignment = NfpConfig.Alignment.DONT_ALIGN
|
half_machine_depth = 0.5 * machine_depth - 1
|
||||||
|
build_plate_polygon = Polygon(numpy.array([
|
||||||
|
[half_machine_width, -half_machine_depth],
|
||||||
|
[-half_machine_width, -half_machine_depth],
|
||||||
|
[-half_machine_width, half_machine_depth],
|
||||||
|
[half_machine_width, half_machine_depth]
|
||||||
|
], numpy.float32))
|
||||||
|
|
||||||
num_bins = nest(node_items, build_plate_bounding_box, spacing, config)
|
disallowed_areas = self._build_volume.getDisallowedAreas()
|
||||||
|
num_disallowed_areas_added = 0
|
||||||
|
for area in disallowed_areas:
|
||||||
|
converted_points = []
|
||||||
|
|
||||||
# Strip the fixed items (previously placed) and the disallowed areas from the results again.
|
# Clip the disallowed areas so that they don't overlap the bounding box (The arranger chokes otherwise)
|
||||||
node_items = list(filter(lambda item: not item.isFixed(), node_items))
|
clipped_area = area.intersectionConvexHulls(build_plate_polygon)
|
||||||
|
|
||||||
found_solution_for_all = num_bins == 1
|
if clipped_area.getPoints() is not None and len(
|
||||||
|
clipped_area.getPoints()) > 2: # numpy array has to be explicitly checked against None
|
||||||
|
for point in clipped_area.getPoints():
|
||||||
|
converted_points.append(Point(int(point[0] * self._factor), int(point[1] * self._factor)))
|
||||||
|
|
||||||
return found_solution_for_all, node_items
|
disallowed_area = Item(converted_points)
|
||||||
|
disallowed_area.markAsDisallowedAreaInBin(0)
|
||||||
|
node_items.append(disallowed_area)
|
||||||
|
num_disallowed_areas_added += 1
|
||||||
|
|
||||||
|
for node in self._fixed_nodes:
|
||||||
|
converted_points = []
|
||||||
|
hull_polygon = node.callDecoration("getConvexHull")
|
||||||
|
|
||||||
def createGroupOperationForArrange(nodes_to_arrange: List["SceneNode"],
|
if hull_polygon is not None and hull_polygon.getPoints() is not None and len(
|
||||||
build_volume: "BuildVolume",
|
hull_polygon.getPoints()) > 2: # numpy array has to be explicitly checked against None
|
||||||
fixed_nodes: Optional[List["SceneNode"]] = None,
|
for point in hull_polygon.getPoints():
|
||||||
factor = 10000,
|
converted_points.append(Point(int(point[0] * self._factor), int(point[1] * self._factor)))
|
||||||
add_new_nodes_in_scene: bool = False) -> Tuple[GroupedOperation, int]:
|
item = Item(converted_points)
|
||||||
scene_root = Application.getInstance().getController().getScene().getRoot()
|
item.markAsFixedInBin(0)
|
||||||
found_solution_for_all, node_items = findNodePlacement(nodes_to_arrange, build_volume, fixed_nodes, factor)
|
node_items.append(item)
|
||||||
|
num_disallowed_areas_added += 1
|
||||||
|
|
||||||
not_fit_count = 0
|
config = NfpConfig()
|
||||||
grouped_operation = GroupedOperation()
|
config.accuracy = 1.0
|
||||||
for node, node_item in zip(nodes_to_arrange, node_items):
|
config.alignment = NfpConfig.Alignment.DONT_ALIGN
|
||||||
if add_new_nodes_in_scene:
|
if self._lock_rotation:
|
||||||
grouped_operation.addOperation(AddSceneNodeOperation(node, scene_root))
|
config.rotations = [0.0]
|
||||||
|
|
||||||
if node_item.binId() == 0:
|
num_bins = nest(node_items, build_plate_bounding_box, spacing, config)
|
||||||
# We found a spot for it
|
|
||||||
rotation_matrix = Matrix()
|
|
||||||
rotation_matrix.setByRotationAxis(node_item.rotation(), Vector(0, -1, 0))
|
|
||||||
grouped_operation.addOperation(RotateOperation(node, Quaternion.fromMatrix(rotation_matrix)))
|
|
||||||
grouped_operation.addOperation(TranslateOperation(node, Vector(node_item.translation().x() / factor, 0,
|
|
||||||
node_item.translation().y() / factor)))
|
|
||||||
else:
|
|
||||||
# We didn't find a spot
|
|
||||||
grouped_operation.addOperation(
|
|
||||||
TranslateOperation(node, Vector(200, node.getWorldPosition().y, -not_fit_count * 20), set_position = True))
|
|
||||||
not_fit_count += 1
|
|
||||||
|
|
||||||
return grouped_operation, not_fit_count
|
# Strip the fixed items (previously placed) and the disallowed areas from the results again.
|
||||||
|
node_items = list(filter(lambda item: not item.isFixed(), node_items))
|
||||||
|
|
||||||
|
found_solution_for_all = num_bins == 1
|
||||||
|
|
||||||
def arrange(nodes_to_arrange: List["SceneNode"],
|
return found_solution_for_all, node_items
|
||||||
build_volume: "BuildVolume",
|
|
||||||
fixed_nodes: Optional[List["SceneNode"]] = None,
|
|
||||||
factor = 10000,
|
|
||||||
add_new_nodes_in_scene: bool = False) -> bool:
|
|
||||||
"""
|
|
||||||
Find placement for a set of scene nodes, and move them by using a single grouped operation.
|
|
||||||
:param nodes_to_arrange: The list of nodes that need to be moved.
|
|
||||||
:param build_volume: The build volume that we want to place the nodes in. It gets size & disallowed areas from this.
|
|
||||||
:param fixed_nodes: List of nods that should not be moved, but should be used when deciding where the others nodes
|
|
||||||
are placed.
|
|
||||||
:param factor: The library that we use is int based. This factor defines how accuracte we want it to be.
|
|
||||||
:param add_new_nodes_in_scene: Whether to create new scene nodes before applying the transformations and rotations
|
|
||||||
|
|
||||||
:return: found_solution_for_all: Whether the algorithm found a place on the buildplate for all the objects
|
def createGroupOperationForArrange(self, *, add_new_nodes_in_scene: bool = False) -> Tuple[GroupedOperation, int]:
|
||||||
"""
|
scene_root = Application.getInstance().getController().getScene().getRoot()
|
||||||
|
found_solution_for_all, node_items = self.findNodePlacement()
|
||||||
|
|
||||||
grouped_operation, not_fit_count = createGroupOperationForArrange(nodes_to_arrange, build_volume, fixed_nodes, factor, add_new_nodes_in_scene)
|
not_fit_count = 0
|
||||||
grouped_operation.push()
|
grouped_operation = GroupedOperation()
|
||||||
return not_fit_count == 0
|
for node, node_item in zip(self._nodes_to_arrange, node_items):
|
||||||
|
if add_new_nodes_in_scene:
|
||||||
|
grouped_operation.addOperation(AddSceneNodeOperation(node, scene_root))
|
||||||
|
|
||||||
|
if node_item.binId() == 0:
|
||||||
|
# We found a spot for it
|
||||||
|
rotation_matrix = Matrix()
|
||||||
|
rotation_matrix.setByRotationAxis(node_item.rotation(), Vector(0, -1, 0))
|
||||||
|
grouped_operation.addOperation(RotateOperation(node, Quaternion.fromMatrix(rotation_matrix)))
|
||||||
|
grouped_operation.addOperation(
|
||||||
|
TranslateOperation(node, Vector(node_item.translation().x() / self._factor, 0,
|
||||||
|
node_item.translation().y() / self._factor)))
|
||||||
|
else:
|
||||||
|
# We didn't find a spot
|
||||||
|
grouped_operation.addOperation(
|
||||||
|
TranslateOperation(node, Vector(200, node.getWorldPosition().y, -not_fit_count * 20), set_position = True))
|
||||||
|
not_fit_count += 1
|
||||||
|
|
||||||
|
return grouped_operation, not_fit_count
|
||||||
|
@ -203,6 +203,9 @@ class BuildVolume(SceneNode):
|
|||||||
if shape:
|
if shape:
|
||||||
self._shape = shape
|
self._shape = shape
|
||||||
|
|
||||||
|
def getShape(self) -> str:
|
||||||
|
return self._shape
|
||||||
|
|
||||||
def getDiagonalSize(self) -> float:
|
def getDiagonalSize(self) -> float:
|
||||||
"""Get the length of the 3D diagonal through the build volume.
|
"""Get the length of the 3D diagonal through the build volume.
|
||||||
|
|
||||||
|
@ -22,7 +22,10 @@ from cura.Operations.SetParentOperation import SetParentOperation
|
|||||||
from cura.MultiplyObjectsJob import MultiplyObjectsJob
|
from cura.MultiplyObjectsJob import MultiplyObjectsJob
|
||||||
from cura.Settings.SetObjectExtruderOperation import SetObjectExtruderOperation
|
from cura.Settings.SetObjectExtruderOperation import SetObjectExtruderOperation
|
||||||
from cura.Settings.ExtruderManager import ExtruderManager
|
from cura.Settings.ExtruderManager import ExtruderManager
|
||||||
from cura.Arranging.Nest2DArrange import createGroupOperationForArrange
|
|
||||||
|
from cura.Arranging.GridArrange import GridArrange
|
||||||
|
from cura.Arranging.Nest2DArrange import Nest2DArrange
|
||||||
|
|
||||||
|
|
||||||
from cura.Operations.SetBuildPlateNumberOperation import SetBuildPlateNumberOperation
|
from cura.Operations.SetBuildPlateNumberOperation import SetBuildPlateNumberOperation
|
||||||
|
|
||||||
@ -82,16 +85,25 @@ class CuraActions(QObject):
|
|||||||
center_operation = TranslateOperation(current_node, Vector(0, center_y, 0), set_position = True)
|
center_operation = TranslateOperation(current_node, Vector(0, center_y, 0), set_position = True)
|
||||||
operation.addOperation(center_operation)
|
operation.addOperation(center_operation)
|
||||||
operation.push()
|
operation.push()
|
||||||
|
|
||||||
@pyqtSlot(int)
|
@pyqtSlot(int)
|
||||||
def multiplySelection(self, count: int) -> None:
|
def multiplySelection(self, count: int) -> None:
|
||||||
"""Multiply all objects in the selection
|
"""Multiply all objects in the selection
|
||||||
|
:param count: The number of times to multiply the selection.
|
||||||
|
"""
|
||||||
|
min_offset = cura.CuraApplication.CuraApplication.getInstance().getBuildVolume().getEdgeDisallowedSize() + 2 # Allow for some rounding errors
|
||||||
|
job = MultiplyObjectsJob(Selection.getAllSelectedObjects(), count, min_offset = max(min_offset, 8))
|
||||||
|
job.start()
|
||||||
|
|
||||||
|
@pyqtSlot(int)
|
||||||
|
def multiplySelectionToGrid(self, count: int) -> None:
|
||||||
|
"""Multiply all objects in the selection
|
||||||
|
|
||||||
:param count: The number of times to multiply the selection.
|
:param count: The number of times to multiply the selection.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
min_offset = cura.CuraApplication.CuraApplication.getInstance().getBuildVolume().getEdgeDisallowedSize() + 2 # Allow for some rounding errors
|
min_offset = cura.CuraApplication.CuraApplication.getInstance().getBuildVolume().getEdgeDisallowedSize() + 2 # Allow for some rounding errors
|
||||||
job = MultiplyObjectsJob(Selection.getAllSelectedObjects(), count, min_offset = max(min_offset, 8))
|
job = MultiplyObjectsJob(Selection.getAllSelectedObjects(), count, min_offset=max(min_offset, 8),
|
||||||
|
grid_arrange=True)
|
||||||
job.start()
|
job.start()
|
||||||
|
|
||||||
@pyqtSlot()
|
@pyqtSlot()
|
||||||
@ -229,9 +241,9 @@ class CuraActions(QObject):
|
|||||||
if node.callDecoration("isSliceable"):
|
if node.callDecoration("isSliceable"):
|
||||||
fixed_nodes.append(node)
|
fixed_nodes.append(node)
|
||||||
# Add the new nodes to the scene, and arrange them
|
# Add the new nodes to the scene, and arrange them
|
||||||
group_operation, not_fit_count = createGroupOperationForArrange(nodes, application.getBuildVolume(),
|
|
||||||
fixed_nodes, factor=10000,
|
arranger = GridArrange(nodes, application.getBuildVolume(), fixed_nodes)
|
||||||
add_new_nodes_in_scene=True)
|
group_operation, not_fit_count = arranger.createGroupOperationForArrange(add_new_nodes_in_scene = True)
|
||||||
group_operation.push()
|
group_operation.push()
|
||||||
|
|
||||||
# deselect currently selected nodes, and select the new nodes
|
# deselect currently selected nodes, and select the new nodes
|
||||||
|
@ -54,7 +54,6 @@ from cura import ApplicationMetadata
|
|||||||
from cura.API import CuraAPI
|
from cura.API import CuraAPI
|
||||||
from cura.API.Account import Account
|
from cura.API.Account import Account
|
||||||
from cura.Arranging.ArrangeObjectsJob import ArrangeObjectsJob
|
from cura.Arranging.ArrangeObjectsJob import ArrangeObjectsJob
|
||||||
from cura.Arranging.Nest2DArrange import arrange
|
|
||||||
from cura.Machines.MachineErrorChecker import MachineErrorChecker
|
from cura.Machines.MachineErrorChecker import MachineErrorChecker
|
||||||
from cura.Machines.Models.BuildPlateModel import BuildPlateModel
|
from cura.Machines.Models.BuildPlateModel import BuildPlateModel
|
||||||
from cura.Machines.Models.CustomQualityProfilesDropDownMenuModel import CustomQualityProfilesDropDownMenuModel
|
from cura.Machines.Models.CustomQualityProfilesDropDownMenuModel import CustomQualityProfilesDropDownMenuModel
|
||||||
@ -115,6 +114,7 @@ from . import CameraAnimation
|
|||||||
from . import CuraActions
|
from . import CuraActions
|
||||||
from . import PlatformPhysics
|
from . import PlatformPhysics
|
||||||
from . import PrintJobPreviewImageProvider
|
from . import PrintJobPreviewImageProvider
|
||||||
|
from .Arranging.Nest2DArrange import Nest2DArrange
|
||||||
from .AutoSave import AutoSave
|
from .AutoSave import AutoSave
|
||||||
from .Machines.Models.CompatibleMachineModel import CompatibleMachineModel
|
from .Machines.Models.CompatibleMachineModel import CompatibleMachineModel
|
||||||
from .Machines.Models.MachineListModel import MachineListModel
|
from .Machines.Models.MachineListModel import MachineListModel
|
||||||
@ -1444,6 +1444,13 @@ class CuraApplication(QtApplication):
|
|||||||
# Single build plate
|
# Single build plate
|
||||||
@pyqtSlot()
|
@pyqtSlot()
|
||||||
def arrangeAll(self) -> None:
|
def arrangeAll(self) -> None:
|
||||||
|
self._arrangeAll(grid_arrangement = False)
|
||||||
|
|
||||||
|
@pyqtSlot()
|
||||||
|
def arrangeAllInGrid(self) -> None:
|
||||||
|
self._arrangeAll(grid_arrangement = True)
|
||||||
|
|
||||||
|
def _arrangeAll(self, *, grid_arrangement: bool) -> None:
|
||||||
nodes_to_arrange = []
|
nodes_to_arrange = []
|
||||||
active_build_plate = self.getMultiBuildPlateModel().activeBuildPlate
|
active_build_plate = self.getMultiBuildPlateModel().activeBuildPlate
|
||||||
locked_nodes = []
|
locked_nodes = []
|
||||||
@ -1473,17 +1480,17 @@ class CuraApplication(QtApplication):
|
|||||||
locked_nodes.append(node)
|
locked_nodes.append(node)
|
||||||
else:
|
else:
|
||||||
nodes_to_arrange.append(node)
|
nodes_to_arrange.append(node)
|
||||||
self.arrange(nodes_to_arrange, locked_nodes)
|
self.arrange(nodes_to_arrange, locked_nodes, grid_arrangement = grid_arrangement)
|
||||||
|
|
||||||
def arrange(self, nodes: List[SceneNode], fixed_nodes: List[SceneNode]) -> None:
|
def arrange(self, nodes: List[SceneNode], fixed_nodes: List[SceneNode], *, grid_arrangement: bool = False) -> None:
|
||||||
"""Arrange a set of nodes given a set of fixed nodes
|
"""Arrange a set of nodes given a set of fixed nodes
|
||||||
|
|
||||||
:param nodes: nodes that we have to place
|
:param nodes: nodes that we have to place
|
||||||
:param fixed_nodes: nodes that are placed in the arranger before finding spots for nodes
|
:param fixed_nodes: nodes that are placed in the arranger before finding spots for nodes
|
||||||
|
:param grid_arrangement: If set to true if objects are to be placed in a grid
|
||||||
"""
|
"""
|
||||||
|
|
||||||
min_offset = self.getBuildVolume().getEdgeDisallowedSize() + 2 # Allow for some rounding errors
|
min_offset = self.getBuildVolume().getEdgeDisallowedSize() + 2 # Allow for some rounding errors
|
||||||
job = ArrangeObjectsJob(nodes, fixed_nodes, min_offset = max(min_offset, 8))
|
job = ArrangeObjectsJob(nodes, fixed_nodes, min_offset = max(min_offset, 8), grid_arrange = grid_arrangement)
|
||||||
job.start()
|
job.start()
|
||||||
|
|
||||||
@pyqtSlot()
|
@pyqtSlot()
|
||||||
@ -1970,7 +1977,8 @@ class CuraApplication(QtApplication):
|
|||||||
if select_models_on_load:
|
if select_models_on_load:
|
||||||
Selection.add(node)
|
Selection.add(node)
|
||||||
try:
|
try:
|
||||||
arrange(nodes_to_arrange, self.getBuildVolume(), fixed_nodes)
|
arranger = Nest2DArrange(nodes_to_arrange, self.getBuildVolume(), fixed_nodes)
|
||||||
|
arranger.arrange()
|
||||||
except:
|
except:
|
||||||
Logger.logException("e", "Failed to arrange the models")
|
Logger.logException("e", "Failed to arrange the models")
|
||||||
|
|
||||||
|
@ -14,17 +14,19 @@ from UM.Operations.TranslateOperation import TranslateOperation
|
|||||||
from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
|
from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
|
||||||
from UM.Scene.SceneNode import SceneNode
|
from UM.Scene.SceneNode import SceneNode
|
||||||
from UM.i18n import i18nCatalog
|
from UM.i18n import i18nCatalog
|
||||||
from cura.Arranging.Nest2DArrange import arrange, createGroupOperationForArrange
|
from cura.Arranging.GridArrange import GridArrange
|
||||||
|
from cura.Arranging.Nest2DArrange import Nest2DArrange
|
||||||
|
|
||||||
i18n_catalog = i18nCatalog("cura")
|
i18n_catalog = i18nCatalog("cura")
|
||||||
|
|
||||||
|
|
||||||
class MultiplyObjectsJob(Job):
|
class MultiplyObjectsJob(Job):
|
||||||
def __init__(self, objects, count, min_offset = 8):
|
def __init__(self, objects, count: int, min_offset: int = 8 ,* , grid_arrange: bool = False):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self._objects = objects
|
self._objects = objects
|
||||||
self._count = count
|
self._count: int = count
|
||||||
self._min_offset = min_offset
|
self._min_offset: int = min_offset
|
||||||
|
self._grid_arrange: bool = grid_arrange
|
||||||
|
|
||||||
def run(self) -> None:
|
def run(self) -> None:
|
||||||
status_message = Message(i18n_catalog.i18nc("@info:status", "Multiplying and placing objects"), lifetime = 0,
|
status_message = Message(i18n_catalog.i18nc("@info:status", "Multiplying and placing objects"), lifetime = 0,
|
||||||
@ -39,7 +41,7 @@ class MultiplyObjectsJob(Job):
|
|||||||
|
|
||||||
root = scene.getRoot()
|
root = scene.getRoot()
|
||||||
|
|
||||||
processed_nodes = [] # type: List[SceneNode]
|
processed_nodes: List[SceneNode] = []
|
||||||
nodes = []
|
nodes = []
|
||||||
|
|
||||||
fixed_nodes = []
|
fixed_nodes = []
|
||||||
@ -76,12 +78,12 @@ class MultiplyObjectsJob(Job):
|
|||||||
found_solution_for_all = True
|
found_solution_for_all = True
|
||||||
group_operation = GroupedOperation()
|
group_operation = GroupedOperation()
|
||||||
if nodes:
|
if nodes:
|
||||||
group_operation, not_fit_count = createGroupOperationForArrange(nodes,
|
if self._grid_arrange:
|
||||||
Application.getInstance().getBuildVolume(),
|
arranger = GridArrange(nodes, Application.getInstance().getBuildVolume(), fixed_nodes)
|
||||||
fixed_nodes,
|
else:
|
||||||
factor = 10000,
|
arranger = Nest2DArrange(nodes, Application.getInstance().getBuildVolume(), fixed_nodes, factor=1000)
|
||||||
add_new_nodes_in_scene = True)
|
|
||||||
found_solution_for_all = not_fit_count == 0
|
group_operation, not_fit_count = arranger.createGroupOperationForArrange(add_new_nodes_in_scene=True)
|
||||||
|
|
||||||
if nodes_to_add_without_arrange:
|
if nodes_to_add_without_arrange:
|
||||||
for nested_node in nodes_to_add_without_arrange:
|
for nested_node in nodes_to_add_without_arrange:
|
||||||
|
@ -41,7 +41,7 @@ Item
|
|||||||
property alias deleteAll: deleteAllAction
|
property alias deleteAll: deleteAllAction
|
||||||
property alias reloadAll: reloadAllAction
|
property alias reloadAll: reloadAllAction
|
||||||
property alias arrangeAll: arrangeAllAction
|
property alias arrangeAll: arrangeAllAction
|
||||||
property alias arrangeSelection: arrangeSelectionAction
|
property alias arrangeAllGrid: arrangeAllGridAction
|
||||||
property alias resetAllTranslation: resetAllTranslationAction
|
property alias resetAllTranslation: resetAllTranslationAction
|
||||||
property alias resetAll: resetAllAction
|
property alias resetAll: resetAllAction
|
||||||
|
|
||||||
@ -462,9 +462,10 @@ Item
|
|||||||
|
|
||||||
Action
|
Action
|
||||||
{
|
{
|
||||||
id: arrangeSelectionAction
|
id: arrangeAllGridAction
|
||||||
text: catalog.i18nc("@action:inmenu menubar:edit","Arrange Selection")
|
text: catalog.i18nc("@action:inmenu menubar:edit","Arrange All Models in a grid")
|
||||||
onTriggered: Printer.arrangeSelection()
|
onTriggered: Printer.arrangeAllInGrid()
|
||||||
|
shortcut: "Shift+Ctrl+R"
|
||||||
}
|
}
|
||||||
|
|
||||||
Action
|
Action
|
||||||
|
@ -66,6 +66,7 @@ Cura.Menu
|
|||||||
Cura.MenuSeparator {}
|
Cura.MenuSeparator {}
|
||||||
Cura.MenuItem { action: Cura.Actions.selectAll }
|
Cura.MenuItem { action: Cura.Actions.selectAll }
|
||||||
Cura.MenuItem { action: Cura.Actions.arrangeAll }
|
Cura.MenuItem { action: Cura.Actions.arrangeAll }
|
||||||
|
Cura.MenuItem { action: Cura.Actions.arrangeAllGrid }
|
||||||
Cura.MenuItem { action: Cura.Actions.deleteAll }
|
Cura.MenuItem { action: Cura.Actions.deleteAll }
|
||||||
Cura.MenuItem { action: Cura.Actions.reloadAll }
|
Cura.MenuItem { action: Cura.Actions.reloadAll }
|
||||||
Cura.MenuItem { action: Cura.Actions.resetAllTranslation }
|
Cura.MenuItem { action: Cura.Actions.resetAllTranslation }
|
||||||
@ -108,9 +109,7 @@ Cura.Menu
|
|||||||
height: UM.Theme.getSize("small_popup_dialog").height
|
height: UM.Theme.getSize("small_popup_dialog").height
|
||||||
minimumWidth: UM.Theme.getSize("small_popup_dialog").width
|
minimumWidth: UM.Theme.getSize("small_popup_dialog").width
|
||||||
minimumHeight: UM.Theme.getSize("small_popup_dialog").height
|
minimumHeight: UM.Theme.getSize("small_popup_dialog").height
|
||||||
|
onAccepted: gridPlacementSelected.checked? CuraActions.multiplySelectionToGrid(copiesField.value) : CuraActions.multiplySelection(copiesField.value)
|
||||||
onAccepted: CuraActions.multiplySelection(copiesField.value)
|
|
||||||
|
|
||||||
buttonSpacing: UM.Theme.getSize("thin_margin").width
|
buttonSpacing: UM.Theme.getSize("thin_margin").width
|
||||||
|
|
||||||
rightButtons:
|
rightButtons:
|
||||||
@ -127,28 +126,49 @@ Cura.Menu
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
Row
|
Column
|
||||||
{
|
{
|
||||||
spacing: UM.Theme.getSize("default_margin").width
|
spacing: UM.Theme.getSize("default_margin").height
|
||||||
|
|
||||||
UM.Label
|
Row
|
||||||
{
|
{
|
||||||
text: catalog.i18nc("@label", "Number of Copies")
|
spacing: UM.Theme.getSize("default_margin").width
|
||||||
anchors.verticalCenter: copiesField.verticalCenter
|
|
||||||
width: contentWidth
|
UM.Label
|
||||||
wrapMode: Text.NoWrap
|
{
|
||||||
|
text: catalog.i18nc("@label", "Number of Copies")
|
||||||
|
anchors.verticalCenter: copiesField.verticalCenter
|
||||||
|
width: contentWidth
|
||||||
|
wrapMode: Text.NoWrap
|
||||||
|
}
|
||||||
|
|
||||||
|
Cura.SpinBox
|
||||||
|
{
|
||||||
|
id: copiesField
|
||||||
|
editable: true
|
||||||
|
focus: true
|
||||||
|
from: 1
|
||||||
|
to: 99
|
||||||
|
width: 2 * UM.Theme.getSize("button").width
|
||||||
|
value: 1
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Cura.SpinBox
|
UM.CheckBox
|
||||||
{
|
{
|
||||||
id: copiesField
|
id: gridPlacementSelected
|
||||||
editable: true
|
text: catalog.i18nc("@label", "Grid Placement")
|
||||||
focus: true
|
|
||||||
from: 1
|
UM.ToolTip
|
||||||
to: 99
|
{
|
||||||
width: 2 * UM.Theme.getSize("button").width
|
visible: parent.hovered
|
||||||
value: 1
|
targetPoint: Qt.point(parent.x + Math.round(parent.width / 2), parent.y)
|
||||||
|
x: 0
|
||||||
|
y: parent.y + parent.height + UM.Theme.getSize("default_margin").height
|
||||||
|
tooltipText: catalog.i18nc("@info", "Multiply selected item and place them in a grid of build plate.")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user