Merge branch 'main' into PP-350_cleanup_print_temperature

This commit is contained in:
saumya.jain 2023-09-12 10:21:21 +02:00
commit 66318d63dc
316 changed files with 7235 additions and 4447 deletions

View file

@ -5,17 +5,17 @@ body:
- type: markdown
attributes:
value: |
**Thank you for using Cura and wanting to report a bug.**
**Thank you for using Cura and wanting to report a bug. 🙏**
Before filing, please check if the issue already exists (either open or closed) by using the search bar on the issues page.
Before filing, [please check if the issue already exists](https://github.com/Ultimaker/Cura/issues?q=is%3Aissue) by using the search bar on the issues page.
If it does, comment there. Even if it's closed, we can reopen it based on your comment.
Also, please note the application version in the title of the issue "For example (5.3.1) Cannot connect to 3rd-party printer". Please do not write things like **Request** or **BUG** in the title, this is what labels are for.
Please include the cura version in the title of the issue. For example, *"[5.4.0] Support Brim is missing in this model"*.
- type: input
attributes:
label: Application Version
label: Cura Version
description: The version of Cura this issue occurs with.
placeholder: 5.3.0
placeholder: 5.4.0
validations:
required: true
- type: input
@ -28,14 +28,14 @@ body:
- type: input
attributes:
label: Printer
description: Which printer was selected in Cura?
placeholder: Ultimaker S7
description: Which printer was selected in Cura? It also helps to mention if you made any firmware modifications to your printer.
placeholder: Ultimaker S7 / Creality CR-10 with Klipper
validations:
required: true
- type: textarea
attributes:
label: Reproduction steps
description: Tell us what you did!
description: Share what you did, so we can reproduce it
placeholder: |
1. Something you did
2. Something you did next
@ -44,42 +44,39 @@ body:
- type: textarea
attributes:
label: Actual results
description: What happens after the above steps have been followed.
description: What happens after the above steps have been followed?
validations:
required: true
- type: textarea
attributes:
label: Expected results
description: What should happen after the above steps have been followed.
description: What should happen after the above steps have been followed?
validations:
required: true
- type: markdown
attributes:
value: |
Please be sure to add the following files:
* To save a project file go to File -> Save project.
Please make sure to .zip your project file.
For big files, you may need to use [WeTransfer](https://wetransfer.com/) or similar file-sharing sites.
G-code files are not project files!
Before you share, please think to yourself. Is this a model that can be shared?
* **Screenshots** of showing the problem, perhaps before/after images.
* A **log file** for crashes and similar issues.
You can find your log file here:
Windows: `%APPDATA%\cura\<Cura version>\cura.log` or usually `C:\Users\\<your username>\AppData\Roaming\cura\<Cura version>\cura.log`
MacOS: `$USER/Library/Application Support/cura/<Cura version>/cura.log`
Ubuntu/Linux: `$USER/.local/share/cura/<Cura version>/cura.log`
If the Cura user interface still starts, you can also reach this directory from the application menu in Help -> Show settings folder
- type: checkboxes
attributes:
label: Checklist of files to include
options:
- label: Log file
- label: Project file
### Please add the following files when they are related to...
* 🔵 **The quality of your print**
Please add **a Project File**. It contains the printer and settings we need for troubleshooting.
To save a project file go to File -> Save project.
Please make sure to .zip your project file. For big files, you may need to use [WeTransfer](https://wetransfer.com/) or similar file-sharing sites.
G-code files are not project files! Before you share, please think to yourself. Is this a model that can be shared?
![Alt Text](https://user-images.githubusercontent.com/40423138/240616958-5a9751f2-bd34-4808-9752-6fde2e27516e.gif)
* 🔵 **Using and interacting with Cura**
Please add **screenshots** showing the issue.
Before and after, and arrows can help here.
* 🔵 **Unexpected crashes and behavior**
Please add **a log file** with information on what your Cura is doing.
You can find your log file here:
Windows: `%APPDATA%\cura\<Cura version>\cura.log`
MacOS: `$USER/Library/Application Support/cura/<Cura version>/cura.log`
Ubuntu/Linux: `$USER/.local/share/cura/<Cura version>/cura.log`
If the Cura user interface still starts, you can also reach this directory from the application menu in Help -> Show settings folder
- type: textarea
attributes:
label: Additional information & file uploads
description: You can add these files and additional information that is relevant to the issue in the comments below.
label: Add your .zip and screenshots here ⬇️
description: You can add the zip file and additional information that is relevant to the issue in the comments below.
validations:
required: true

View file

@ -28,6 +28,6 @@ This fixes... OR This improves... -->
<!-- Check if relevant -->
- [ ] My code follows the style guidelines of this project as described in [UltiMaker Meta](https://github.com/Ultimaker/Meta) and [Cura QML best practices](https://github.com/Ultimaker/Cura/wiki/QML-Best-Practices)
- [ ] I have read the [Contribution guide](https://github.com/Ultimaker/Cura/blob/main/contributing.md)
- [ ] I have read the [Contribution guide](https://github.com/Ultimaker/Cura/blob/main/CONTRIBUTING.md)
- [ ] I have commented my code, particularly in hard-to-understand areas
- [ ] I have uploaded any files required to test this change
- [ ] I have uploaded any files required to test this change

View file

@ -1,158 +1,153 @@
name: Create and Upload Conan package
on:
workflow_call:
inputs:
project_name:
required: true
type: string
workflow_call:
inputs:
project_name:
required: true
type: string
recipe_id_full:
required: true
type: string
recipe_id_full:
required: true
type: string
build_id:
required: true
type: number
build_id:
required: true
type: number
build_info:
required: false
default: true
type: boolean
build_info:
required: false
default: true
type: boolean
recipe_id_latest:
required: false
type: string
recipe_id_latest:
required: false
type: string
runs_on:
required: true
type: string
runs_on:
required: true
type: string
python_version:
required: true
type: string
python_version:
required: true
type: string
conan_config_branch:
required: false
type: string
conan_config_branch:
required: false
type: string
conan_logging_level:
required: false
type: string
conan_logging_level:
required: false
type: string
conan_clean_local_cache:
required: false
type: boolean
default: false
conan_clean_local_cache:
required: false
type: boolean
default: false
conan_upload_community:
required: false
default: true
type: boolean
conan_upload_community:
required: false
default: true
type: boolean
env:
CONAN_LOGIN_USERNAME: ${{ secrets.CONAN_USER }}
CONAN_PASSWORD: ${{ secrets.CONAN_PASS }}
CONAN_LOG_RUN_TO_OUTPUT: 1
CONAN_LOGGING_LEVEL: ${{ inputs.conan_logging_level }}
CONAN_NON_INTERACTIVE: 1
CONAN_LOGIN_USERNAME: ${{ secrets.CONAN_USER }}
CONAN_PASSWORD: ${{ secrets.CONAN_PASS }}
CONAN_LOG_RUN_TO_OUTPUT: 1
CONAN_LOGGING_LEVEL: ${{ inputs.conan_logging_level }}
CONAN_NON_INTERACTIVE: 1
jobs:
conan-package-create:
runs-on: ${{ inputs.runs_on }}
conan-package-create:
runs-on: ${{ inputs.runs_on }}
steps:
- name: Checkout
uses: actions/checkout@v3
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Setup Python and pip
uses: actions/setup-python@v4
with:
python-version: ${{ inputs.python_version }}
cache: 'pip'
cache-dependency-path: .github/workflows/requirements-conan-package.txt
- name: Setup Python and pip
uses: actions/setup-python@v4
with:
python-version: ${{ inputs.python_version }}
cache: 'pip'
cache-dependency-path: .github/workflows/requirements-conan-package.txt
- name: Install Python requirements for runner
run: pip install -r https://raw.githubusercontent.com/Ultimaker/Cura/main/.github/workflows/requirements-conan-package.txt
# Note the runner requirements are always installed from the main branch in the Ultimaker/Cura repo
- name: Install Python requirements for runner
run: pip install -r https://raw.githubusercontent.com/Ultimaker/Cura/main/.github/workflows/requirements-conan-package.txt
# Note the runner requirements are always installed from the main branch in the Ultimaker/Cura repo
- name: Use Conan download cache (Bash)
if: ${{ runner.os != 'Windows' }}
run: conan config set storage.download_cache="$HOME/.conan/conan_download_cache"
- name: Use Conan download cache (Bash)
if: ${{ runner.os != 'Windows' }}
run: conan config set storage.download_cache="$HOME/.conan/conan_download_cache"
- name: Use Conan download cache (Powershell)
if: ${{ runner.os == 'Windows' }}
run: conan config set storage.download_cache="C:\Users\runneradmin\.conan\conan_download_cache"
- name: Use Conan download cache (Powershell)
if: ${{ runner.os == 'Windows' }}
run: conan config set storage.download_cache="C:\Users\runneradmin\.conan\conan_download_cache"
- name: Cache Conan local repository packages (Bash)
uses: actions/cache@v3
if: ${{ runner.os != 'Windows' }}
with:
path: |
$HOME/.conan/data
$HOME/.conan/conan_download_cache
key: conan-${{ inputs.runs_on }}-${{ runner.arch }}-create-cache
- name: Cache Conan local repository packages (Bash)
uses: actions/cache@v3
if: ${{ runner.os != 'Windows' }}
with:
path: |
$HOME/.conan/data
$HOME/.conan/conan_download_cache
key: conan-${{ inputs.runs_on }}-${{ runner.arch }}-create-cache
- name: Cache Conan local repository packages (Powershell)
uses: actions/cache@v3
if: ${{ runner.os == 'Windows' }}
with:
path: |
C:\Users\runneradmin\.conan\data
C:\.conan
C:\Users\runneradmin\.conan\conan_download_cache
key: conan-${{ inputs.runs_on }}-${{ runner.arch }}-create-cache
- name: Cache Conan local repository packages (Powershell)
uses: actions/cache@v3
if: ${{ runner.os == 'Windows' }}
with:
path: |
C:\Users\runneradmin\.conan\data
C:\.conan
C:\Users\runneradmin\.conan\conan_download_cache
key: conan-${{ inputs.runs_on }}-${{ runner.arch }}-create-cache
- name: Install MacOS system requirements
if: ${{ runner.os == 'Macos' }}
run: brew install autoconf automake ninja
- name: Install MacOS system requirements
if: ${{ runner.os == 'Macos' }}
run: brew install autoconf automake ninja
# NOTE: Due to what are probably github issues, we have to remove the cache and reconfigure before the rest.
# This is maybe because grub caches the disk it uses last time, which is recreated each time.
- name: Install Linux system requirements
if: ${{ runner.os == 'Linux' }}
run: |
sudo rm /var/cache/debconf/config.dat
sudo dpkg --configure -a
sudo add-apt-repository ppa:ubuntu-toolchain-r/test -y
sudo apt update
sudo apt upgrade
sudo apt install build-essential checkinstall libegl-dev zlib1g-dev libssl-dev ninja-build autoconf libx11-dev libx11-xcb-dev libfontenc-dev libice-dev libsm-dev libxau-dev libxaw7-dev libxcomposite-dev libxcursor-dev libxdamage-dev libxdmcp-dev libxext-dev libxfixes-dev libxi-dev libxinerama-dev libxkbfile-dev libxmu-dev libxmuu-dev libxpm-dev libxrandr-dev libxrender-dev libxres-dev libxss-dev libxt-dev libxtst-dev libxv-dev libxvmc-dev libxxf86vm-dev xtrans-dev libxcb-render0-dev libxcb-render-util0-dev libxcb-xkb-dev libxcb-icccm4-dev libxcb-image0-dev libxcb-keysyms1-dev libxcb-randr0-dev libxcb-shape0-dev libxcb-sync-dev libxcb-xfixes0-dev libxcb-xinerama0-dev xkb-data libxcb-dri3-dev uuid-dev libxcb-util-dev libxkbcommon-x11-dev pkg-config flex bison -y
# NOTE: Due to what are probably github issues, we have to remove the cache and reconfigure before the rest.
# This is maybe because grub caches the disk it uses last time, which is recreated each time.
- name: Install Linux system requirements
if: ${{ runner.os == 'Linux' }}
run: |
sudo rm /var/cache/debconf/config.dat
sudo dpkg --configure -a
sudo add-apt-repository ppa:ubuntu-toolchain-r/test -y
sudo apt update
sudo apt upgrade
sudo apt install build-essential checkinstall libegl-dev zlib1g-dev libssl-dev ninja-build autoconf libx11-dev libx11-xcb-dev libfontenc-dev libice-dev libsm-dev libxau-dev libxaw7-dev libxcomposite-dev libxcursor-dev libxdamage-dev libxdmcp-dev libxext-dev libxfixes-dev libxi-dev libxinerama-dev libxkbfile-dev libxmu-dev libxmuu-dev libxpm-dev libxrandr-dev libxrender-dev libxres-dev libxss-dev libxt-dev libxtst-dev libxv-dev libxvmc-dev libxxf86vm-dev xtrans-dev libxcb-render0-dev libxcb-render-util0-dev libxcb-xkb-dev libxcb-icccm4-dev libxcb-image0-dev libxcb-keysyms1-dev libxcb-randr0-dev libxcb-shape0-dev libxcb-sync-dev libxcb-xfixes0-dev libxcb-xinerama0-dev xkb-data libxcb-dri3-dev uuid-dev libxcb-util-dev libxkbcommon-x11-dev pkg-config flex bison -y
- name: Install GCC-12 on ubuntu-22.04
if: ${{ startsWith(inputs.runs_on, 'ubuntu-22.04') }}
run: |
sudo apt install g++-12 gcc-12 -y
sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-12 12
sudo update-alternatives --install /usr/bin/g++ g++ /usr/bin/g++-12 12
- name: Install GCC-13 on ubuntu
if: ${{ startsWith(inputs.runs_on, 'ubuntu') }}
run: |
sudo apt install g++-13 gcc-13 -y
sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-13 13
sudo update-alternatives --install /usr/bin/g++ g++ /usr/bin/g++-13 13
- name: Use GCC-10 on ubuntu-20.04
if: ${{ startsWith(inputs.runs_on, 'ubuntu-20.04') }}
run: |
sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-10 10
sudo update-alternatives --install /usr/bin/g++ g++ /usr/bin/g++-10 10
- name: Create the default Conan profile
run: conan profile new default --detect
- name: Create the default Conan profile
run: conan profile new default --detect
- name: Get Conan configuration from branch
if: ${{ inputs.conan_config_branch != '' }}
run: conan config install https://github.com/Ultimaker/conan-config.git -a "-b ${{ inputs.conan_config_branch }}"
- name: Get Conan configuration from branch
if: ${{ inputs.conan_config_branch != '' }}
run: conan config install https://github.com/Ultimaker/conan-config.git -a "-b ${{ inputs.conan_config_branch }}"
- name: Get Conan configuration
run: |
conan config install https://github.com/Ultimaker/conan-config.git
conan config install https://github.com/Ultimaker/conan-config.git -a "-b runner/${{ runner.os }}/${{ runner.arch }}"
- name: Get Conan configuration
if: ${{ inputs.conan_config_branch == '' }}
run: conan config install https://github.com/Ultimaker/conan-config.git
- name: Add Cura private Artifactory remote
run: conan remote add cura-private https://ultimaker.jfrog.io/artifactory/api/conan/cura-private True
- name: Add Cura private Artifactory remote
run: conan remote add cura-private https://ultimaker.jfrog.io/artifactory/api/conan/cura-private True
- name: Create the Packages
run: conan install ${{ inputs.recipe_id_full }} --build=missing --update -c tools.build:skip_test=True
- name: Create the Packages
run: conan install ${{ inputs.recipe_id_full }} --build=missing --update
- name: Upload the Package(s)
if: ${{ always() && inputs.conan_upload_community }}
run: conan upload ${{ inputs.recipe_id_full }} -r cura --all -c
- name: Upload the Package(s)
if: ${{ always() && inputs.conan_upload_community }}
run: conan upload ${{ inputs.recipe_id_full }} -r cura --all -c
- name: Upload the Package(s) to the private Artifactory
if: ${{ always() && ! inputs.conan_upload_community }}
run: conan upload ${{ inputs.recipe_id_full }} -r cura-private --all -c
- name: Upload the Package(s) to the private Artifactory
if: ${{ always() && ! inputs.conan_upload_community }}
run: conan upload ${{ inputs.recipe_id_full }} -r cura-private --all -c

View file

@ -49,15 +49,15 @@ on:
- '[1-9].[0-9][0-9].[0-9]*'
env:
CONAN_LOGIN_USERNAME_CURA: ${{ secrets.CONAN_USER }}
CONAN_PASSWORD_CURA: ${{ secrets.CONAN_PASS }}
CONAN_LOGIN_USERNAME_CURA_CE: ${{ secrets.CONAN_USER }}
CONAN_PASSWORD_CURA_CE: ${{ secrets.CONAN_PASS }}
CONAN_LOG_RUN_TO_OUTPUT: 1
CONAN_LOGGING_LEVEL: ${{ inputs.conan_logging_level }}
CONAN_NON_INTERACTIVE: 1
CONAN_LOGIN_USERNAME_CURA: ${{ secrets.CONAN_USER }}
CONAN_PASSWORD_CURA: ${{ secrets.CONAN_PASS }}
CONAN_LOGIN_USERNAME_CURA_CE: ${{ secrets.CONAN_USER }}
CONAN_PASSWORD_CURA_CE: ${{ secrets.CONAN_PASS }}
CONAN_LOG_RUN_TO_OUTPUT: 1
CONAN_LOGGING_LEVEL: ${{ inputs.conan_logging_level }}
CONAN_NON_INTERACTIVE: 1
permissions: {}
permissions: { }
jobs:
conan-recipe-version:
permissions:
@ -103,18 +103,23 @@ jobs:
sudo apt update
sudo apt upgrade
sudo apt install efibootmgr build-essential checkinstall libegl-dev zlib1g-dev libssl-dev ninja-build autoconf libx11-dev libx11-xcb-dev libfontenc-dev libice-dev libsm-dev libxau-dev libxaw7-dev libxcomposite-dev libxcursor-dev libxdamage-dev libxdmcp-dev libxext-dev libxfixes-dev libxi-dev libxinerama-dev libxkbfile-dev libxmu-dev libxmuu-dev libxpm-dev libxrandr-dev libxrender-dev libxres-dev libxss-dev libxt-dev libxtst-dev libxv-dev libxvmc-dev libxxf86vm-dev xtrans-dev libxcb-render0-dev libxcb-render-util0-dev libxcb-xkb-dev libxcb-icccm4-dev libxcb-image0-dev libxcb-keysyms1-dev libxcb-randr0-dev libxcb-shape0-dev libxcb-sync-dev libxcb-xfixes0-dev libxcb-xinerama0-dev xkb-data libxcb-dri3-dev uuid-dev libxcb-util-dev libxkbcommon-x11-dev pkg-config flex bison g++-12 gcc-12 -y
sudo apt install g++-12 gcc-12 -y
sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-12 12
sudo update-alternatives --install /usr/bin/g++ g++ /usr/bin/g++-12 12
- name: Install GCC-13
run: |
sudo apt install g++-13 gcc-13 -y
sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-13 13
sudo update-alternatives --install /usr/bin/g++ g++ /usr/bin/g++-13 13
- name: Create the default Conan profile
run: conan profile new default --detect --force
- name: Get Conan configuration
run: conan config install https://github.com/Ultimaker/conan-config.git
run: |
conan config install https://github.com/Ultimaker/conan-config.git
conan config install https://github.com/Ultimaker/conan-config.git -a "-b runner/${{ runner.os }}/${{ runner.arch }}"
- name: Create the Packages
run: conan create . ${{ needs.conan-recipe-version.outputs.recipe_id_full }} --build=missing --update -o ${{ needs.conan-recipe-version.outputs.project_name }}:devtools=True
run: conan create . ${{ needs.conan-recipe-version.outputs.recipe_id_full }} --build=missing --update -o ${{ needs.conan-recipe-version.outputs.project_name }}:devtools=True -c tools.build:skip_test=True
- name: Create the latest alias
if: always()

View file

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

View file

@ -1,151 +0,0 @@
name: Cura All Installers
run-name: ${{ inputs.cura_conan_version }} for exe ${{ inputs.build_windows_exe }}, msi ${{ inputs.build_windows_msi }}, dmg ${{ inputs.build_macos }}, pkg ${{ inputs.build_macos_installer }}, appimage ${{ inputs.build_linux }} - enterprise ${{ inputs.enterprise }}
on:
workflow_dispatch:
inputs:
cura_conan_version:
description: 'Cura Conan Version'
default: 'cura/latest@ultimaker/testing'
required: true
type: string
conan_args:
description: 'Conan args: eq.: --require-override'
default: ''
required: false
type: string
conan_config:
description: 'Conan config branch to use'
default: ''
required: false
type: string
enterprise:
description: 'Build Cura as an Enterprise edition'
default: false
required: true
type: boolean
staging:
description: 'Use staging API'
default: false
required: true
type: boolean
installer:
description: 'Create the installer'
default: true
required: true
type: boolean
build_windows_exe:
description: 'Build for Windows exe'
default: false
required: true
type: boolean
build_windows_msi:
description: 'Build for msi+pkg'
default: true
required: true
type: boolean
build_linux:
description: 'Build for Linux'
default: true
required: true
type: boolean
build_macos:
description: 'Build dmg for MacOS'
default: true
required: true
type: boolean
# Run the nightly at 3:25 UTC on working days
schedule:
- cron: '25 3 * * 1-5'
jobs:
windows-installer-create-exe:
if: ${{ inputs.build_windows_exe }}
uses: ./.github/workflows/cura-installer.yml
with:
platform: 'windows-2022'
os_name: 'win64'
cura_conan_version: ${{ inputs.cura_conan_version }}
conan_args: ${{ inputs.conan_args }}
conan_config: ${{ inputs.conan_config }}
enterprise: ${{ inputs.enterprise }}
staging: ${{ inputs.staging }}
installer: ${{ inputs.installer }}
msi_installer: false
secrets: inherit
windows-installer-create-msi:
if: ${{ inputs.build_windows_msi }}
uses: ./.github/workflows/cura-installer.yml
with:
platform: 'windows-2022'
os_name: 'win64'
cura_conan_version: ${{ inputs.cura_conan_version }}
conan_args: ${{ inputs.conan_args }}
conan_config: ${{ inputs.conan_config }}
enterprise: ${{ inputs.enterprise }}
staging: ${{ inputs.staging }}
installer: ${{ inputs.installer }}
msi_installer: true
secrets: inherit
linux-installer-create:
if: ${{ inputs.build_linux }}
uses: ./.github/workflows/cura-installer.yml
with:
platform: 'ubuntu-20.04'
os_name: 'linux'
cura_conan_version: ${{ inputs.cura_conan_version }}
conan_args: ${{ inputs.conan_args }}
conan_config: ${{ inputs.conan_config }}
enterprise: ${{ inputs.enterprise }}
staging: ${{ inputs.staging }}
installer: ${{ inputs.installer }}
msi_installer: false
secrets: inherit
linux-modern-installer-create:
if: ${{ inputs.build_linux }}
uses: ./.github/workflows/cura-installer.yml
with:
platform: 'ubuntu-22.04'
os_name: 'linux-modern'
cura_conan_version: ${{ inputs.cura_conan_version }}
conan_args: ${{ inputs.conan_args }}
conan_config: ${{ inputs.conan_config }}
enterprise: ${{ inputs.enterprise }}
staging: ${{ inputs.staging }}
installer: ${{ inputs.installer }}
msi_installer: false
secrets: inherit
macos-dmg-create:
if: ${{ inputs.build_macos }}
uses: ./.github/workflows/cura-installer.yml
with:
platform: 'macos-11'
os_name: 'mac'
cura_conan_version: ${{ inputs.cura_conan_version }}
conan_args: ${{ inputs.conan_args }}
conan_config: ${{ inputs.conan_config }}
enterprise: ${{ inputs.enterprise }}
staging: ${{ inputs.staging }}
installer: ${{ inputs.installer }}
msi_installer: false
secrets: inherit
macos-installer-create:
if: ${{ inputs.build_macos }}
uses: ./.github/workflows/cura-installer.yml
with:
platform: 'macos-11'
os_name: 'mac'
cura_conan_version: ${{ inputs.cura_conan_version }}
conan_args: ${{ inputs.conan_args }}
conan_config: ${{ inputs.conan_config }}
enterprise: ${{ inputs.enterprise }}
staging: ${{ inputs.staging }}
installer: ${{ inputs.installer }}
msi_installer: true
secrets: inherit

View file

@ -1,372 +0,0 @@
name: Cura Installer
run-name: ${{ inputs.cura_conan_version }} for ${{ inputs.platform }} by @${{ github.actor }}
on:
workflow_call:
inputs:
platform:
description: 'Selected Installer OS'
default: 'ubuntu-20.04'
required: true
type: string
os_name:
description: 'OS Friendly Name'
default: 'linux'
required: true
type: string
cura_conan_version:
description: 'Cura Conan Version'
default: 'cura/latest@ultimaker/testing'
required: true
type: string
conan_args:
description: 'Conan args: eq.: --require-override'
default: ''
required: false
type: string
conan_config:
description: 'Conan config branch to use'
default: ''
required: false
type: string
enterprise:
description: 'Build Cura as an Enterprise edition'
default: false
required: true
type: boolean
staging:
description: 'Use staging API'
default: false
required: true
type: boolean
installer:
description: 'Create the installer'
default: true
required: true
type: boolean
msi_installer:
description: 'Create the msi'
default: false
required: true
type: boolean
env:
CONAN_LOGIN_USERNAME_CURA: ${{ secrets.CONAN_USER }}
CONAN_PASSWORD_CURA: ${{ secrets.CONAN_PASS }}
CONAN_LOGIN_USERNAME_CURA_CE: ${{ secrets.CONAN_USER }}
CONAN_PASSWORD_CURA_CE: ${{ secrets.CONAN_PASS }}
CONAN_LOG_RUN_TO_OUTPUT: 1
CONAN_LOGGING_LEVEL: ${{ inputs.conan_logging_level }}
CONAN_NON_INTERACTIVE: 1
CODESIGN_IDENTITY: ${{ secrets.CODESIGN_IDENTITY }}
MAC_NOTARIZE_USER: ${{ secrets.MAC_NOTARIZE_USER }}
MAC_NOTARIZE_PASS: ${{ secrets.MAC_NOTARIZE_PASS }}
MACOS_CERT_P12: ${{ secrets.MACOS_CERT_P12 }}
MACOS_CERT_INSTALLER_P12: ${{ secrets.MACOS_CERT_INSTALLER_P12 }}
MACOS_CERT_USER: ${{ secrets.MACOS_CERT_USER }}
GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }}
MACOS_CERT_PASSPHRASE: ${{ secrets.MACOS_CERT_PASSPHRASE }}
WIN_CERT_INSTALLER_CER: ${{ secrets.WIN_CERT_INSTALLER_CER }}
WIN_CERT_INSTALLER_CER_PASS: ${{ secrets.WIN_CERT_INSTALLER_CER_PASS }}
CURA_CONAN_VERSION: ${{ inputs.cura_conan_version }}
ENTERPRISE: ${{ inputs.enterprise }}
STAGING: ${{ inputs.staging }}
jobs:
cura-installer-create:
runs-on: ${{ inputs.platform }}
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Setup Python and pip
uses: actions/setup-python@v4
with:
python-version: '3.10.x'
cache: 'pip'
cache-dependency-path: .github/workflows/requirements-conan-package.txt
- name: Install Python requirements for runner
run: pip install -r https://raw.githubusercontent.com/Ultimaker/Cura/main/.github/workflows/requirements-conan-package.txt
# Note the runner requirements are always installed from the main branch in the Ultimaker/Cura repo
- name: Use Conan download cache (Bash)
if: ${{ runner.os != 'Windows' }}
run: conan config set storage.download_cache="$HOME/.conan/conan_download_cache"
- name: Use Conan download cache (Powershell)
if: ${{ runner.os == 'Windows' }}
run: conan config set storage.download_cache="C:\Users\runneradmin\.conan\conan_download_cache"
- name: Cache Conan local repository packages (Bash)
uses: actions/cache@v3
if: ${{ runner.os != 'Windows' }}
with:
path: |
$HOME/.conan/data
$HOME/.conan/conan_download_cache
key: conan-${{ runner.os }}-${{ runner.arch }}-installer-cache
- name: Cache Conan local repository packages (Powershell)
uses: actions/cache@v3
if: ${{ runner.os == 'Windows' }}
with:
path: |
C:\Users\runneradmin\.conan\data
C:\.conan
C:\Users\runneradmin\.conan\conan_download_cache
key: conan-${{ runner.os }}-${{ runner.arch }}-installer-cache
- name: Install MacOS system requirements
if: ${{ runner.os == 'Macos' }}
run: brew install autoconf automake ninja create-dmg # Delete create-dmg when deprecating dmg
- name: Hack needed specifically for ubuntu-22.04 from mid-Feb 2023 onwards
if: ${{ runner.os == 'Linux' && startsWith(inputs.platform, '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
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
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.platform, '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.platform, 'ubuntu-20.04') }}
run: |
sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-10 10
sudo update-alternatives --install /usr/bin/g++ g++ /usr/bin/g++-10 10
- name: Create the default Conan profile
run: conan profile new default --detect
- name: Configure GPG Key Linux (Bash)
if: ${{ runner.os == 'Linux' }}
run: echo -n "$GPG_PRIVATE_KEY" | base64 --decode | gpg --import
- name: Configure Macos keychain Developer Cert(Bash)
id: macos-keychain-developer-cert
if: ${{ runner.os == 'Macos' }}
uses: apple-actions/import-codesign-certs@v1
with:
keychain-password: ${{ secrets.MACOS_KEYCHAIN_PASSWORD }}
p12-file-base64: ${{ secrets.MACOS_CERT_P12 }}
p12-password: ${{ secrets.MACOS_CERT_PASSPHRASE }}
- name: Configure Macos keychain Installer Cert (Bash)
id: macos-keychain-installer-cert
if: ${{ runner.os == 'Macos' }}
uses: apple-actions/import-codesign-certs@v1
with:
keychain-password: ${{ secrets.MACOS_KEYCHAIN_PASSWORD }}
create-keychain: false # keychain is created in previous use of action.
p12-file-base64: ${{ secrets.MACOS_CERT_INSTALLER_P12 }}
p12-password: ${{ secrets.MACOS_CERT_PASSPHRASE }}
- name: Create PFX certificate from BASE64_PFX_CONTENT secret
if: ${{ runner.os == 'Windows' }}
id: create-pfx
env:
PFX_CONTENT: ${{ secrets.WIN_CERT_INSTALLER_CER }}
run: |
$pfxPath = Join-Path -Path $env:RUNNER_TEMP -ChildPath "cert.pfx";
$encodedBytes = [System.Convert]::FromBase64String($env:PFX_CONTENT);
Set-Content $pfxPath -Value $encodedBytes -AsByteStream;
echo "PFX_PATH=$pfxPath" >> $env:GITHUB_OUTPUT;
- name: Get Conan configuration from branch
if: ${{ inputs.conan_config != '' }}
run: conan config install https://github.com/Ultimaker/conan-config.git -a "-b ${{ inputs.conan_config }}"
- name: Get Conan configuration
if: ${{ inputs.conan_config == '' }}
run: conan config install https://github.com/Ultimaker/conan-config.git
- name: Create the Packages (Bash)
if: ${{ runner.os != 'Windows' }}
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: Create the Packages (Powershell)
if: ${{ runner.os == 'Windows' }}
run: conan install $Env:CURA_CONAN_VERSION ${{ inputs.conan_args }} --build=missing --update -if cura_inst -g VirtualPythonEnv -o cura:enterprise=$Env:ENTERPRISE -o cura:staging=$Env:STAGING --json "cura_inst/conan_install_info.json"
- name: Set Environment variables for Cura (bash)
if: ${{ runner.os != 'Windows' }}
run: |
. ./cura_inst/bin/activate_github_actions_env.sh
. ./cura_inst/bin/activate_github_actions_version_env.sh
- name: Set Environment variables for Cura (Powershell)
if: ${{ runner.os == 'Windows' }}
run: |
echo "${Env:WIX}\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
.\cura_inst\Scripts\activate_github_actions_env.ps1
.\cura_inst\Scripts\activate_github_actions_version_env.ps1
- name: Unlock Macos keychain (Bash)
if: ${{ runner.os == 'Macos' }}
run: security unlock -p $TEMP_KEYCHAIN_PASSWORD signing_temp.keychain
env:
TEMP_KEYCHAIN_PASSWORD: ${{ steps.macos-keychain-developer-cert.outputs.keychain-password }}
# 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)
if: ${{ runner.os != 'Windows' }}
run: |
cp ./openssl/lib/*.so* ./cura_inst/bin/ || true
cp ./openssl/lib/*.dylib* ./cura_inst/bin/ || true
- name: Copy OpenSSL shared (Powershell)
if: ${{ runner.os == 'Windows' }}
run: |
cp openssl/bin/*.dll ./cura_inst/Scripts/
cp openssl/lib/*.lib ./cura_inst/Lib/
- 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 ""
installer_filename = f"UltiMaker-Cura-{os.getenv('CURA_VERSION_FULL')}{enterprise}-${{ inputs.os_name }}"
if "${{ runner.os }}" == "Windows":
installer_ext = "msi" if "${{ inputs.msi_installer }}" == "true" else "exe"
elif "${{ runner.os }}" == "macOS":
installer_ext = "pkg" if "${{ inputs.msi_installer }}" == "true" else "dmg"
else:
installer_ext = "AppImage"
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")
f.writelines(f"INSTALLER_EXT={installer_ext}\n")
f.writelines(f"FULL_INSTALLER_FILENAME={installer_filename}.{installer_ext}\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.FULL_INSTALLER_FILENAME }} uses:\n")
for dep in sorted_deps:
f.writelines(f"`{dep}`\n")
- name: Archive the artifacts (bash)
if: ${{ !inputs.installer && runner.os != 'Windows' }}
run: tar -zcf "./${{ steps.filename.outputs.INSTALLER_FILENAME }}.tar.gz" "./UltiMaker-Cura/"
working-directory: dist
- name: Archive the artifacts (Powershell)
if: ${{ !inputs.installer && runner.os == 'Windows' }}
run: Compress-Archive -Path ".\UltiMaker-Cura" -DestinationPath ".\${{ steps.filename.outputs.INSTALLER_FILENAME }}.zip"
working-directory: dist
- name: Create the Windows exe installer (Powershell)
if: ${{ inputs.installer && runner.os == 'Windows' && !inputs.msi_installer }}
run: |
python ..\cura_inst\packaging\NSIS\create_windows_installer.py ../cura_inst . "${{ steps.filename.outputs.FULL_INSTALLER_FILENAME }}"
working-directory: dist
- name: Create the Windows msi installer (Powershell)
if: ${{ inputs.installer && runner.os == 'Windows' && inputs.msi_installer }}
run: |
python ..\cura_inst\packaging\msi\create_windows_msi.py ..\cura_inst .\UltiMaker-Cura "${{ steps.filename.outputs.FULL_INSTALLER_FILENAME }}" "$Env:CURA_APP_NAME"
working-directory: dist
- name: Sign the Windows exe installer (Powershell)
if: ${{ inputs.installer && runner.os == 'Windows' && !inputs.msi_installer }}
env:
PFX_PATH: ${{ steps.create-pfx.outputs.PFX_PATH }}
run: |
& "C:/Program Files (x86)/Windows Kits/10/bin/10.0.17763.0/x86/signtool.exe" sign /f $Env:PFX_PATH /p "$Env:WIN_CERT_INSTALLER_CER_PASS" /fd SHA256 /t http://timestamp.digicert.com "${{ steps.filename.outputs.FULL_INSTALLER_FILENAME }}"
working-directory: dist
- name: Sign the Windows msi installer (Powershell)
if: ${{ inputs.installer && runner.os == 'Windows' && inputs.msi_installer }}
env:
PFX_PATH: ${{ steps.create-pfx.outputs.PFX_PATH }}
run: |
& "C:/Program Files (x86)/Windows Kits/10/bin/10.0.17763.0/x86/signtool.exe" sign /f $Env:PFX_PATH /p "$Env:WIN_CERT_INSTALLER_CER_PASS" /fd SHA256 /t http://timestamp.digicert.com "${{ steps.filename.outputs.FULL_INSTALLER_FILENAME }}"
working-directory: dist
- name: Create the Linux AppImage (Bash)
if: ${{ inputs.installer && runner.os == 'Linux' }}
run: python ../cura_inst/packaging/AppImage/create_appimage.py ./UltiMaker-Cura $CURA_VERSION_FULL "${{ steps.filename.outputs.FULL_INSTALLER_FILENAME }}"
working-directory: dist
- name: Create the MacOS dmg and/or pkg (Bash)
if: ${{ github.event.inputs.installer == 'true' && runner.os == 'Macos' }}
run: python ../cura_inst/packaging/MacOS/build_macos.py ../cura_inst . $CURA_CONAN_VERSION "${{ steps.filename.outputs.FULL_INSTALLER_FILENAME }}" "$CURA_APP_NAME"
working-directory: dist
- name: Upload the artifacts
uses: actions/upload-artifact@v3
with:
name: ${{ steps.filename.outputs.INSTALLER_FILENAME }}-${{ steps.filename.outputs.INSTALLER_EXT }}
path: |
dist/*.tar.gz
dist/*.zip
dist/${{ steps.filename.outputs.FULL_INSTALLER_FILENAME }}
dist/*.asc
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

235
.github/workflows/installers.yml vendored Normal file
View file

@ -0,0 +1,235 @@
name: All installers
run-name: ${{ inputs.cura_conan_version }} by @${{ github.actor }}
on:
workflow_dispatch:
inputs:
cura_conan_version:
description: 'Cura Conan Version'
default: 'cura/latest@ultimaker/testing'
required: true
type: string
conan_args:
description: 'Conan args: eq.: --require-override'
default: ''
required: false
type: string
enterprise:
description: 'Build Cura as an Enterprise edition'
default: false
required: true
type: boolean
staging:
description: 'Use staging API'
default: false
required: true
type: boolean
nightly:
description: 'Upload to nightly release'
default: false
required: true
type: boolean
schedule:
# Daily at 8:10 CET
- cron: '10 7 * * *'
env:
CURA_CONAN_VERSION: ${{ inputs.cura_conan_version || 'cura/latest@ultimaker/testing' }}
CONAN_ARGS: ${{ inputs.conan_args || '' }}
ENTERPRISE: ${{ inputs.enterprise || false }}
STAGING: ${{ inputs.staging || false }}
jobs:
windows-installer:
uses: ./.github/workflows/windows.yml
with:
cura_conan_version: ${{ github.event.inputs.cura_conan_version }}
conan_args: ${{ github.event.inputs.conan_args }}
enterprise: ${{ github.event.inputs.enterprise == 'true' }}
staging: ${{ github.event.inputs.staging == 'true' }}
architecture: X64
operating_system: windows-2022
secrets: inherit
linux-installer:
uses: ./.github/workflows/linux.yml
with:
cura_conan_version: ${{ github.event.inputs.cura_conan_version }}
conan_args: ${{ github.event.inputs.conan_args }}
enterprise: ${{ github.event.inputs.enterprise == 'true' }}
staging: ${{ github.event.inputs.staging == 'true' }}
architecture: X64
operating_system: ubuntu-22.04
secrets: inherit
macos-installer:
uses: ./.github/workflows/macos.yml
with:
cura_conan_version: ${{ github.event.inputs.cura_conan_version }}
conan_args: ${{ github.event.inputs.conan_args }}
enterprise: ${{ github.event.inputs.enterprise == 'true' }}
staging: ${{ github.event.inputs.staging == 'true' }}
architecture: X64
operating_system: macos-11.0
secrets: inherit
# macos-arm-installer:
# uses: ./.github/workflows/macos.yml
# with:
# cura_conan_version: ${{ github.event.inputs.cura_conan_version }}
# conan_args: ${{ github.event.inputs.conan_args }}
# enterprise: ${{ github.event.inputs.enterprise == 'true' }}
# staging: ${{ github.event.inputs.staging == 'true' }}
# architecture: ARM64
# operating_system: self-hosted
# secrets: inherit
# Run and update nightly release when the nightly input is set to true or if the schedule is triggered
update-nightly-release:
if: ${{ always() && (! cancelled()) && contains(needs.*.result, 'success') && (! contains(needs.*.result, 'failure')) && (inputs.nightly || github.event_name == 'schedule') }}
runs-on: ubuntu-latest
needs: [ windows-installer, linux-installer, macos-installer ]
# needs: [ windows-installer, linux-installer, macos-installer, macos-arm-installer ]
steps:
- name: Checkout
uses: actions/checkout@v3
# It's not necessary to download all three, but it does make sure we have at least one if an OS is skipped.
- name: Download the run info
uses: actions/download-artifact@v2
with:
name: linux-run-info
- name: Set the run info as environment variables
run: |
. run_info.sh
- name: Output the name file name and extension
id: filename
shell: python
run: |
import os
import datetime
enterprise = "-Enterprise" if "${{ github.event.inputs.enterprise }}" == "true" else ""
linux = f"UltiMaker-Cura-{os.getenv('CURA_VERSION_FULL')}{enterprise}-linux-X64"
mac_x64_dmg = f"UltiMaker-Cura-{os.getenv('CURA_VERSION_FULL')}{enterprise}-macos-X64"
mac_x64_pkg = f"UltiMaker-Cura-{os.getenv('CURA_VERSION_FULL')}{enterprise}-macos-X64"
mac_arm_dmg = f"UltiMaker-Cura-{os.getenv('CURA_VERSION_FULL')}{enterprise}-macos-ARM64"
mac_arm_pkg = f"UltiMaker-Cura-{os.getenv('CURA_VERSION_FULL')}{enterprise}-macos-ARM64"
win_msi = installer_filename = f"UltiMaker-Cura-{os.getenv('CURA_VERSION_FULL')}{enterprise}-win64-X64"
win_exe = installer_filename = f"UltiMaker-Cura-{os.getenv('CURA_VERSION_FULL')}{enterprise}-win64-X64"
nightly_name = "UltiMaker-Cura-" + os.getenv('CURA_VERSION_FULL').split("+")[0]
nightly_creation_time = str(datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"))
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"LINUX={linux}\n")
f.writelines(f"MAC_X64_DMG={mac_x64_dmg}\n")
f.writelines(f"MAC_X64_PKG={mac_x64_pkg}\n")
f.writelines(f"MAC_ARM_DMG={mac_arm_dmg}\n")
f.writelines(f"MAC_ARM_PKG={mac_arm_pkg}\n")
f.writelines(f"WIN_MSI={win_msi}\n")
f.writelines(f"WIN_EXE={win_exe}\n")
f.writelines(f"NIGHTLY_NAME={nightly_name}\n")
f.writelines(f"NIGHTLY_TIME={nightly_creation_time}\n")
- name: Download linux installer jobs artifacts
uses: actions/download-artifact@v2
with:
name: ${{ steps.filename.outputs.LINUX }}-AppImage
path: installers
- name: Rename Linux installer to nightlies
run: |
mv installers/${{ steps.filename.outputs.LINUX }}.AppImage installers/${{ steps.filename.outputs.NIGHTLY_NAME }}-linux-X64.AppImage
- name: Update nightly release for Linux
run: |
gh release upload nightly installers/${{ steps.filename.outputs.NIGHTLY_NAME }}-linux-X64.AppImage --clobber
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Download win msi installer jobs artifacts
uses: actions/download-artifact@v2
with:
name: ${{ steps.filename.outputs.WIN_MSI }}-msi
path: installers
- name: Download win exe installer jobs artifacts
uses: actions/download-artifact@v2
with:
name: ${{ steps.filename.outputs.WIN_EXE }}-exe
path: installers
- name: Rename Windows installers to nightlies
run: |
mv installers/${{ steps.filename.outputs.WIN_MSI }}.msi installers/${{ steps.filename.outputs.NIGHTLY_NAME }}-win64-X64.msi
mv installers/${{ steps.filename.outputs.WIN_EXE }}.exe installers/${{ steps.filename.outputs.NIGHTLY_NAME }}-win64-X64.exe
- name: Update nightly release for Windows
run: |
gh release upload nightly installers/${{ steps.filename.outputs.NIGHTLY_NAME }}-win64-X64.msi --clobber
gh release upload nightly installers/${{ steps.filename.outputs.NIGHTLY_NAME }}-win64-X64.exe --clobber
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Download MacOS (X64) dmg installer jobs artifacts
uses: actions/download-artifact@v2
with:
name: ${{ steps.filename.outputs.MAC_X64_DMG }}-dmg
path: installers
- name: Download MacOS (X64) pkg installer jobs artifacts
uses: actions/download-artifact@v2
with:
name: ${{ steps.filename.outputs.MAC_X64_PKG }}-pkg
path: installers
- name: Rename MacOS (X64) installers to nightlies
run: |
mv installers/${{ steps.filename.outputs.MAC_X64_DMG }}.dmg installers/${{ steps.filename.outputs.NIGHTLY_NAME }}-macos-X64.dmg
mv installers/${{ steps.filename.outputs.MAC_X64_PKG }}.pkg installers/${{ steps.filename.outputs.NIGHTLY_NAME }}-macos-X64.pkg
- name: Update nightly release for MacOS (X64)
run: |
gh release upload nightly installers/${{ steps.filename.outputs.NIGHTLY_NAME }}-macos-X64.dmg --clobber
gh release upload nightly installers/${{ steps.filename.outputs.NIGHTLY_NAME }}-macos-X64.pkg --clobber
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# - name: Download MacOS (ARM-64) dmg installer jobs artifacts
# uses: actions/download-artifact@v2
# with:
# name: ${{ steps.filename.outputs.MAC_ARM_DMG }}-dmg
# path: installers
#
# - name: Download MacOS (ARM-64) pkg installer jobs artifacts
# uses: actions/download-artifact@v2
# with:
# name: ${{ steps.filename.outputs.MAC_ARM_PKG }}-pkg
# path: installers
#
# - name: Rename MacOS (ARM-64) installers to nightlies
# run: |
# mv installers/${{ steps.filename.outputs.MAC_ARM_DMG }}.dmg installers/${{ steps.filename.outputs.NIGHTLY_NAME }}-macos-ARM64.dmg
# mv installers/${{ steps.filename.outputs.MAC_ARM_PKG }}.pkg installers/${{ steps.filename.outputs.NIGHTLY_NAME }}-macos-ARM64.pkg
#
# - name: Update nightly release for MacOS (ARM-64)
# run: |
# gh release upload nightly installers/${{ steps.filename.outputs.NIGHTLY_NAME }}-macos-ARM64.dmg --clobber
# gh release upload nightly installers/${{ steps.filename.outputs.NIGHTLY_NAME }}-macos-ARM64.pkg --clobber
# env:
# GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Update nightly release description (with date)
if: always()
run: |
gh release edit nightly --title "${{ steps.filename.outputs.NIGHTLY_NAME }}" --notes "Nightly release created on: ${{ steps.filename.outputs.NIGHTLY_TIME }}"
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

285
.github/workflows/linux.yml vendored Normal file
View file

@ -0,0 +1,285 @@
name: Linux Installer
run-name: ${{ inputs.cura_conan_version }} for Linux-${{ inputs.architecture }} by @${{ github.actor }}
on:
workflow_dispatch:
inputs:
cura_conan_version:
description: 'Cura Conan Version'
default: 'cura/latest@ultimaker/testing'
required: true
type: string
conan_args:
description: 'Conan args: eq.: --require-override'
default: ''
required: false
type: string
enterprise:
description: 'Build Cura as an Enterprise edition'
default: false
required: true
type: boolean
staging:
description: 'Use staging API'
default: false
required: true
type: boolean
architecture:
description: 'Architecture'
required: true
default: 'X64'
type: choice
options:
- X64
operating_system:
description: 'OS'
required: true
default: 'ubuntu-22.04'
type: choice
options:
- ubuntu-22.04
workflow_call:
inputs:
cura_conan_version:
description: 'Cura Conan Version'
default: 'cura/latest@ultimaker/testing'
required: true
type: string
conan_args:
description: 'Conan args: eq.: --require-override'
default: ''
required: false
type: string
enterprise:
description: 'Build Cura as an Enterprise edition'
default: false
required: true
type: boolean
staging:
description: 'Use staging API'
default: false
required: true
type: boolean
architecture:
description: 'Architecture'
required: true
default: 'X64'
type: string
operating_system:
description: 'OS'
required: true
default: 'ubuntu-22.04'
type: string
env:
CONAN_LOGIN_USERNAME_CURA: ${{ secrets.CONAN_USER }}
CONAN_PASSWORD_CURA: ${{ secrets.CONAN_PASS }}
GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }}
CURA_CONAN_VERSION: ${{ inputs.cura_conan_version }}
ENTERPRISE: ${{ inputs.enterprise }}
STAGING: ${{ inputs.staging }}
jobs:
cura-installer-create:
runs-on: ${{ inputs.operating_system }}
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Setup Python and pip
uses: actions/setup-python@v4
with:
python-version: '3.10.x'
cache: 'pip'
cache-dependency-path: .github/workflows/requirements-conan-package.txt
- name: Install Python requirements for runner
run: pip install -r .github/workflows/requirements-conan-package.txt
- name: Cache Conan local repository packages (Bash)
uses: actions/cache@v3
with:
path: |
$HOME/.conan/data
$HOME/.conan/conan_download_cache
key: conan-${{ runner.os }}-${{ runner.arch }}-installer-cache
- name: Hack needed specifically for ubuntu-22.04 from mid-Feb 2023 onwards
if: ${{ startsWith(inputs.operating_system, 'ubuntu-22.04') }}
run: sudo apt remove libodbc2 libodbcinst2 unixodbc-common -y
# NOTE: Due to what are probably github issues, we have to remove the cache and reconfigure before the rest.
# This is maybe because grub caches the disk it uses last time, which is recreated each time.
- name: Install Linux system requirements
run: |
sudo rm /var/cache/debconf/config.dat
sudo dpkg --configure -a
sudo add-apt-repository ppa:ubuntu-toolchain-r/test -y
sudo apt update
sudo apt upgrade
sudo apt install build-essential checkinstall libegl-dev zlib1g-dev libssl-dev ninja-build autoconf libx11-dev libx11-xcb-dev libfontenc-dev libice-dev libsm-dev libxau-dev libxaw7-dev libxcomposite-dev libxcursor-dev libxdamage-dev libxdmcp-dev libxext-dev libxfixes-dev libxi-dev libxinerama-dev libxkbfile-dev libxmu-dev libxmuu-dev libxpm-dev libxrandr-dev libxrender-dev libxres-dev libxss-dev libxt-dev libxtst-dev libxv-dev libxvmc-dev libxxf86vm-dev xtrans-dev libxcb-render0-dev libxcb-render-util0-dev libxcb-xkb-dev libxcb-icccm4-dev libxcb-image0-dev libxcb-keysyms1-dev libxcb-randr0-dev libxcb-shape0-dev libxcb-sync-dev libxcb-xfixes0-dev libxcb-xinerama0-dev xkb-data libxcb-dri3-dev uuid-dev libxcb-util-dev libxkbcommon-x11-dev pkg-config binutils coreutils desktop-file-utils fakeroot fuse libgdk-pixbuf2.0-dev patchelf squashfs-tools strace util-linux zsync -y
# Get the AppImage tool
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
# Get the AppImage builder
wget --no-check-certificate --quiet -O $GITHUB_WORKSPACE/appimage-builder-x86_64.AppImage https://github.com/AppImageCrafters/appimage-builder/releases/download/v1.1.0/appimage-builder-1.1.0-x86_64.AppImage
chmod +x appimage-builder-x86_64.AppImage
echo "APPIMAGEBUILDER_LOCATION=$GITHUB_WORKSPACE/appimage-builder-x86_64.AppImage" >> $GITHUB_ENV
# Make sure these tools can be found on the path
echo "$GITHUB_WORKSPACE" >> $GITHUB_PATH
- 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 ""
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-builder/create_appimage.py ./UltiMaker-Cura $CURA_VERSION_FULL "${{ steps.filename.outputs.INSTALLER_FILENAME }}.AppImage"
chmod +x "${{ steps.filename.outputs.INSTALLER_FILENAME }}.AppImage"
working-directory: dist
- name: Upload the AppImage
uses: actions/upload-artifact@v3
with:
name: ${{ steps.filename.outputs.INSTALLER_FILENAME }}-AppImage
path: |
dist/${{ steps.filename.outputs.INSTALLER_FILENAME }}.AppImage
retention-days: 5
- name: Write the run info
shell: python
run: |
import os
with open("run_info.sh", "w") as f:
f.writelines(f'echo "CURA_VERSION_FULL={os.environ["CURA_VERSION_FULL"]}" >> $GITHUB_ENV\n')
- name: Upload the run info
uses: actions/upload-artifact@v3
with:
name: linux-run-info
path: |
run_info.sh
retention-days: 5
notify-export:
if: ${{ always() }}
needs: [ cura-installer-create ]
uses: ultimaker/cura/.github/workflows/notify.yml@main
with:
success: ${{ contains(join(needs.*.result, ','), 'success') }}
success_title: "Create the Cura distributions"
success_body: "Installers for ${{ inputs.cura_conan_version }}"
failure_title: "Failed to create the Cura distributions"
failure_body: "Failed to create at least 1 installer for ${{ inputs.cura_conan_version }}"
secrets: inherit

294
.github/workflows/macos.yml vendored Normal file
View file

@ -0,0 +1,294 @@
name: Macos Installer
run-name: ${{ inputs.cura_conan_version }} for Macos-${{ inputs.architecture }} by @${{ github.actor }}
on:
workflow_dispatch:
inputs:
cura_conan_version:
description: 'Cura Conan Version'
default: 'cura/latest@ultimaker/testing'
required: true
type: string
conan_args:
description: 'Conan args: eq.: --require-override'
default: ''
required: false
type: string
enterprise:
description: 'Build Cura as an Enterprise edition'
default: false
required: true
type: boolean
staging:
description: 'Use staging API'
default: false
required: true
type: boolean
architecture:
description: 'Architecture'
required: true
default: 'X64'
type: choice
options:
- X64
- ARM64
operating_system:
description: 'OS'
required: true
default: 'macos-11'
type: choice
options:
- self-hosted
- macos-11
- macos-12
workflow_call:
inputs:
cura_conan_version:
description: 'Cura Conan Version'
default: 'cura/latest@ultimaker/testing'
required: true
type: string
conan_args:
description: 'Conan args: eq.: --require-override'
default: ''
required: false
type: string
enterprise:
description: 'Build Cura as an Enterprise edition'
default: false
required: true
type: boolean
staging:
description: 'Use staging API'
default: false
required: true
type: boolean
architecture:
description: 'Architecture'
required: true
default: 'X64'
type: string
operating_system:
description: 'OS'
required: true
default: 'macos-11'
type: string
env:
CONAN_LOGIN_USERNAME_CURA: ${{ secrets.CONAN_USER }}
CONAN_PASSWORD_CURA: ${{ secrets.CONAN_PASS }}
CODESIGN_IDENTITY: ${{ secrets.CODESIGN_IDENTITY }}
MAC_NOTARIZE_USER: ${{ secrets.MAC_NOTARIZE_USER }}
MAC_NOTARIZE_PASS: ${{ secrets.MAC_NOTARIZE_PASS }}
MACOS_CERT_P12: ${{ secrets.MACOS_CERT_P12 }}
MACOS_CERT_INSTALLER_P12: ${{ secrets.MACOS_CERT_INSTALLER_P12 }}
MACOS_CERT_USER: ${{ secrets.MACOS_CERT_USER }}
MACOS_CERT_PASSPHRASE: ${{ secrets.MACOS_CERT_PASSPHRASE }}
CURA_CONAN_VERSION: ${{ inputs.cura_conan_version }}
ENTERPRISE: ${{ inputs.enterprise }}
STAGING: ${{ inputs.staging }}
jobs:
cura-installer-create:
runs-on: ${{ inputs.operating_system }}
outputs:
INSTALLER_FILENAME: ${{ steps.filename.outputs.INSTALLER_FILENAME }}
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Setup Python and pip
uses: actions/setup-python@v4
with:
python-version: '3.10.x'
cache: 'pip'
cache-dependency-path: .github/workflows/requirements-conan-package.txt
- name: Install Python requirements for runner
run: pip install -r .github/workflows/requirements-conan-package.txt
- name: Cache Conan local repository packages (Bash)
uses: actions/cache@v3
with:
path: |
$HOME/.conan/data
$HOME/.conan/conan_download_cache
key: conan-${{ runner.os }}-${{ runner.arch }}-installer-cache
- name: Install MacOS system requirements
run: brew install cmake autoconf automake ninja create-dmg
- name: Create the default Conan profile
run: conan profile new default --detect --force
- name: Remove Macos keychain (Bash)
run: security delete-keychain signing_temp.keychain || true
- name: Configure Macos keychain Developer Cert(Bash)
id: macos-keychain-developer-cert
uses: apple-actions/import-codesign-certs@v1
with:
keychain-password: ${{ secrets.MACOS_KEYCHAIN_PASSWORD }}
p12-file-base64: ${{ secrets.MACOS_CERT_P12 }}
p12-password: ${{ secrets.MACOS_CERT_PASSPHRASE }}
- name: Configure Macos keychain Installer Cert (Bash)
id: macos-keychain-installer-cert
uses: apple-actions/import-codesign-certs@v1
with:
keychain-password: ${{ secrets.MACOS_KEYCHAIN_PASSWORD }}
create-keychain: false # keychain is created in previous use of action.
p12-file-base64: ${{ secrets.MACOS_CERT_INSTALLER_P12 }}
p12-password: ${{ secrets.MACOS_CERT_PASSPHRASE }}
- 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
- name: Unlock Macos keychain (Bash)
run: security unlock -p $TEMP_KEYCHAIN_PASSWORD signing_temp.keychain
env:
TEMP_KEYCHAIN_PASSWORD: ${{ steps.macos-keychain-developer-cert.outputs.keychain-password }}
# 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 ""
installer_filename = f"UltiMaker-Cura-{os.getenv('CURA_VERSION_FULL')}{enterprise}-macos-${{ 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 Macos dmg (Bash)
run: python ../cura_inst/packaging/MacOS/build_macos.py --source_path ../cura_inst --dist_path . --cura_conan_version $CURA_CONAN_VERSION --filename "${{ steps.filename.outputs.INSTALLER_FILENAME }}" --build_dmg --build_pkg --app_name "$CURA_APP_NAME"
working-directory: dist
- name: Upload the dmg
uses: actions/upload-artifact@v3
with:
name: ${{ steps.filename.outputs.INSTALLER_FILENAME }}-dmg
path: |
dist/${{ steps.filename.outputs.INSTALLER_FILENAME }}.dmg
retention-days: 5
- name: Upload the pkg
uses: actions/upload-artifact@v3
with:
name: ${{ steps.filename.outputs.INSTALLER_FILENAME }}-pkg
path: |
dist/${{ steps.filename.outputs.INSTALLER_FILENAME }}.pkg
retention-days: 5
- name: Write the run info
shell: python
run: |
import os
with open("run_info.sh", "w") as f:
f.writelines(f'echo "CURA_VERSION_FULL={os.environ["CURA_VERSION_FULL"]}" >> $GITHUB_ENV\n')
- name: Upload the run info
uses: actions/upload-artifact@v3
with:
name: macos-run-info
path: |
run_info.sh
retention-days: 5
notify-export:
if: ${{ always() }}
needs: [ cura-installer-create ]
uses: ultimaker/cura/.github/workflows/notify.yml@main
with:
success: ${{ contains(join(needs.*.result, ','), 'success') }}
success_title: "Create the Cura distributions"
success_body: "Installers for ${{ inputs.cura_conan_version }}"
failure_title: "Failed to create the Cura distributions"
failure_body: "Failed to create at least 1 installer for ${{ inputs.cura_conan_version }}"
secrets: inherit

View file

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

View file

@ -17,20 +17,20 @@ jobs:
exempt-issue-labels: 'Status: Triage,Developer Environment :computer:,Status: On Backlog,PR: Community Contribution :crown:,PR: Printer Definitions :factory:,PR: Translations :books:'
stale-issue-label: 'Status: Stale :hourglass:'
labels-to-add-when-unstale: 'Status: Triage'
only-labels: "Type: Bug,Status: Deferred"
only-labels: "Type: New Feature,Status: Deferred"
stale-issue-message: |
Hi 👋,
We are cleaning our list of issues to improve our focus.
This bug seems to be older than a year, which is at least three major Cura releases ago.
This feature request seems to be older than a year, which is at least three major Cura releases ago.
It also received the label Deferred indicating that we did not have time to work on it back then and haven't found time to work on it since.
If this is still a problem for you in the current version of Cura, can you please leave a comment?
If this is still something that you think can improve how you and others use Cura, can you please leave a comment?
We will have a fresh set of eyes to look at it.
If it is not a problem anymore, you don't have to do anything, and this issue will be automatically closed in 14 days.
If it has been resolved or don't need it to be improved anymore, you don't have to do anything, and this issue will be automatically closed in 14 days.
close-issue-message: |
This issue was closed because it has been inactive for 14 days since being marked as stale.
If you encounter this issue and still experience this to be a problem, you are welcome to make a fresh new issue with an updated description and screenshots.
If you encounter this issue and still have a need for this, you are welcome to make a fresh new issue with an updated description.
permissions:
contents: write # only for delete-branch option
issues: write

View file

@ -2,163 +2,165 @@
name: unit-test
on:
push:
paths:
- 'plugins/**'
- 'resources/**'
- 'cura/**'
- 'icons/**'
- 'tests/**'
- 'packaging/**'
- '.github/workflows/conan-*.yml'
- '.github/workflows/unit-test.yml'
- '.github/workflows/notify.yml'
- '.github/workflows/requirements-conan-package.txt'
- 'requirements*.txt'
- 'conanfile.py'
- 'conandata.yml'
- 'GitVersion.yml'
- '*.jinja'
branches:
- main
- 'CURA-*'
- '[1-9]+.[0-9]+'
tags:
- '[0-9]+.[0-9]+.[0-9]+'
- '[0-9]+.[0-9]+-beta'
pull_request:
paths:
- 'plugins/**'
- 'resources/**'
- 'cura/**'
- 'icons/**'
- 'tests/**'
- 'packaging/**'
- '.github/workflows/conan-*.yml'
- '.github/workflows/unit-test.yml'
- '.github/workflows/notify.yml'
- '.github/workflows/requirements-conan-package.txt'
- 'requirements*.txt'
- 'conanfile.py'
- 'conandata.yml'
- 'GitVersion.yml'
- '*.jinja'
branches:
- main
- '[1-9]+.[0-9]+'
tags:
- '[0-9]+.[0-9]+.[0-9]+'
- '[0-9]+.[0-9]+-beta'
push:
paths:
- 'plugins/**'
- 'resources/**'
- 'cura/**'
- 'icons/**'
- 'tests/**'
- 'packaging/**'
- '.github/workflows/conan-*.yml'
- '.github/workflows/unit-test.yml'
- '.github/workflows/notify.yml'
- '.github/workflows/requirements-conan-package.txt'
- 'requirements*.txt'
- 'conanfile.py'
- 'conandata.yml'
- 'GitVersion.yml'
- '*.jinja'
branches:
- main
- 'CURA-*'
- '[1-9]+.[0-9]+'
tags:
- '[0-9]+.[0-9]+.[0-9]+'
- '[0-9]+.[0-9]+-beta'
pull_request:
paths:
- 'plugins/**'
- 'resources/**'
- 'cura/**'
- 'icons/**'
- 'tests/**'
- 'packaging/**'
- '.github/workflows/conan-*.yml'
- '.github/workflows/unit-test.yml'
- '.github/workflows/notify.yml'
- '.github/workflows/requirements-conan-package.txt'
- 'requirements*.txt'
- 'conanfile.py'
- 'conandata.yml'
- 'GitVersion.yml'
- '*.jinja'
branches:
- main
- '[1-9]+.[0-9]+'
tags:
- '[0-9]+.[0-9]+.[0-9]+'
- '[0-9]+.[0-9]+-beta'
env:
CONAN_LOGIN_USERNAME_CURA: ${{ secrets.CONAN_USER }}
CONAN_PASSWORD_CURA: ${{ secrets.CONAN_PASS }}
CONAN_LOGIN_USERNAME_CURA_CE: ${{ secrets.CONAN_USER }}
CONAN_PASSWORD_CURA_CE: ${{ secrets.CONAN_PASS }}
CONAN_LOG_RUN_TO_OUTPUT: 1
CONAN_LOGGING_LEVEL: info
CONAN_NON_INTERACTIVE: 1
CONAN_LOGIN_USERNAME_CURA: ${{ secrets.CONAN_USER }}
CONAN_PASSWORD_CURA: ${{ secrets.CONAN_PASS }}
CONAN_LOGIN_USERNAME_CURA_CE: ${{ secrets.CONAN_USER }}
CONAN_PASSWORD_CURA_CE: ${{ secrets.CONAN_PASS }}
CONAN_LOG_RUN_TO_OUTPUT: 1
CONAN_LOGGING_LEVEL: info
CONAN_NON_INTERACTIVE: 1
permissions:
contents: read
jobs:
conan-recipe-version:
uses: ultimaker/cura/.github/workflows/conan-recipe-version.yml@main
conan-recipe-version:
uses: ultimaker/cura/.github/workflows/conan-recipe-version.yml@main
with:
project_name: cura
testing:
runs-on: ubuntu-22.04
needs: [ conan-recipe-version ]
steps:
- name: Checkout
uses: actions/checkout@v3
with:
project_name: cura
fetch-depth: 2
testing:
runs-on: ubuntu-22.04
needs: [ conan-recipe-version ]
- name: Setup Python and pip
uses: actions/setup-python@v4
with:
python-version: '3.11.x'
architecture: 'x64'
cache: 'pip'
cache-dependency-path: .github/workflows/requirements-conan-package.txt
steps:
- name: Checkout
uses: actions/checkout@v3
with:
fetch-depth: 2
- name: Install Python requirements and Create default Conan profile
run: pip install -r requirements-conan-package.txt
working-directory: .github/workflows/
- name: Setup Python and pip
uses: actions/setup-python@v4
with:
python-version: '3.11.x'
architecture: 'x64'
cache: 'pip'
cache-dependency-path: .github/workflows/requirements-conan-package.txt
- name: Use Conan download cache (Bash)
if: ${{ runner.os != 'Windows' }}
run: conan config set storage.download_cache="$HOME/.conan/conan_download_cache"
- name: Install Python requirements and Create default Conan profile
run: pip install -r requirements-conan-package.txt
working-directory: .github/workflows/
- name: Cache Conan local repository packages (Bash)
uses: actions/cache@v3
if: ${{ runner.os != 'Windows' }}
with:
path: |
$HOME/.conan/data
$HOME/.conan/conan_download_cache
key: conan-${{ runner.os }}-${{ runner.arch }}-unit-cache
- name: Use Conan download cache (Bash)
if: ${{ runner.os != 'Windows' }}
run: conan config set storage.download_cache="$HOME/.conan/conan_download_cache"
# NOTE: Due to what are probably github issues, we have to remove the cache and reconfigure before the rest.
# This is maybe because grub caches the disk it uses last time, which is recreated each time.
- name: Install Linux system requirements
if: ${{ runner.os == 'Linux' }}
run: |
sudo rm /var/cache/debconf/config.dat
sudo dpkg --configure -a
sudo add-apt-repository ppa:ubuntu-toolchain-r/test -y
sudo apt update
sudo apt upgrade
sudo apt install build-essential checkinstall libegl-dev zlib1g-dev libssl-dev ninja-build autoconf libx11-dev libx11-xcb-dev libfontenc-dev libice-dev libsm-dev libxau-dev libxaw7-dev libxcomposite-dev libxcursor-dev libxdamage-dev libxdmcp-dev libxext-dev libxfixes-dev libxi-dev libxinerama-dev libxkbfile-dev libxmu-dev libxmuu-dev libxpm-dev libxrandr-dev libxrender-dev libxres-dev libxss-dev libxt-dev libxtst-dev libxv-dev libxvmc-dev libxxf86vm-dev xtrans-dev libxcb-render0-dev libxcb-render-util0-dev libxcb-xkb-dev libxcb-icccm4-dev libxcb-image0-dev libxcb-keysyms1-dev libxcb-randr0-dev libxcb-shape0-dev libxcb-sync-dev libxcb-xfixes0-dev libxcb-xinerama0-dev xkb-data libxcb-dri3-dev uuid-dev libxcb-util-dev libxkbcommon-x11-dev pkg-config -y
- name: Cache Conan local repository packages (Bash)
uses: actions/cache@v3
if: ${{ runner.os != 'Windows' }}
with:
path: |
$HOME/.conan/data
$HOME/.conan/conan_download_cache
key: conan-${{ runner.os }}-${{ runner.arch }}-unit-cache
- name: Install GCC-13
run: |
sudo apt install g++-13 gcc-13 -y
sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-13 13
sudo update-alternatives --install /usr/bin/g++ g++ /usr/bin/g++-13 13
# NOTE: Due to what are probably github issues, we have to remove the cache and reconfigure before the rest.
# This is maybe because grub caches the disk it uses last time, which is recreated each time.
- name: Install Linux system requirements
if: ${{ runner.os == 'Linux' }}
run: |
sudo rm /var/cache/debconf/config.dat
sudo dpkg --configure -a
sudo add-apt-repository ppa:ubuntu-toolchain-r/test -y
sudo apt update
sudo apt upgrade
sudo apt install build-essential checkinstall libegl-dev zlib1g-dev libssl-dev ninja-build autoconf libx11-dev libx11-xcb-dev libfontenc-dev libice-dev libsm-dev libxau-dev libxaw7-dev libxcomposite-dev libxcursor-dev libxdamage-dev libxdmcp-dev libxext-dev libxfixes-dev libxi-dev libxinerama-dev libxkbfile-dev libxmu-dev libxmuu-dev libxpm-dev libxrandr-dev libxrender-dev libxres-dev libxss-dev libxt-dev libxtst-dev libxv-dev libxvmc-dev libxxf86vm-dev xtrans-dev libxcb-render0-dev libxcb-render-util0-dev libxcb-xkb-dev libxcb-icccm4-dev libxcb-image0-dev libxcb-keysyms1-dev libxcb-randr0-dev libxcb-shape0-dev libxcb-sync-dev libxcb-xfixes0-dev libxcb-xinerama0-dev xkb-data libxcb-dri3-dev uuid-dev libxcb-util-dev libxkbcommon-x11-dev pkg-config -y
- name: Get Conan configuration
run: |
conan config install https://github.com/Ultimaker/conan-config.git
conan config install https://github.com/Ultimaker/conan-config.git -a "-b runner/${{ runner.os }}/${{ runner.arch }}"
- name: Install GCC-12 on ubuntu-22.04
run: |
sudo apt install g++-12 gcc-12 -y
sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-12 12
sudo update-alternatives --install /usr/bin/g++ g++ /usr/bin/g++-12 12
- name: Get Conan configuration
run: conan config install https://github.com/Ultimaker/conan-config.git
- name: Get Conan profile
run: conan profile new default --detect --force
- name: Get Conan profile
run: conan profile new default --detect --force
- name: Install dependencies
run: conan install . ${{ needs.conan-recipe-version.outputs.recipe_id_full }} --build=missing --update -o cura:devtools=True -g VirtualPythonEnv -if venv
- name: Install dependencies
run: conan install . ${{ needs.conan-recipe-version.outputs.recipe_id_full }} --build=missing --update -o cura:devtools=True -g VirtualPythonEnv -if venv
- name: Upload the Dependency package(s)
run: conan upload "*" -r cura --all -c
- name: Upload the Dependency package(s)
run: conan upload "*" -r cura --all -c
- name: Set Environment variables for Cura (bash)
if: ${{ runner.os != 'Windows' }}
run: |
. ./venv/bin/activate_github_actions_env.sh
- name: Set Environment variables for Cura (bash)
if: ${{ runner.os != 'Windows' }}
run: |
. ./venv/bin/activate_github_actions_env.sh
- name: Run Unit Test
id: run-test
run: |
pytest --junitxml=junit_cura.xml
working-directory: tests
- name: Run Unit Test
id: run-test
run: |
pytest --junitxml=junit_cura.xml
working-directory: tests
- name: Save PR metadata
if: always()
run: |
echo ${{ github.event.number }} > pr-id.txt
echo ${{ github.event.pull_request.head.repo.full_name }} > pr-head-repo.txt
echo ${{ github.event.pull_request.head.ref }} > pr-head-ref.txt
working-directory: tests
- name: Save PR metadata
if: always()
run: |
echo ${{ github.event.number }} > pr-id.txt
echo ${{ github.event.pull_request.head.repo.full_name }} > pr-head-repo.txt
echo ${{ github.event.pull_request.head.ref }} > pr-head-ref.txt
working-directory: tests
- name: Upload Test Results
if: always()
uses: actions/upload-artifact@v3
with:
name: test-result
path: |
tests/**/*.xml
tests/pr-id.txt
tests/pr-head-repo.txt
tests/pr-head-ref.txt
- name: Upload Test Results
if: always()
uses: actions/upload-artifact@v3
with:
name: test-result
path: |
tests/**/*.xml
tests/pr-id.txt
tests/pr-head-repo.txt
tests/pr-head-ref.txt

View file

@ -1,75 +1,87 @@
name: update-translations
on:
push:
paths:
- 'plugins/**'
- 'resources/**'
- 'cura/**'
- 'icons/**'
- 'tests/**'
- 'packaging/**'
- '.github/workflows/conan-*.yml'
- '.github/workflows/notify.yml'
- '.github/workflows/requirements-conan-package.txt'
- 'requirements*.txt'
- 'conanfile.py'
- 'conandata.yml'
- 'GitVersion.yml'
- '*.jinja'
push:
paths:
- 'plugins/**'
- 'resources/**'
- 'cura/**'
- 'icons/**'
- 'tests/**'
- 'packaging/**'
- '.github/workflows/conan-*.yml'
- '.github/workflows/notify.yml'
- '.github/workflows/requirements-conan-package.txt'
- 'requirements*.txt'
- 'conanfile.py'
- 'conandata.yml'
- 'GitVersion.yml'
- '*.jinja'
branches:
- '[1-9].[0-9]'
- '[1-9].[0-9][0-9]'
tags:
- '[1-9].[0-9].[0-9]*'
- '[1-9].[0-9].[0-9]'
- '[1-9].[0-9][0-9].[0-9]*'
jobs:
update-translations:
name: Update translations
update-translations:
name: Update translations
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Cache Conan data
id: cache-conan
uses: actions/cache@v3
with:
path: ~/.conan
key: ${{ runner.os }}-conan
- name: Cache Conan data
id: cache-conan
uses: actions/cache@v3
with:
path: ~/.conan
key: ${{ runner.os }}-conan
- name: Setup Python and pip
uses: actions/setup-python@v4
with:
python-version: 3.10.x
cache: pip
cache-dependency-path: .github/workflows/requirements-conan-package.txt
- name: Setup Python and pip
uses: actions/setup-python@v4
with:
python-version: 3.11.x
cache: pip
cache-dependency-path: .github/workflows/requirements-conan-package.txt
- name: Install Python requirements for runner
run: pip install -r .github/workflows/requirements-conan-package.txt
- name: Install Python requirements for runner
run: pip install -r .github/workflows/requirements-conan-package.txt
# NOTE: Due to what are probably github issues, we have to remove the cache and reconfigure before the rest.
# This is maybe because grub caches the disk it uses last time, which is recreated each time.
- name: Install Linux system requirements
if: ${{ runner.os == 'Linux' }}
run: |
sudo rm /var/cache/debconf/config.dat
sudo dpkg --configure -a
sudo add-apt-repository ppa:ubuntu-toolchain-r/test -y
sudo apt update
sudo apt upgrade
sudo apt install efibootmgr build-essential checkinstall libegl-dev zlib1g-dev libssl-dev ninja-build autoconf libx11-dev libx11-xcb-dev libfontenc-dev libice-dev libsm-dev libxau-dev libxaw7-dev libxcomposite-dev libxcursor-dev libxdamage-dev libxdmcp-dev libxext-dev libxfixes-dev libxi-dev libxinerama-dev libxkbfile-dev libxmu-dev libxmuu-dev libxpm-dev libxrandr-dev libxrender-dev libxres-dev libxss-dev libxt-dev libxtst-dev libxv-dev libxvmc-dev libxxf86vm-dev xtrans-dev libxcb-render0-dev libxcb-render-util0-dev libxcb-xkb-dev libxcb-icccm4-dev libxcb-image0-dev libxcb-keysyms1-dev libxcb-randr0-dev libxcb-shape0-dev libxcb-sync-dev libxcb-xfixes0-dev libxcb-xinerama0-dev xkb-data libxcb-dri3-dev uuid-dev libxcb-util-dev libxkbcommon-x11-dev pkg-config flex bison g++-12 gcc-12 -y
sudo apt install g++-12 gcc-12 -y
sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-12 12
sudo update-alternatives --install /usr/bin/g++ g++ /usr/bin/g++-12 12
# NOTE: Due to what are probably github issues, we have to remove the cache and reconfigure before the rest.
# This is maybe because grub caches the disk it uses last time, which is recreated each time.
- name: Install Linux system requirements
if: ${{ runner.os == 'Linux' }}
run: |
sudo rm /var/cache/debconf/config.dat
sudo dpkg --configure -a
sudo add-apt-repository ppa:ubuntu-toolchain-r/test -y
sudo apt update
sudo apt upgrade
sudo apt install efibootmgr build-essential checkinstall libegl-dev zlib1g-dev libssl-dev ninja-build autoconf libx11-dev libx11-xcb-dev libfontenc-dev libice-dev libsm-dev libxau-dev libxaw7-dev libxcomposite-dev libxcursor-dev libxdamage-dev libxdmcp-dev libxext-dev libxfixes-dev libxi-dev libxinerama-dev libxkbfile-dev libxmu-dev libxmuu-dev libxpm-dev libxrandr-dev libxrender-dev libxres-dev libxss-dev libxt-dev libxtst-dev libxv-dev libxvmc-dev libxxf86vm-dev xtrans-dev libxcb-render0-dev libxcb-render-util0-dev libxcb-xkb-dev libxcb-icccm4-dev libxcb-image0-dev libxcb-keysyms1-dev libxcb-randr0-dev libxcb-shape0-dev libxcb-sync-dev libxcb-xfixes0-dev libxcb-xinerama0-dev xkb-data libxcb-dri3-dev uuid-dev libxcb-util-dev libxkbcommon-x11-dev pkg-config flex bison g++-12 gcc-12 -y
- name: Create the default Conan profile
run: conan profile new default --detect --force
- name: Install GCC-13
run: |
sudo apt install g++-13 gcc-13 -y
sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-13 13
sudo update-alternatives --install /usr/bin/g++ g++ /usr/bin/g++-13 13
- name: Get Conan configuration
run: conan config install https://github.com/Ultimaker/conan-config.git
- name: Create the default Conan profile
run: conan profile new default --detect --force
- name: generate the files using Conan install
run: conan install . --build=missing --update -o cura:devtools=True
- name: Get Conan configuration
run: |
conan config install https://github.com/Ultimaker/conan-config.git
conan config install https://github.com/Ultimaker/conan-config.git -a "-b runner/${{ runner.os }}/${{ runner.arch }}"
- uses: stefanzweifel/git-auto-commit-action@v4
with:
file_pattern: resources/i18n/*.po resources/i18n/*.pot
status_options: --untracked-files=no
commit_message: update translations
- name: generate the files using Conan install
run: conan install . --build=missing --update -o cura:devtools=True
- uses: stefanzweifel/git-auto-commit-action@v4
with:
file_pattern: resources/i18n/*.po resources/i18n/*.pot
status_options: --untracked-files=no
commit_message: update translations

287
.github/workflows/windows.yml vendored Normal file
View file

@ -0,0 +1,287 @@
name: Windows Installer
run-name: ${{ inputs.cura_conan_version }} for Windows-${{ inputs.architecture }} by @${{ github.actor }}
on:
workflow_dispatch:
inputs:
cura_conan_version:
description: 'Cura Conan Version'
default: 'cura/latest@ultimaker/testing'
required: true
type: string
conan_args:
description: 'Conan args: eq.: --require-override'
default: ''
required: false
type: string
enterprise:
description: 'Build Cura as an Enterprise edition'
default: false
required: true
type: boolean
staging:
description: 'Use staging API'
default: false
required: true
type: boolean
architecture:
description: 'Architecture'
required: true
default: 'X64'
type: choice
options:
- X64
operating_system:
description: 'OS'
required: true
default: 'windows-2022'
type: choice
options:
- windows-2022
workflow_call:
inputs:
cura_conan_version:
description: 'Cura Conan Version'
default: 'cura/latest@ultimaker/testing'
required: true
type: string
conan_args:
description: 'Conan args: eq.: --require-override'
default: ''
required: false
type: string
enterprise:
description: 'Build Cura as an Enterprise edition'
default: false
required: true
type: boolean
staging:
description: 'Use staging API'
default: false
required: true
type: boolean
architecture:
description: 'Architecture'
required: true
default: 'X64'
type: string
operating_system:
description: 'OS'
required: true
default: 'windows-2022'
type: string
env:
CONAN_LOGIN_USERNAME_CURA: ${{ secrets.CONAN_USER }}
CONAN_PASSWORD_CURA: ${{ secrets.CONAN_PASS }}
WIN_CERT_INSTALLER_CER: ${{ secrets.WIN_CERT_INSTALLER_CER }}
WIN_CERT_INSTALLER_CER_PASS: ${{ secrets.WIN_CERT_INSTALLER_CER_PASS }}
CURA_CONAN_VERSION: ${{ inputs.cura_conan_version }}
ENTERPRISE: ${{ inputs.enterprise }}
STAGING: ${{ inputs.staging }}
jobs:
cura-installer-create:
runs-on: ${{ inputs.operating_system }}
outputs:
INSTALLER_FILENAME: ${{ steps.filename.outputs.INSTALLER_FILENAME }}
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Setup Python and pip
uses: actions/setup-python@v4
with:
python-version: '3.10.x'
cache: 'pip'
cache-dependency-path: .github/workflows/requirements-conan-package.txt
- name: Install Python requirements for runner
run: pip install -r .github/workflows/requirements-conan-package.txt
- name: Cache Conan local repository packages (Powershell)
uses: actions/cache@v3
with:
path: |
C:\Users\runneradmin\.conan\data
C:\.conan
C:\Users\runneradmin\.conan\conan_download_cache
key: conan-${{ runner.os }}-${{ runner.arch }}-installer-cache
- name: Create the default Conan profile
run: conan profile new default --detect --force
- name: Get Conan configuration
run: |
conan config install https://github.com/Ultimaker/conan-config.git
conan config install https://github.com/Ultimaker/conan-config.git -a "-b runner/${{ runner.os }}/${{ runner.arch }}"
- name: Use Conan download cache (Powershell)
run: conan config set storage.download_cache="C:\Users\runneradmin\.conan\conan_download_cache"
- name: Create the Packages (Powershell)
run: conan install $Env:CURA_CONAN_VERSION ${{ inputs.conan_args }} --build=missing --update -if cura_inst -g VirtualPythonEnv -o cura:enterprise=$Env:ENTERPRISE -o cura:staging=$Env: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 (Powershell)
run: |
echo "${Env:WIX}\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
.\cura_inst\Scripts\activate_github_actions_env.ps1
.\cura_inst\Scripts\activate_github_actions_version_env.ps1
- name: Install OpenSSL shared
run: conan install openssl/1.1.1l@_/_ --build=missing --update -o openssl:shared=True -g deploy
- name: Copy OpenSSL shared (Powershell)
run: |
cp openssl/bin/*.dll ./cura_inst/Scripts/
cp openssl/lib/*.lib ./cura_inst/Lib/
- 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 ""
installer_filename = f"UltiMaker-Cura-{os.getenv('CURA_VERSION_FULL')}{enterprise}-win64-${{ 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 PFX certificate from BASE64_PFX_CONTENT secret
id: create-pfx
env:
PFX_CONTENT: ${{ secrets.WIN_CERT_INSTALLER_CER }}
run: |
$pfxPath = Join-Path -Path $env:RUNNER_TEMP -ChildPath "cert.pfx";
$encodedBytes = [System.Convert]::FromBase64String($env:PFX_CONTENT);
Set-Content $pfxPath -Value $encodedBytes -AsByteStream;
echo "PFX_PATH=$pfxPath" >> $env:GITHUB_OUTPUT;
- name: Create the Windows msi installer (Powershell)
run: |
python ..\cura_inst\packaging\msi\create_windows_msi.py ..\cura_inst .\UltiMaker-Cura "${{steps.filename.outputs.INSTALLER_FILENAME }}.msi" "$Env:CURA_APP_NAME"
working-directory: dist
- name: Sign the Windows msi installer (Powershell)
env:
PFX_PATH: ${{ steps.create-pfx.outputs.PFX_PATH }}
run: |
& "C:/Program Files (x86)/Windows Kits/10/bin/10.0.17763.0/x86/signtool.exe" sign /f $Env:PFX_PATH /p "$Env:WIN_CERT_INSTALLER_CER_PASS" /fd SHA256 /t http://timestamp.digicert.com "${{steps.filename.outputs.INSTALLER_FILENAME }}.msi"
working-directory: dist
- name: Create the Windows exe installer (Powershell)
run: |
python ..\cura_inst\packaging\NSIS\create_windows_installer.py ../cura_inst . "${{steps.filename.outputs.INSTALLER_FILENAME }}.exe"
working-directory: dist
- name: Sign the Windows exe installer (Powershell)
env:
PFX_PATH: ${{ steps.create-pfx.outputs.PFX_PATH }}
run: |
& "C:/Program Files (x86)/Windows Kits/10/bin/10.0.17763.0/x86/signtool.exe" sign /f $Env:PFX_PATH /p "$Env:WIN_CERT_INSTALLER_CER_PASS" /fd SHA256 /t http://timestamp.digicert.com "${{steps.filename.outputs.INSTALLER_FILENAME }}.exe"
working-directory: dist
- name: Upload the msi
uses: actions/upload-artifact@v3
with:
name: ${{steps.filename.outputs.INSTALLER_FILENAME }}-msi
path: |
dist/${{steps.filename.outputs.INSTALLER_FILENAME }}.msi
retention-days: 5
- name: Upload the exe
uses: actions/upload-artifact@v3
with:
name: ${{steps.filename.outputs.INSTALLER_FILENAME }}-exe
path: |
dist/${{steps.filename.outputs.INSTALLER_FILENAME }}.exe
retention-days: 5
# NOTE: The extension is .sh, since this isn't going to build-environment, so not on the Win build image.
- name: Write the run info
shell: python
run: |
import os
with open("run_info.sh", "w") as f:
f.writelines(f'echo "CURA_VERSION_FULL={os.environ["CURA_VERSION_FULL"]}" >> $GITHUB_ENV\n')
# NOTE: The extension is .sh, since this isn't going to build-environment, so not on the Win build image.
- name: Upload the run info
uses: actions/upload-artifact@v3
with:
name: windows-run-info
path: |
run_info.sh
retention-days: 5
notify-export:
if: ${{ always() }}
needs: [ cura-installer-create ]
uses: ultimaker/cura/.github/workflows/notify.yml@main
with:
success: ${{ contains(join(needs.*.result, ','), 'success') }}
success_title: "Create the Cura distributions"
success_body: "Installers for ${{ inputs.cura_conan_version }}"
failure_title: "Failed to create the Cura distributions"
failure_body: "Failed to create at least 1 installer for ${{ inputs.cura_conan_version }}"
secrets: inherit

3
.gitignore vendored
View file

@ -101,3 +101,6 @@ graph_info.json
Ultimaker-Cura.spec
.run/
/printer-linter/src/printerlinter.egg-info/
/resources/qml/Dialogs/AboutDialogVersionsList.qml
/plugins/CuraEngineGradualFlow
/resources/bundled_packages/bundled_*.json

View file

@ -0,0 +1,61 @@
import QtQuick 2.2
import QtQuick.Controls 2.9
import UM 1.6 as UM
import Cura 1.5 as Cura
ListView
{
id: projectBuildInfoList
visible: false
anchors.top: creditsNotes.bottom
anchors.topMargin: UM.Theme.getSize("default_margin").height
width: parent.width
height: base.height - y - (2 * UM.Theme.getSize("default_margin").height + closeButton.height)
ScrollBar.vertical: UM.ScrollBar
{
id: projectBuildInfoListScrollBar
}
delegate: Row
{
spacing: UM.Theme.getSize("narrow_margin").width
UM.Label
{
text: (model.name)
width: (projectBuildInfoList.width* 0.4) | 0
elide: Text.ElideRight
}
UM.Label
{
text: (model.version)
width: (projectBuildInfoList.width *0.6) | 0
elide: Text.ElideRight
}
}
model: ListModel
{
id: developerInfo
}
Component.onCompleted:
{
var conan_installs = {{ conan_installs }};
var python_installs = {{ python_installs }};
developerInfo.append({ name : "<H1>Conan Installs</H1>", version : '' });
for (var n in conan_installs)
{
developerInfo.append({ name : conan_installs[n][0], version : conan_installs[n][1] });
}
developerInfo.append({ name : '', version : '' });
developerInfo.append({ name : "<H1>Python Installs</H1>", version : '' });
for (var n in python_installs)
{
developerInfo.append({ name : python_installs[n][0], version : python_installs[n][1] });
}
}
}

View file

@ -18,8 +18,8 @@ url: "https://ultimaker.com/software/ultimaker-cura"
repository-code: "https://github.com/Ultimaker/Cura"
license: LGPL-3.0
license-url: "https://github.com/Ultimaker/Cura/blob/main/LICENSE"
version: 5.2.1
date-released: "2022-10-19"
version: 5.4.0
date-released: "2023-07-04"
keywords:
- Ultimaker
- Cura

1
FUNDING.yml Normal file
View file

@ -0,0 +1 @@
github: [ultimaker]

View file

@ -12,7 +12,7 @@
[![Badge Test]][Test]
[![Badge Conan]][Conan]
![Badge Downloads]
<br>
<br>
@ -84,6 +84,7 @@
[Badge Conan]: https://img.shields.io/github/workflow/status/Ultimaker/Cura/conan-package?style=for-the-badge&logoColor=white&labelColor=6185aa&color=4c6987&logo=Conan&label=Conan%20Package
[Badge Test]: https://img.shields.io/github/workflow/status/Ultimaker/Cura/unit-test?style=for-the-badge&logoColor=white&labelColor=4a999d&color=346c6e&logo=Codacy&label=Unit%20Test
[Badge Size]: https://img.shields.io/github/repo-size/ultimaker/cura?style=for-the-badge&logoColor=white&labelColor=715a97&color=584674&logo=GoogleAnalytics
[Badge Downloads]: https://img.shields.io/github/downloads-pre/Ultimaker/Cura/latest/total?style=for-the-badge
<!---------------------------------[ Buttons ]--------------------------------->

View file

@ -19,6 +19,14 @@ pyinstaller:
package: "cura"
src: "plugins"
dst: "share/cura/plugins"
curaengine_gradual_flow_plugin:
package: "curaengine_plugin_gradual_flow"
src: "res/plugins/CuraEngineGradualFlow"
dst: "share/cura/plugins/CuraEngineGradualFlow"
curaengine_gradual_flow_plugin_bundled:
package: "curaengine_plugin_gradual_flow"
src: "res/bundled_packages"
dst: "share/cura/resources/bundled_packages"
cura_resources:
package: "cura"
src: "resources"

View file

@ -4,13 +4,13 @@ from pathlib import Path
from jinja2 import Template
from conan import ConanFile
from conan.tools.files import copy, rmdir, save, mkdir
from conan.tools.files import copy, rmdir, save, mkdir, rm
from conan.tools.microsoft import unix_path
from conan.tools.env import VirtualRunEnv, Environment, VirtualBuildEnv
from conan.tools.scm import Version
from conan.errors import ConanInvalidConfiguration, ConanException
required_conan_version = "<=1.56.0"
required_conan_version = ">=1.58.0 <2.0.0"
class CuraConan(ConanFile):
@ -21,12 +21,11 @@ class CuraConan(ConanFile):
description = "3D printer / slicing GUI built on top of the Uranium framework"
topics = ("conan", "python", "pyqt6", "qt", "qml", "3d-printing", "slicer")
build_policy = "missing"
exports = "LICENSE*", "UltiMaker-Cura.spec.jinja", "CuraVersion.py.jinja"
exports = "LICENSE*", "*.jinja"
settings = "os", "compiler", "build_type", "arch"
# FIXME: Remove specific branch once merged to main
python_requires = "umbase/[>=0.1.7]@ultimaker/stable", "translationextractor/[>=2.1.1]@ultimaker/stable"
python_requires_extend = "umbase.UMBaseConanfile"
python_requires = "translationextractor/[>=2.1.1]@ultimaker/stable"
options = {
"enterprise": ["True", "False", "true", "false"], # Workaround for GH Action passing boolean as lowercase string
@ -138,6 +137,37 @@ class CuraConan(ConanFile):
return "'x86_64'"
return "None"
def _generate_about_versions(self, location):
with open(os.path.join(self.recipe_folder, "AboutDialogVersionsList.qml.jinja"), "r") as f:
cura_version_py = Template(f.read())
conan_installs = []
python_installs = []
# list of conan installs
for _, dependency in self.dependencies.host.items():
conan_installs.append([dependency.ref.name,dependency.ref.version])
#list of python installs
outer = '"' if self.settings.os == "Windows" else "'"
inner = "'" if self.settings.os == "Windows" else '"'
python_ins_cmd = f"python -c {outer}import pkg_resources; print({inner};{inner}.join([(s.key+{inner},{inner}+ s.version) for s in pkg_resources.working_set])){outer}"
from six import StringIO
buffer = StringIO()
self.run(python_ins_cmd, run_environment= True, env = "conanrun", output=buffer)
packages = str(buffer.getvalue()).split("-----------------\n")
package = packages[1].strip('\r\n').split(";")
for pack in package:
python_installs.append(pack.split(","))
with open(os.path.join(location, "AboutDialogVersionsList.qml"), "w") as f:
f.write(cura_version_py.render(
conan_installs = conan_installs,
python_installs = python_installs
))
def _generate_cura_version(self, location):
with open(os.path.join(self.recipe_folder, "CuraVersion.py.jinja"), "r") as f:
cura_version_py = Template(f.read())
@ -179,8 +209,8 @@ class CuraConan(ConanFile):
src_path = os.path.join(self.source_folder, data["src"])
else:
src_path = os.path.join(self.deps_cpp_info[data["package"]].rootpath, data["src"])
elif "root" in data: # get the paths relative from the sourcefolder
src_path = os.path.join(self.source_folder, data["root"], data["src"])
elif "root" in data: # get the paths relative from the install folder
src_path = os.path.join(self.install_folder, data["root"], data["src"])
else:
continue
if Path(src_path).exists():
@ -191,7 +221,9 @@ class CuraConan(ConanFile):
if "package" in binary: # get the paths from conan package
src_path = os.path.join(self.deps_cpp_info[binary["package"]].rootpath, binary["src"])
elif "root" in binary: # get the paths relative from the sourcefolder
src_path = os.path.join(self.source_folder, binary["root"], binary["src"])
src_path = str(self.source_path.joinpath(binary["root"], binary["src"]))
if self.settings.os == "Windows":
src_path = src_path.replace("\\", "\\\\")
else:
continue
if not Path(src_path).exists():
@ -262,6 +294,9 @@ class CuraConan(ConanFile):
self.options["pysavitar"].shared = True
self.options["pynest2d"].shared = True
self.options["cpython"].shared = True
self.options["boost"].header_only = True
if self.settings.os == "Linux":
self.options["curaengine_grpc_definitions"].shared = True
def validate(self):
version = self.conf_info.get("user.cura:version", default = self.version, check_type = str)
@ -269,10 +304,14 @@ class CuraConan(ConanFile):
raise ConanInvalidConfiguration("Only versions 5+ are support")
def requirements(self):
self.requires("pyarcus/5.2.2")
self.requires("boost/1.82.0")
self.requires("curaengine_grpc_definitions/latest@ultimaker/testing")
self.requires("zlib/1.2.13")
self.requires("pyarcus/5.3.0")
self.requires("curaengine/(latest)@ultimaker/testing")
self.requires("pysavitar/5.2.2")
self.requires("pynest2d/5.2.2")
self.requires("pysavitar/5.3.0")
self.requires("pynest2d/5.3.0")
self.requires("curaengine_plugin_gradual_flow/(latest)@ultimaker/testing")
self.requires("uranium/(latest)@ultimaker/testing")
self.requires("cura_binary_data/(latest)@ultimaker/testing")
self.requires("cpython/3.10.4")
@ -307,6 +346,47 @@ class CuraConan(ConanFile):
vr.generate()
self._generate_cura_version(os.path.join(self.source_folder, "cura"))
self._generate_about_versions(os.path.join(self.source_folder, "resources","qml", "Dialogs"))
if not self.in_local_cache:
# Copy CuraEngine.exe to bindirs of Virtual Python Environment
curaengine = self.dependencies["curaengine"].cpp_info
copy(self, "CuraEngine.exe", curaengine.bindirs[0], self.source_folder, keep_path = False)
copy(self, "CuraEngine", curaengine.bindirs[0], self.source_folder, keep_path = False)
# Copy the external plugins that we want to bundle with Cura
rmdir(self,str(self.source_path.joinpath("plugins", "CuraEngineGradualFlow")))
curaengine_plugin_gradual_flow = self.dependencies["curaengine_plugin_gradual_flow"].cpp_info
copy(self, "*.py", curaengine_plugin_gradual_flow.resdirs[0], str(self.source_path.joinpath("plugins", "CuraEngineGradualFlow")), keep_path = True)
ext = ".exe" if self.settings.os == "Windows" else ""
copy(self, f"curaengine_plugin_gradual_flow{ext}", curaengine_plugin_gradual_flow.resdirs[0], str(self.source_path.joinpath("plugins", "CuraEngineGradualFlow")), keep_path = True)
copy(self, "*.json", curaengine_plugin_gradual_flow.resdirs[0], str(self.source_path.joinpath("plugins", "CuraEngineGradualFlow")), keep_path = True)
copy(self, "bundled_*.json", curaengine_plugin_gradual_flow.resdirs[1], str(self.source_path.joinpath("resources", "bundled_packages")), keep_path = False)
# Copy resources of cura_binary_data
cura_binary_data = self.dependencies["cura_binary_data"].cpp_info
copy(self, "*", cura_binary_data.resdirs[0], str(self._share_dir.joinpath("cura")), keep_path = True)
copy(self, "*", cura_binary_data.resdirs[1], str(self._share_dir.joinpath("uranium")), keep_path = True)
if self.settings.os == "Windows":
copy(self, "*", cura_binary_data.resdirs[2], str(self._share_dir.joinpath("windows")), keep_path = True)
for dependency in self.dependencies.host.values():
for bindir in dependency.cpp_info.bindirs:
copy(self, "*.dll", bindir, str(self._site_packages), keep_path = False)
for libdir in dependency.cpp_info.libdirs:
copy(self, "*.pyd", libdir, str(self._site_packages), keep_path = False)
copy(self, "*.pyi", libdir, str(self._site_packages), keep_path = False)
copy(self, "*.dylib", libdir, str(self._base_dir.joinpath("lib")), keep_path = False)
# Copy materials (flat)
rmdir(self, os.path.join(self.source_folder, "resources", "materials"))
fdm_materials = self.dependencies["fdm_materials"].cpp_info
copy(self, "*", fdm_materials.resdirs[0], self.source_folder)
# Copy internal resources
if self.options.internal:
cura_private_data = self.dependencies["cura_private_data"].cpp_info
copy(self, "*", cura_private_data.resdirs[0], str(self._share_dir.joinpath("cura")))
if self.options.devtools:
entitlements_file = "'{}'".format(os.path.join(self.source_folder, "packaging", "MacOS", "cura.entitlements"))
@ -336,80 +416,21 @@ class CuraConan(ConanFile):
cpp_info = self.dependencies["gettext"].cpp_info
self.run(f"{cpp_info.bindirs[0]}/msgfmt {po_file} -o {mo_file} -f", env="conanbuild", ignore_errors=True)
def imports(self):
self.copy("CuraEngine.exe", root_package = "curaengine", src = "@bindirs", dst = "", keep_path = False)
self.copy("CuraEngine", root_package = "curaengine", src = "@bindirs", dst = "", keep_path = False)
rmdir(self, os.path.join(self.source_folder, "resources", "materials"))
self.copy("*.fdm_material", root_package = "fdm_materials", src = "@resdirs", dst = "resources/materials", keep_path = False)
self.copy("*.sig", root_package = "fdm_materials", src = "@resdirs", dst = "resources/materials", keep_path = False)
if self.options.internal:
self.copy("*", root_package = "cura_private_data", src = self.deps_cpp_info["cura_private_data"].resdirs[0],
dst = self._share_dir.joinpath("cura", "resources"), keep_path = True)
# Copy resources of cura_binary_data
self.copy("*", root_package = "cura_binary_data", src = self.deps_cpp_info["cura_binary_data"].resdirs[0],
dst = self._share_dir.joinpath("cura", "resources"), keep_path = True)
self.copy("*", root_package = "cura_binary_data", src = self.deps_cpp_info["cura_binary_data"].resdirs[1],
dst =self._share_dir.joinpath("uranium", "resources"), keep_path = True)
self.copy("*.dll", src = "@bindirs", dst = self._site_packages)
self.copy("*.pyd", src = "@libdirs", dst = self._site_packages)
self.copy("*.pyi", src = "@libdirs", dst = self._site_packages)
self.copy("*.dylib", src = "@libdirs", dst = self._script_dir)
def deploy(self):
# Copy CuraEngine.exe to bindirs of Virtual Python Environment
curaengine = self.dependencies["curaengine"].cpp_info
copy(self, "CuraEngine.exe", curaengine.bindirs[0], str(self._base_dir), keep_path = False)
copy(self, "CuraEngine", curaengine.bindirs[0], str(self._base_dir), keep_path = False)
copy(self, "*", os.path.join(self.package_folder, self.cpp.package.resdirs[2]), os.path.join(self.install_folder, "packaging"), keep_path = True)
# Copy resources of Cura (keep folder structure)
# Copy resources of Cura (keep folder structure) needed by pyinstaller to determine the module structure
copy(self, "*", os.path.join(self.package_folder, self.cpp_info.bindirs[0]), str(self._base_dir), keep_path = False)
copy(self, "*", os.path.join(self.package_folder, self.cpp_info.libdirs[0]), str(self._site_packages.joinpath("cura")), keep_path = True)
copy(self, "*", os.path.join(self.package_folder, self.cpp_info.resdirs[0]), str(self._share_dir.joinpath("cura", "resources")), keep_path = True)
copy(self, "*", os.path.join(self.package_folder, self.cpp_info.resdirs[1]), str(self._share_dir.joinpath("cura", "plugins")), keep_path = True)
# Copy materials (flat)
fdm_materials = self.dependencies["fdm_materials"].cpp_info
copy(self, "*", fdm_materials.resdirs[0], str(self._share_dir.joinpath("cura")))
# Copy internal resources
if self.options.internal:
cura_private_data = self.dependencies["cura_private_data"].cpp_info
copy(self, "*", cura_private_data.resdirs[0], str(self._share_dir.joinpath("cura")))
# Copy resources of Uranium (keep folder structure)
uranium = self.dependencies["uranium"].cpp_info
copy(self, "*", uranium.resdirs[0], str(self._share_dir.joinpath("uranium", "resources")), keep_path = True)
copy(self, "*", uranium.resdirs[1], str(self._share_dir.joinpath("uranium", "plugins")), keep_path = True)
copy(self, "*", uranium.libdirs[0], str(self._site_packages.joinpath("UM")), keep_path = True)
# TODO: figure out if this is still needed
copy(self, "*", os.path.join(uranium.libdirs[0], "Qt", "qml", "UM"), str(self._site_packages.joinpath("PyQt6", "Qt6", "qml", "UM")), keep_path = True)
# Copy resources of cura_binary_data
cura_binary_data = self.dependencies["cura_binary_data"].cpp_info
copy(self, "*", cura_binary_data.resdirs[0], str(self._share_dir.joinpath("cura")), keep_path = True)
copy(self, "*", cura_binary_data.resdirs[1], str(self._share_dir.joinpath("uranium")), keep_path = True)
if self.settings.os == "Windows":
copy(self, "*", cura_binary_data.resdirs[2], str(self._share_dir.joinpath("windows")), keep_path = True)
for dependency in self.dependencies.host.values():
for bindir in dependency.cpp_info.bindirs:
copy(self, "*.dll", bindir, str(self._site_packages), keep_path = False)
for libdir in dependency.cpp_info.libdirs:
copy(self, "*.pyd", libdir, str(self._site_packages), keep_path = False)
copy(self, "*.pyi", libdir, str(self._site_packages), keep_path = False)
copy(self, "*.dylib", libdir, str(self._base_dir.joinpath("lib")), keep_path = False)
# Copy packaging scripts
copy(self, "*", os.path.join(self.package_folder, self.cpp_info.resdirs[2]), str(self._base_dir.joinpath("packaging")), keep_path = True)
# Copy requirements.txt's
copy(self, "*.txt", os.path.join(self.package_folder, self.cpp_info.resdirs[-1]), str(self._base_dir.joinpath("pip_requirements")), keep_path = False)
# Generate the GitHub Action version info Environment
version = self.conf_info.get("user.cura:version", default = self.version, check_type = str)
cura_version = Version(version)
@ -432,6 +453,7 @@ echo "CURA_APP_NAME={{ cura_app_name }}" >> ${{ env_prefix }}GITHUB_ENV
save(self, os.path.join(self._script_dir, f"activate_github_actions_version_env{ext}"), activate_github_actions_version_env)
self._generate_cura_version(os.path.join(self._site_packages, "cura"))
self._generate_about_versions(str(self._share_dir.joinpath("cura", "resources", "qml", "Dialogs")))
entitlements_file = "'{}'".format(Path(self.cpp_info.res_paths[2], "MacOS", "cura.entitlements"))
self._generate_pyinstaller_spec(location = self._base_dir,
@ -448,6 +470,13 @@ echo "CURA_APP_NAME={{ cura_app_name }}" >> ${{ env_prefix }}GITHUB_ENV
copy(self, "requirement*.txt", src = self.source_folder, dst = os.path.join(self.package_folder, self.cpp.package.resdirs[-1]))
copy(self, "*", src = os.path.join(self.source_folder, "packaging"), dst = os.path.join(self.package_folder, self.cpp.package.resdirs[2]))
# Remove the CuraEngineGradualFlow plugin from the package
rmdir(self, os.path.join(self.package_folder, self.cpp.package.resdirs[1], "CuraEngineGradualFlow"))
rm(self, "bundled_*.json", os.path.join(self.package_folder, self.cpp.package.resdirs[0], "bundled_packages"), recursive = False)
# Remove the fdm_materials from the package
rmdir(self, os.path.join(self.package_folder, self.cpp.package.resdirs[0], "materials"))
def package_info(self):
self.user_info.pip_requirements = "requirements.txt"
self.user_info.pip_requirements_git = "requirements-ultimaker.txt"

View file

@ -8,17 +8,20 @@ from UM.Logger import Logger
from UM.Message import Message
from UM.Scene.SceneNode import SceneNode
from UM.i18n import i18nCatalog
from cura.Arranging.Nest2DArrange import arrange
from cura.Arranging.GridArrange import GridArrange
from cura.Arranging.Nest2DArrange import Nest2DArrange
i18n_catalog = i18nCatalog("cura")
class ArrangeObjectsJob(Job):
def __init__(self, nodes: List[SceneNode], fixed_nodes: List[SceneNode], min_offset = 8) -> None:
def __init__(self, nodes: List[SceneNode], fixed_nodes: List[SceneNode], min_offset = 8,
*, grid_arrange: bool = False) -> None:
super().__init__()
self._nodes = nodes
self._fixed_nodes = fixed_nodes
self._min_offset = min_offset
self._grid_arrange = grid_arrange
def run(self):
found_solution_for_all = False
@ -29,10 +32,18 @@ class ArrangeObjectsJob(Job):
title = i18n_catalog.i18nc("@info:title", "Finding Location"))
status_message.show()
if self._grid_arrange:
arranger = GridArrange(self._nodes, Application.getInstance().getBuildVolume(), self._fixed_nodes)
else:
arranger = Nest2DArrange(self._nodes, Application.getInstance().getBuildVolume(), self._fixed_nodes,
factor=1000)
found_solution_for_all = False
try:
found_solution_for_all = arrange(self._nodes, Application.getInstance().getBuildVolume(), self._fixed_nodes)
found_solution_for_all = arranger.arrange()
except: # If the thread crashes, the message should still close
Logger.logException("e", "Unable to arrange the objects on the buildplate. The arrange algorithm has crashed.")
Logger.logException("e",
"Unable to arrange the objects on the buildplate. The arrange algorithm has crashed.")
status_message.hide()

View file

@ -0,0 +1,28 @@
from typing import List, TYPE_CHECKING, Optional, Tuple, Set
if TYPE_CHECKING:
from UM.Operations.GroupedOperation import GroupedOperation
class Arranger:
def createGroupOperationForArrange(self, *, add_new_nodes_in_scene: bool = False) -> Tuple["GroupedOperation", int]:
"""
Find placement for a set of scene nodes, but don't actually move them just yet.
:param add_new_nodes_in_scene: Whether to create new scene nodes before applying the transformations and rotations
:return: tuple (found_solution_for_all, node_items)
WHERE
found_solution_for_all: Whether the algorithm found a place on the buildplate for all the objects
node_items: A list of the nodes return by libnest2d, which contain the new positions on the buildplate
"""
raise NotImplementedError
def arrange(self, *, add_new_nodes_in_scene: bool = False) -> bool:
"""
Find placement for a set of scene nodes, and move them by using a single grouped operation.
:param add_new_nodes_in_scene: Whether to create new scene nodes before applying the transformations and rotations
:return: found_solution_for_all: Whether the algorithm found a place on the buildplate for all the objects
"""
grouped_operation, not_fit_count = self.createGroupOperationForArrange(
add_new_nodes_in_scene=add_new_nodes_in_scene)
grouped_operation.push()
return not_fit_count == 0

View file

@ -0,0 +1,347 @@
import math
from typing import List, TYPE_CHECKING, Tuple, Set, Union
if TYPE_CHECKING:
from UM.Scene.SceneNode import SceneNode
from cura.BuildVolume import BuildVolume
from UM.Application import Application
from UM.Math.AxisAlignedBox import AxisAlignedBox
from UM.Math.Polygon import Polygon
from UM.Math.Vector import Vector
from UM.Operations.AddSceneNodeOperation import AddSceneNodeOperation
from UM.Operations.GroupedOperation import GroupedOperation
from UM.Operations.TranslateOperation import TranslateOperation
from cura.Arranging.Arranger import Arranger
class GridArrange(Arranger):
def __init__(self, nodes_to_arrange: List["SceneNode"], build_volume: "BuildVolume", fixed_nodes: List["SceneNode"] = None):
if fixed_nodes is None:
fixed_nodes = []
self._nodes_to_arrange = nodes_to_arrange
self._build_volume = build_volume
self._build_volume_bounding_box = build_volume.getBoundingBox()
self._fixed_nodes = fixed_nodes
self._margin_x: float = 1
self._margin_y: float = 1
self._grid_width = 0
self._grid_height = 0
for node in self._nodes_to_arrange:
bounding_box = node.getBoundingBox()
self._grid_width = max(self._grid_width, bounding_box.width)
self._grid_height = max(self._grid_height, bounding_box.depth)
self._grid_width += self._margin_x
self._grid_height += self._margin_y
# Round up the grid size to the nearest cm, this assures that new objects will
# be placed on integer offsets from each other
grid_precision = 10 # 1cm
rounded_grid_width = math.ceil(self._grid_width / grid_precision) * grid_precision
rounded_grid_height = math.ceil(self._grid_height / grid_precision) * grid_precision
# The space added by the "grid precision rounding up" of the grid size
self._grid_round_margin_x = rounded_grid_width - self._grid_width
self._grid_round_margin_y = rounded_grid_height - self._grid_height
self._grid_width = rounded_grid_width
self._grid_height = rounded_grid_height
self._offset_x = 0
self._offset_y = 0
self._findOptimalGridOffset()
coord_initial_leftover_x = self._build_volume_bounding_box.right + 2 * self._grid_width
coord_initial_leftover_y = (self._build_volume_bounding_box.back + self._build_volume_bounding_box.front) * 0.5
self._initial_leftover_grid_x, self._initial_leftover_grid_y = self._coordSpaceToGridSpace(
coord_initial_leftover_x, coord_initial_leftover_y)
self._initial_leftover_grid_x = math.floor(self._initial_leftover_grid_x)
self._initial_leftover_grid_y = math.floor(self._initial_leftover_grid_y)
# Find grid indexes that intersect with fixed objects
self._fixed_nodes_grid_ids = set()
for node in self._fixed_nodes:
self._fixed_nodes_grid_ids = self._fixed_nodes_grid_ids.union(
self._intersectingGridIdxInclusive(node.getBoundingBox()))
# grid indexes that are in disallowed area
for polygon in self._build_volume.getDisallowedAreas():
self._fixed_nodes_grid_ids = self._fixed_nodes_grid_ids.union(self._intersectingGridIdxInclusive(polygon))
self._build_plate_grid_ids = self._intersectingGridIdxExclusive(self._build_volume_bounding_box)
# Filter out the corner grid squares if the build plate shape is elliptic
if self._build_volume.getShape() == "elliptic":
self._build_plate_grid_ids = set(
filter(lambda grid_id: self._checkGridUnderDiscSpace(grid_id[0], grid_id[1]),
self._build_plate_grid_ids))
self._allowed_grid_idx = self._build_plate_grid_ids.difference(self._fixed_nodes_grid_ids)
def createGroupOperationForArrange(self, *, add_new_nodes_in_scene: bool = False) -> Tuple[GroupedOperation, int]:
# Find the sequence in which items are placed
coord_build_plate_center_x = self._build_volume_bounding_box.width * 0.5 + self._build_volume_bounding_box.left
coord_build_plate_center_y = self._build_volume_bounding_box.depth * 0.5 + self._build_volume_bounding_box.back
grid_build_plate_center_x, grid_build_plate_center_y = self._coordSpaceToGridSpace(coord_build_plate_center_x,
coord_build_plate_center_y)
sequence: List[Tuple[int, int]] = list(self._allowed_grid_idx)
sequence.sort(key=lambda grid_id: (grid_build_plate_center_x - grid_id[0]) ** 2 + (
grid_build_plate_center_y - grid_id[1]) ** 2)
scene_root = Application.getInstance().getController().getScene().getRoot()
grouped_operation = GroupedOperation()
for grid_id, node in zip(sequence, self._nodes_to_arrange):
if add_new_nodes_in_scene:
grouped_operation.addOperation(AddSceneNodeOperation(node, scene_root))
grid_x, grid_y = grid_id
operation = self._moveNodeOnGrid(node, grid_x, grid_y)
grouped_operation.addOperation(operation)
leftover_nodes = self._nodes_to_arrange[len(sequence):]
left_over_grid_y = self._initial_leftover_grid_y
for node in leftover_nodes:
if add_new_nodes_in_scene:
grouped_operation.addOperation(AddSceneNodeOperation(node, scene_root))
# find the first next grid position that isn't occupied by a fixed node
while (self._initial_leftover_grid_x, left_over_grid_y) in self._fixed_nodes_grid_ids:
left_over_grid_y = left_over_grid_y - 1
operation = self._moveNodeOnGrid(node, self._initial_leftover_grid_x, left_over_grid_y)
grouped_operation.addOperation(operation)
left_over_grid_y = left_over_grid_y - 1
return grouped_operation, len(leftover_nodes)
def _findOptimalGridOffset(self):
if len(self._fixed_nodes) == 0:
self._offset_x = 0
self._offset_y = 0
return
if len(self._fixed_nodes) == 1:
center_grid_x = 0.5 * self._grid_width + self._build_volume_bounding_box.left
center_grid_y = 0.5 * self._grid_height + self._build_volume_bounding_box.back
bounding_box = self._fixed_nodes[0].getBoundingBox()
center_node_x = (bounding_box.left + bounding_box.right) * 0.5
center_node_y = (bounding_box.back + bounding_box.front) * 0.5
self._offset_x = center_node_x - center_grid_x
self._offset_y = center_node_y - center_grid_y
return
# If there are multiple fixed nodes, an optimal solution is not always possible
# We will try to find an offset that minimizes the number of grid intersections
# with fixed nodes. The algorithm below achieves this by utilizing a scanline
# algorithm. In this algorithm each axis is solved separately as offsetting
# is completely independent in each axis. The comments explaining the algorithm
# below are for the x-axis, but the same applies for the y-axis.
#
# Each node either occupies ceil((node.right - node.right) / grid_width) or
# ceil((node.right - node.right) / grid_width) + 1 grid squares. We will call
# these the node's "footprint".
#
# ┌────────────────┐
# minimum foot-print │ NODE │
# └────────────────┘
# │ grid 1 │ grid 2 │ grid 3 │ grid 4 | grid 5 |
# ┌────────────────┐
# maximum foot-print │ NODE │
# └────────────────┘
#
# The algorithm will find the grid offset such that the number of nodes with
# a _minimal_ footprint is _maximized_.
# The scanline algorithm works as follows, we create events for both end points
# of each node's footprint. The event have two properties,
# - the coordinate: the amount the endpoint can move to the
# left before it crosses a grid line
# - the change: either +1 or -1, indicating whether crossing the grid line
# would result in a minimal footprint node becoming a maximal footprint
class Event:
def __init__(self, coord: float, change: float):
self.coord = coord
self.change = change
# create events for both the horizontal and vertical axis
events_horizontal: List[Event] = []
events_vertical: List[Event] = []
for node in self._fixed_nodes:
bounding_box = node.getBoundingBox()
left = bounding_box.left - self._build_volume_bounding_box.left
right = bounding_box.right - self._build_volume_bounding_box.left
back = bounding_box.back - self._build_volume_bounding_box.back
front = bounding_box.front - self._build_volume_bounding_box.back
value_left = math.ceil(left / self._grid_width) * self._grid_width - left
value_right = math.ceil(right / self._grid_width) * self._grid_width - right
value_back = math.ceil(back / self._grid_height) * self._grid_height - back
value_front = math.ceil(front / self._grid_height) * self._grid_height - front
# give nodes a weight according to their size. This
# weight is heuristically chosen to be proportional to
# the number of grid squares the node-boundary occupies
weight = bounding_box.width + bounding_box.depth
events_horizontal.append(Event(value_left, weight))
events_horizontal.append(Event(value_right, -weight))
events_vertical.append(Event(value_back, weight))
events_vertical.append(Event(value_front, -weight))
events_horizontal.sort(key=lambda event: event.coord)
events_vertical.sort(key=lambda event: event.coord)
def findOptimalShiftAxis(events: List[Event], interval: float) -> float:
# executing the actual scanline algorithm
# iteratively go through events (left to right) and keep track of the
# current footprint. The optimal location is the one with the minimal
# footprint. If there are multiple locations with the same minimal
# footprint, the optimal location is the one with the largest range
# between the left and right endpoint of the footprint.
prev_offset = events[-1].coord - interval
current_minimal_footprint_count = 0
best_minimal_footprint_count = float('inf')
best_offset_span = float('-inf')
best_offset = 0.0
for event in events:
offset_span = event.coord - prev_offset
if current_minimal_footprint_count < best_minimal_footprint_count or (
current_minimal_footprint_count == best_minimal_footprint_count and offset_span > best_offset_span):
best_minimal_footprint_count = current_minimal_footprint_count
best_offset_span = offset_span
best_offset = event.coord
current_minimal_footprint_count += event.change
prev_offset = event.coord
return best_offset - best_offset_span * 0.5
center_grid_x = 0.5 * self._grid_width
center_grid_y = 0.5 * self._grid_height
optimal_center_x = self._grid_width - findOptimalShiftAxis(events_horizontal, self._grid_width)
optimal_center_y = self._grid_height - findOptimalShiftAxis(events_vertical, self._grid_height)
self._offset_x = optimal_center_x - center_grid_x
self._offset_y = optimal_center_y - center_grid_y
def _moveNodeOnGrid(self, node: "SceneNode", grid_x: int, grid_y: int) -> "Operation.Operation":
coord_grid_x, coord_grid_y = self._gridSpaceToCoordSpace(grid_x, grid_y)
center_grid_x = coord_grid_x + (0.5 * self._grid_width)
center_grid_y = coord_grid_y + (0.5 * self._grid_height)
bounding_box = node.getBoundingBox()
center_node_x = (bounding_box.left + bounding_box.right) * 0.5
center_node_y = (bounding_box.back + bounding_box.front) * 0.5
delta_x = center_grid_x - center_node_x
delta_y = center_grid_y - center_node_y
return TranslateOperation(node, Vector(delta_x, 0, delta_y))
def _getGridCornerPoints(
self,
bounds: Union[AxisAlignedBox, Polygon],
*,
margin_x: float = 0.0,
margin_y: float = 0.0
) -> Tuple[float, float, float, float]:
if isinstance(bounds, AxisAlignedBox):
coord_x1 = bounds.left - margin_x
coord_x2 = bounds.right + margin_x
coord_y1 = bounds.back - margin_y
coord_y2 = bounds.front + margin_y
elif isinstance(bounds, Polygon):
coord_x1 = float('inf')
coord_y1 = float('inf')
coord_x2 = float('-inf')
coord_y2 = float('-inf')
for x, y in bounds.getPoints():
coord_x1 = min(coord_x1, x)
coord_y1 = min(coord_y1, y)
coord_x2 = max(coord_x2, x)
coord_y2 = max(coord_y2, y)
else:
raise TypeError("bounds must be either an AxisAlignedBox or a Polygon")
coord_x1 -= margin_x
coord_x2 += margin_x
coord_y1 -= margin_y
coord_y2 += margin_y
grid_x1, grid_y1 = self._coordSpaceToGridSpace(coord_x1, coord_y1)
grid_x2, grid_y2 = self._coordSpaceToGridSpace(coord_x2, coord_y2)
return grid_x1, grid_y1, grid_x2, grid_y2
def _intersectingGridIdxInclusive(self, bounds: Union[AxisAlignedBox, Polygon]) -> Set[Tuple[int, int]]:
grid_x1, grid_y1, grid_x2, grid_y2 = self._getGridCornerPoints(
bounds,
margin_x=-(self._margin_x + self._grid_round_margin_x) * 0.5,
margin_y=-(self._margin_y + self._grid_round_margin_y) * 0.5,
)
grid_idx = set()
for grid_x in range(math.floor(grid_x1), math.ceil(grid_x2)):
for grid_y in range(math.floor(grid_y1), math.ceil(grid_y2)):
grid_idx.add((grid_x, grid_y))
return grid_idx
def _intersectingGridIdxExclusive(self, bounds: Union[AxisAlignedBox, Polygon]) -> Set[Tuple[int, int]]:
grid_x1, grid_y1, grid_x2, grid_y2 = self._getGridCornerPoints(
bounds,
margin_x=(self._margin_x + self._grid_round_margin_x) * 0.5,
margin_y=(self._margin_y + self._grid_round_margin_y) * 0.5,
)
grid_idx = set()
for grid_x in range(math.ceil(grid_x1), math.floor(grid_x2)):
for grid_y in range(math.ceil(grid_y1), math.floor(grid_y2)):
grid_idx.add((grid_x, grid_y))
return grid_idx
def _gridSpaceToCoordSpace(self, x: float, y: float) -> Tuple[float, float]:
grid_x = x * self._grid_width + self._build_volume_bounding_box.left + self._offset_x
grid_y = y * self._grid_height + self._build_volume_bounding_box.back + self._offset_y
return grid_x, grid_y
def _coordSpaceToGridSpace(self, grid_x: float, grid_y: float) -> Tuple[float, float]:
coord_x = (grid_x - self._build_volume_bounding_box.left - self._offset_x) / self._grid_width
coord_y = (grid_y - self._build_volume_bounding_box.back - self._offset_y) / self._grid_height
return coord_x, coord_y
def _checkGridUnderDiscSpace(self, grid_x: int, grid_y: int) -> bool:
left, back = self._gridSpaceToCoordSpace(grid_x, grid_y)
right, front = self._gridSpaceToCoordSpace(grid_x + 1, grid_y + 1)
corners = [(left, back), (right, back), (right, front), (left, front)]
return all([self._checkPointUnderDiscSpace(x, y) for x, y in corners])
def _checkPointUnderDiscSpace(self, x: float, y: float) -> bool:
disc_x, disc_y = self._coordSpaceToDiscSpace(x, y)
distance_to_center_squared = disc_x ** 2 + disc_y ** 2
return distance_to_center_squared <= 1.0
def _coordSpaceToDiscSpace(self, x: float, y: float) -> Tuple[float, float]:
# Transform coordinate system to
#
# coord_build_plate_left = -1
# | coord_build_plate_right = 1
# v (0,1) v
# ┌───────┬───────┐ < coord_build_plate_back = -1
# │ │ │
# │ │(0,0) │
# (-1,0)├───────o───────┤(1,0)
# │ │ │
# │ │ │
# └───────┴───────┘ < coord_build_plate_front = +1
# (0,-1)
disc_x = ((x - self._build_volume_bounding_box.left) / self._build_volume_bounding_box.width) * 2.0 - 1.0
disc_y = ((y - self._build_volume_bounding_box.back) / self._build_volume_bounding_box.depth) * 2.0 - 1.0
return disc_x, disc_y

View file

@ -15,149 +15,137 @@ from UM.Operations.AddSceneNodeOperation import AddSceneNodeOperation
from UM.Operations.GroupedOperation import GroupedOperation
from UM.Operations.RotateOperation import RotateOperation
from UM.Operations.TranslateOperation import TranslateOperation
from cura.Arranging.Arranger import Arranger
if TYPE_CHECKING:
from UM.Scene.SceneNode import SceneNode
from cura.BuildVolume import BuildVolume
def findNodePlacement(nodes_to_arrange: List["SceneNode"], build_volume: "BuildVolume", fixed_nodes: Optional[List["SceneNode"]] = None, factor = 10000) -> Tuple[bool, List[Item]]:
"""
Find placement for a set of scene nodes, but don't actually move them just yet.
:param nodes_to_arrange: The list of nodes that need to be moved.
:param build_volume: The build volume that we want to place the nodes in. It gets size & disallowed areas from this.
:param fixed_nodes: List of nods that should not be moved, but should be used when deciding where the others nodes
are placed.
:param factor: The library that we use is int based. This factor defines how accurate we want it to be.
class Nest2DArrange(Arranger):
def __init__(self,
nodes_to_arrange: List["SceneNode"],
build_volume: "BuildVolume",
fixed_nodes: Optional[List["SceneNode"]] = None,
*,
factor: int = 10000,
lock_rotation: bool = False):
"""
:param nodes_to_arrange: The list of nodes that need to be moved.
:param build_volume: The build volume that we want to place the nodes in. It gets size & disallowed areas from this.
:param fixed_nodes: List of nods that should not be moved, but should be used when deciding where the others nodes
are placed.
:param factor: The library that we use is int based. This factor defines how accuracte we want it to be.
:param lock_rotation: If set to true the orientation of the object will remain the same
"""
super().__init__()
self._nodes_to_arrange = nodes_to_arrange
self._build_volume = build_volume
self._fixed_nodes = fixed_nodes
self._factor = factor
self._lock_rotation = lock_rotation
:return: tuple (found_solution_for_all, node_items)
WHERE
found_solution_for_all: Whether the algorithm found a place on the buildplate for all the objects
node_items: A list of the nodes return by libnest2d, which contain the new positions on the buildplate
"""
spacing = int(1.5 * factor) # 1.5mm spacing.
def findNodePlacement(self) -> Tuple[bool, List[Item]]:
spacing = int(1.5 * self._factor) # 1.5mm spacing.
machine_width = build_volume.getWidth()
machine_depth = build_volume.getDepth()
build_plate_bounding_box = Box(int(machine_width * factor), int(machine_depth * factor))
machine_width = self._build_volume.getWidth()
machine_depth = self._build_volume.getDepth()
build_plate_bounding_box = Box(int(machine_width * self._factor), int(machine_depth * self._factor))
if fixed_nodes is None:
fixed_nodes = []
if self._fixed_nodes is None:
self._fixed_nodes = []
# Add all the items we want to arrange
node_items = []
for node in nodes_to_arrange:
hull_polygon = node.callDecoration("getConvexHull")
if not hull_polygon or hull_polygon.getPoints is None:
Logger.log("w", "Object {} cannot be arranged because it has no convex hull.".format(node.getName()))
continue
converted_points = []
for point in hull_polygon.getPoints():
converted_points.append(Point(int(point[0] * factor), int(point[1] * factor)))
item = Item(converted_points)
node_items.append(item)
# Use a tiny margin for the build_plate_polygon (the nesting doesn't like overlapping disallowed areas)
half_machine_width = 0.5 * machine_width - 1
half_machine_depth = 0.5 * machine_depth - 1
build_plate_polygon = Polygon(numpy.array([
[half_machine_width, -half_machine_depth],
[-half_machine_width, -half_machine_depth],
[-half_machine_width, half_machine_depth],
[half_machine_width, half_machine_depth]
], numpy.float32))
disallowed_areas = build_volume.getDisallowedAreas()
num_disallowed_areas_added = 0
for area in disallowed_areas:
converted_points = []
# Clip the disallowed areas so that they don't overlap the bounding box (The arranger chokes otherwise)
clipped_area = area.intersectionConvexHulls(build_plate_polygon)
if clipped_area.getPoints() is not None and len(clipped_area.getPoints()) > 2: # numpy array has to be explicitly checked against None
for point in clipped_area.getPoints():
converted_points.append(Point(int(point[0] * factor), int(point[1] * factor)))
disallowed_area = Item(converted_points)
disallowed_area.markAsDisallowedAreaInBin(0)
node_items.append(disallowed_area)
num_disallowed_areas_added += 1
for node in fixed_nodes:
converted_points = []
hull_polygon = node.callDecoration("getConvexHull")
if hull_polygon is not None and hull_polygon.getPoints() is not None and len(hull_polygon.getPoints()) > 2: # numpy array has to be explicitly checked against None
# Add all the items we want to arrange
node_items = []
for node in self._nodes_to_arrange:
hull_polygon = node.callDecoration("getConvexHull")
if not hull_polygon or hull_polygon.getPoints is None:
Logger.log("w", "Object {} cannot be arranged because it has no convex hull.".format(node.getName()))
continue
converted_points = []
for point in hull_polygon.getPoints():
converted_points.append(Point(int(point[0] * factor), int(point[1] * factor)))
converted_points.append(Point(int(point[0] * self._factor), int(point[1] * self._factor)))
item = Item(converted_points)
item.markAsFixedInBin(0)
node_items.append(item)
num_disallowed_areas_added += 1
config = NfpConfig()
config.accuracy = 1.0
config.alignment = NfpConfig.Alignment.DONT_ALIGN
# Use a tiny margin for the build_plate_polygon (the nesting doesn't like overlapping disallowed areas)
half_machine_width = 0.5 * machine_width - 1
half_machine_depth = 0.5 * machine_depth - 1
build_plate_polygon = Polygon(numpy.array([
[half_machine_width, -half_machine_depth],
[-half_machine_width, -half_machine_depth],
[-half_machine_width, half_machine_depth],
[half_machine_width, half_machine_depth]
], numpy.float32))
num_bins = nest(node_items, build_plate_bounding_box, spacing, config)
disallowed_areas = self._build_volume.getDisallowedAreas()
num_disallowed_areas_added = 0
for area in disallowed_areas:
converted_points = []
# Strip the fixed items (previously placed) and the disallowed areas from the results again.
node_items = list(filter(lambda item: not item.isFixed(), node_items))
# Clip the disallowed areas so that they don't overlap the bounding box (The arranger chokes otherwise)
clipped_area = area.intersectionConvexHulls(build_plate_polygon)
found_solution_for_all = num_bins == 1
if clipped_area.getPoints() is not None and len(
clipped_area.getPoints()) > 2: # numpy array has to be explicitly checked against None
for point in clipped_area.getPoints():
converted_points.append(Point(int(point[0] * self._factor), int(point[1] * self._factor)))
return found_solution_for_all, node_items
disallowed_area = Item(converted_points)
disallowed_area.markAsDisallowedAreaInBin(0)
node_items.append(disallowed_area)
num_disallowed_areas_added += 1
for node in self._fixed_nodes:
converted_points = []
hull_polygon = node.callDecoration("getConvexHull")
def createGroupOperationForArrange(nodes_to_arrange: List["SceneNode"],
build_volume: "BuildVolume",
fixed_nodes: Optional[List["SceneNode"]] = None,
factor = 10000,
add_new_nodes_in_scene: bool = False) -> Tuple[GroupedOperation, int]:
scene_root = Application.getInstance().getController().getScene().getRoot()
found_solution_for_all, node_items = findNodePlacement(nodes_to_arrange, build_volume, fixed_nodes, factor)
if hull_polygon is not None and hull_polygon.getPoints() is not None and len(
hull_polygon.getPoints()) > 2: # numpy array has to be explicitly checked against None
for point in hull_polygon.getPoints():
converted_points.append(Point(int(point[0] * self._factor), int(point[1] * self._factor)))
item = Item(converted_points)
item.markAsFixedInBin(0)
node_items.append(item)
num_disallowed_areas_added += 1
not_fit_count = 0
grouped_operation = GroupedOperation()
for node, node_item in zip(nodes_to_arrange, node_items):
if add_new_nodes_in_scene:
grouped_operation.addOperation(AddSceneNodeOperation(node, scene_root))
config = NfpConfig()
config.accuracy = 1.0
config.alignment = NfpConfig.Alignment.DONT_ALIGN
if self._lock_rotation:
config.rotations = [0.0]
if node_item.binId() == 0:
# We found a spot for it
rotation_matrix = Matrix()
rotation_matrix.setByRotationAxis(node_item.rotation(), Vector(0, -1, 0))
grouped_operation.addOperation(RotateOperation(node, Quaternion.fromMatrix(rotation_matrix)))
grouped_operation.addOperation(TranslateOperation(node, Vector(node_item.translation().x() / factor, 0,
node_item.translation().y() / factor)))
else:
# We didn't find a spot
grouped_operation.addOperation(
TranslateOperation(node, Vector(200, node.getWorldPosition().y, -not_fit_count * 20), set_position = True))
not_fit_count += 1
num_bins = nest(node_items, build_plate_bounding_box, spacing, config)
return grouped_operation, not_fit_count
# Strip the fixed items (previously placed) and the disallowed areas from the results again.
node_items = list(filter(lambda item: not item.isFixed(), node_items))
found_solution_for_all = num_bins == 1
def arrange(nodes_to_arrange: List["SceneNode"],
build_volume: "BuildVolume",
fixed_nodes: Optional[List["SceneNode"]] = None,
factor = 10000,
add_new_nodes_in_scene: bool = False) -> bool:
"""
Find placement for a set of scene nodes, and move them by using a single grouped operation.
:param nodes_to_arrange: The list of nodes that need to be moved.
:param build_volume: The build volume that we want to place the nodes in. It gets size & disallowed areas from this.
:param fixed_nodes: List of nods that should not be moved, but should be used when deciding where the others nodes
are placed.
:param factor: The library that we use is int based. This factor defines how accuracte we want it to be.
:param add_new_nodes_in_scene: Whether to create new scene nodes before applying the transformations and rotations
return found_solution_for_all, node_items
:return: found_solution_for_all: Whether the algorithm found a place on the buildplate for all the objects
"""
def createGroupOperationForArrange(self, *, add_new_nodes_in_scene: bool = False) -> Tuple[GroupedOperation, int]:
scene_root = Application.getInstance().getController().getScene().getRoot()
found_solution_for_all, node_items = self.findNodePlacement()
grouped_operation, not_fit_count = createGroupOperationForArrange(nodes_to_arrange, build_volume, fixed_nodes, factor, add_new_nodes_in_scene)
grouped_operation.push()
return not_fit_count == 0
not_fit_count = 0
grouped_operation = GroupedOperation()
for node, node_item in zip(self._nodes_to_arrange, node_items):
if add_new_nodes_in_scene:
grouped_operation.addOperation(AddSceneNodeOperation(node, scene_root))
if node_item.binId() == 0:
# We found a spot for it
rotation_matrix = Matrix()
rotation_matrix.setByRotationAxis(node_item.rotation(), Vector(0, -1, 0))
grouped_operation.addOperation(RotateOperation(node, Quaternion.fromMatrix(rotation_matrix)))
grouped_operation.addOperation(
TranslateOperation(node, Vector(node_item.translation().x() / self._factor, 0,
node_item.translation().y() / self._factor)))
else:
# We didn't find a spot
grouped_operation.addOperation(
TranslateOperation(node, Vector(200, node.getWorldPosition().y, -not_fit_count * 20), set_position = True))
not_fit_count += 1
return grouped_operation, not_fit_count

110
cura/BackendPlugin.py Normal file
View file

@ -0,0 +1,110 @@
# Copyright (c) 2023 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
import subprocess
from typing import Optional, List
from UM.Logger import Logger
from UM.Message import Message
from UM.Settings.AdditionalSettingDefinitionAppender import AdditionalSettingDefinitionsAppender
from UM.PluginObject import PluginObject
from UM.i18n import i18nCatalog
from UM.Platform import Platform
class BackendPlugin(AdditionalSettingDefinitionsAppender, PluginObject):
catalog = i18nCatalog("cura")
def __init__(self) -> None:
super().__init__()
self.__port: int = 0
self._plugin_address: str = "127.0.0.1"
self._plugin_command: Optional[List[str]] = None
self._process = None
self._is_running = False
self._supported_slots: List[int] = []
def getSupportedSlots(self) -> List[int]:
return self._supported_slots
def isRunning(self):
return self._is_running
def setPort(self, port: int) -> None:
self.__port = port
def getPort(self) -> int:
return self.__port
def getAddress(self) -> str:
return self._plugin_address
def _validatePluginCommand(self) -> list[str]:
"""
Validate the plugin command and add the port parameter if it is missing.
:return: A list of strings containing the validated plugin command.
"""
if not self._plugin_command or "--port" in self._plugin_command:
return self._plugin_command or []
return self._plugin_command + ["--address", self.getAddress(), "--port", str(self.__port)]
def start(self) -> bool:
"""
Starts the backend_plugin process.
:return: True if the plugin process started successfully, False otherwise.
"""
try:
# STDIN needs to be None because we provide no input, but communicate via a local socket instead.
# The NUL device sometimes doesn't exist on some computers.
Logger.info(f"Starting backend_plugin [{self._plugin_id}] with command: {self._validatePluginCommand()}")
popen_kwargs = {"stdin": None}
if Platform.isWindows():
popen_kwargs["creationflags"] = subprocess.CREATE_NO_WINDOW
self._process = subprocess.Popen(self._validatePluginCommand(), **popen_kwargs)
self._is_running = True
return True
except PermissionError:
Logger.log("e", f"Couldn't start EnginePlugin: {self._plugin_id} No permission to execute process.")
self._showMessage(self.catalog.i18nc("@info:plugin_failed",
f"Couldn't start EnginePlugin: {self._plugin_id}\nNo permission to execute process."),
message_type = Message.MessageType.ERROR)
except FileNotFoundError:
Logger.logException("e", f"Unable to find local EnginePlugin server executable for: {self._plugin_id}")
self._showMessage(self.catalog.i18nc("@info:plugin_failed",
f"Unable to find local EnginePlugin server executable for: {self._plugin_id}"),
message_type = Message.MessageType.ERROR)
except BlockingIOError:
Logger.logException("e", f"Couldn't start EnginePlugin: {self._plugin_id} Resource is temporarily unavailable")
self._showMessage(self.catalog.i18nc("@info:plugin_failed",
f"Couldn't start EnginePlugin: {self._plugin_id}\nResource is temporarily unavailable"),
message_type = Message.MessageType.ERROR)
except OSError as e:
Logger.logException("e", f"Couldn't start EnginePlugin {self._plugin_id} Operating system is blocking it (antivirus?)")
self._showMessage(self.catalog.i18nc("@info:plugin_failed",
f"Couldn't start EnginePlugin: {self._plugin_id}\nOperating system is blocking it (antivirus?)"),
message_type = Message.MessageType.ERROR)
return False
def stop(self) -> bool:
if not self._process:
self._is_running = False
return True # Nothing to stop
try:
self._process.terminate()
return_code = self._process.wait()
self._is_running = False
Logger.log("d", f"EnginePlugin: {self._plugin_id} was killed. Received return code {return_code}")
return True
except PermissionError:
Logger.log("e", f"Unable to kill running EnginePlugin: {self._plugin_id} Access is denied.")
self._showMessage(self.catalog.i18nc("@info:plugin_failed",
f"Unable to kill running EnginePlugin: {self._plugin_id}\nAccess is denied."),
message_type = Message.MessageType.ERROR)
return False
def _showMessage(self, message: str, message_type: Message.MessageType = Message.MessageType.ERROR) -> None:
Message(message, title=self.catalog.i18nc("@info:title", "EnginePlugin"), message_type = message_type).show()

View file

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

View file

@ -22,7 +22,7 @@ except ImportError:
from PyQt6.QtCore import QT_VERSION_STR, PYQT_VERSION_STR, QUrl
from PyQt6.QtWidgets import QDialog, QDialogButtonBox, QVBoxLayout, QLabel, QTextEdit, QGroupBox, QCheckBox, QPushButton
from PyQt6.QtGui import QDesktopServices
from PyQt6.QtGui import QDesktopServices, QTextCursor
from UM.Application import Application
from UM.Logger import Logger
@ -309,7 +309,7 @@ class CrashHandler:
trace = "".join(trace_list)
text_area.setText(trace)
text_area.setReadOnly(True)
text_area.moveCursor(QTextCursor.MoveOperation.End) # Move cursor to end, so we see last bit of the exception
layout.addWidget(text_area)
group.setLayout(layout)
@ -400,7 +400,7 @@ class CrashHandler:
text_area.setText(logdata)
text_area.setReadOnly(True)
text_area.moveCursor(QTextCursor.MoveOperation.End) # Move cursor to end, so we see last bit of the log
layout.addWidget(text_area)
group.setLayout(layout)

View file

@ -1,15 +1,18 @@
# Copyright (c) 2018 Ultimaker B.V.
# Copyright (c) 2023 UltiMaker
# Cura is released under the terms of the LGPLv3 or higher.
from PyQt6.QtCore import QObject, QUrl
from PyQt6.QtGui import QDesktopServices
from typing import List, cast
from PyQt6.QtCore import QObject, QUrl, QMimeData
from PyQt6.QtGui import QDesktopServices
from PyQt6.QtWidgets import QApplication
from UM.Event import CallFunctionEvent
from UM.FlameProfiler import pyqtSlot
from UM.Math.Vector import Vector
from UM.Scene.Selection import Selection
from UM.Scene.Iterator.BreadthFirstIterator import BreadthFirstIterator
from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
from UM.Operations.GroupedOperation import GroupedOperation
from UM.Operations.RemoveSceneNodeOperation import RemoveSceneNodeOperation
from UM.Operations.TranslateOperation import TranslateOperation
@ -20,6 +23,10 @@ from cura.MultiplyObjectsJob import MultiplyObjectsJob
from cura.Settings.SetObjectExtruderOperation import SetObjectExtruderOperation
from cura.Settings.ExtruderManager import ExtruderManager
from cura.Arranging.GridArrange import GridArrange
from cura.Arranging.Nest2DArrange import Nest2DArrange
from cura.Operations.SetBuildPlateNumberOperation import SetBuildPlateNumberOperation
from UM.Logger import Logger
@ -78,16 +85,25 @@ class CuraActions(QObject):
center_operation = TranslateOperation(current_node, Vector(0, center_y, 0), set_position = True)
operation.addOperation(center_operation)
operation.push()
@pyqtSlot(int)
def multiplySelection(self, count: int) -> None:
"""Multiply all objects in the selection
:param count: The number of times to multiply the selection.
"""
min_offset = cura.CuraApplication.CuraApplication.getInstance().getBuildVolume().getEdgeDisallowedSize() + 2 # Allow for some rounding errors
job = MultiplyObjectsJob(Selection.getAllSelectedObjects(), count, min_offset = max(min_offset, 8))
job.start()
@pyqtSlot(int)
def multiplySelectionToGrid(self, count: int) -> None:
"""Multiply all objects in the selection
:param count: The number of times to multiply the selection.
"""
min_offset = cura.CuraApplication.CuraApplication.getInstance().getBuildVolume().getEdgeDisallowedSize() + 2 # Allow for some rounding errors
job = MultiplyObjectsJob(Selection.getAllSelectedObjects(), count, min_offset = max(min_offset, 8))
job = MultiplyObjectsJob(Selection.getAllSelectedObjects(), count, min_offset=max(min_offset, 8),
grid_arrange=True)
job.start()
@pyqtSlot()
@ -181,5 +197,60 @@ class CuraActions(QObject):
Selection.clear()
@pyqtSlot()
def cut(self) -> None:
self.copy()
self.deleteSelection()
@pyqtSlot()
def copy(self) -> None:
mesh_writer = cura.CuraApplication.CuraApplication.getInstance().getMeshFileHandler().getWriter("3MFWriter")
if not mesh_writer:
Logger.log("e", "No 3MF writer found, unable to copy.")
return
# Get the selected nodes
selected_objects = Selection.getAllSelectedObjects()
# Serialize the nodes to a string
scene_string = mesh_writer.sceneNodesToString(selected_objects)
# Put the string on the clipboard
QApplication.clipboard().setText(scene_string)
@pyqtSlot()
def paste(self) -> None:
application = cura.CuraApplication.CuraApplication.getInstance()
mesh_reader = application.getMeshFileHandler().getReaderForFile(".3mf")
if not mesh_reader:
Logger.log("e", "No 3MF reader found, unable to paste.")
return
# Parse the scene from the clipboard
scene_string = QApplication.clipboard().text()
nodes = mesh_reader.stringToSceneNodes(scene_string)
if not nodes:
# Nothing to paste
return
# Find all fixed nodes, these are the nodes that should be avoided when arranging
fixed_nodes = []
root = application.getController().getScene().getRoot()
for node in DepthFirstIterator(root):
# Only count sliceable objects
if node.callDecoration("isSliceable"):
fixed_nodes.append(node)
# Add the new nodes to the scene, and arrange them
arranger = GridArrange(nodes, application.getBuildVolume(), fixed_nodes)
group_operation, not_fit_count = arranger.createGroupOperationForArrange(add_new_nodes_in_scene = True)
group_operation.push()
# deselect currently selected nodes, and select the new nodes
for node in Selection.getAllSelectedObjects():
Selection.remove(node)
for node in nodes:
Selection.add(node)
def _openUrl(self, url: QUrl) -> None:
QDesktopServices.openUrl(url)

View file

@ -5,6 +5,7 @@ import os
import sys
import tempfile
import time
import platform
from typing import cast, TYPE_CHECKING, Optional, Callable, List, Any, Dict
import numpy
@ -49,11 +50,11 @@ from UM.Settings.Validator import Validator
from UM.View.SelectionPass import SelectionPass # For typing.
from UM.Workspace.WorkspaceReader import WorkspaceReader
from UM.i18n import i18nCatalog
from UM.Version import Version
from cura import ApplicationMetadata
from cura.API import CuraAPI
from cura.API.Account import Account
from cura.Arranging.ArrangeObjectsJob import ArrangeObjectsJob
from cura.Arranging.Nest2DArrange import arrange
from cura.Machines.MachineErrorChecker import MachineErrorChecker
from cura.Machines.Models.BuildPlateModel import BuildPlateModel
from cura.Machines.Models.CustomQualityProfilesDropDownMenuModel import CustomQualityProfilesDropDownMenuModel
@ -114,6 +115,7 @@ from . import CameraAnimation
from . import CuraActions
from . import PlatformPhysics
from . import PrintJobPreviewImageProvider
from .Arranging.Nest2DArrange import Nest2DArrange
from .AutoSave import AutoSave
from .Machines.Models.CompatibleMachineModel import CompatibleMachineModel
from .Machines.Models.MachineListModel import MachineListModel
@ -205,6 +207,8 @@ class CuraApplication(QtApplication):
self._cura_scene_controller = None
self._machine_error_checker = None
self._backend_plugins: List[BackendPlugin] = []
self._machine_settings_manager = MachineSettingsManager(self, parent = self)
self._material_management_model = None
self._quality_management_model = None
@ -408,7 +412,9 @@ class CuraApplication(QtApplication):
SettingFunction.registerOperator("extruderValue", self._cura_formula_functions.getValueInExtruder)
SettingFunction.registerOperator("extruderValues", self._cura_formula_functions.getValuesInAllExtruders)
SettingFunction.registerOperator("anyExtruderNrWithOrDefault", self._cura_formula_functions.getAnyExtruderPositionWithOrDefault)
SettingFunction.registerOperator("anyExtruderWithMaterial", self._cura_formula_functions.getExtruderPositionWithMaterial)
SettingFunction.registerOperator("anyExtruderNrWithOrDefault",
self._cura_formula_functions.getAnyExtruderPositionWithOrDefault)
SettingFunction.registerOperator("resolveOrValue", self._cura_formula_functions.getResolveOrValue)
SettingFunction.registerOperator("defaultExtruderPosition", self._cura_formula_functions.getDefaultExtruderPosition)
SettingFunction.registerOperator("valueFromContainer", self._cura_formula_functions.getValueFromContainerAtIndex)
@ -494,6 +500,36 @@ class CuraApplication(QtApplication):
def startSplashWindowPhase(self) -> None:
"""Runs preparations that needs to be done before the starting process."""
self.setRequiredPlugins([
# Misc.:
"ConsoleLogger", # You want to be able to read the log if something goes wrong.
"CuraEngineBackend", # Cura is useless without this one since you can't slice.
"FileLogger", # You want to be able to read the log if something goes wrong.
"XmlMaterialProfile", # Cura crashes without this one.
"Marketplace",
# This contains the interface to enable/disable plug-ins, so if you disable it you can't enable it back.
"PrepareStage", # Cura is useless without this one since you can't load models.
"PreviewStage", # This shows the list of the plugin views that are installed in Cura.
"MonitorStage", # Major part of Cura's functionality.
"LocalFileOutputDevice", # Major part of Cura's functionality.
"LocalContainerProvider", # Cura is useless without any profiles or setting definitions.
# Views:
"SimpleView", # Dependency of SolidView.
"SolidView", # Displays models. Cura is useless without it.
# Readers & Writers:
"GCodeWriter", # Cura is useless if it can't write its output.
"STLReader", # Most common model format, so disabling this makes Cura 90% useless.
"3MFWriter", # Required for writing project files.
# Tools:
"CameraTool", # Needed to see the scene. Cura is useless without it.
"SelectionTool", # Dependency of the rest of the tools.
"TranslateTool", # You'll need this for almost every print.
])
# Plugins need to be set here, since in the super the check is done if they are actually loaded.
super().startSplashWindowPhase()
if not self.getIsHeadLess():
@ -502,33 +538,7 @@ class CuraApplication(QtApplication):
except FileNotFoundError:
Logger.log("w", "Unable to find the window icon.")
self.setRequiredPlugins([
# Misc.:
"ConsoleLogger", #You want to be able to read the log if something goes wrong.
"CuraEngineBackend", #Cura is useless without this one since you can't slice.
"FileLogger", #You want to be able to read the log if something goes wrong.
"XmlMaterialProfile", #Cura crashes without this one.
"Marketplace", #This contains the interface to enable/disable plug-ins, so if you disable it you can't enable it back.
"PrepareStage", #Cura is useless without this one since you can't load models.
"PreviewStage", #This shows the list of the plugin views that are installed in Cura.
"MonitorStage", #Major part of Cura's functionality.
"LocalFileOutputDevice", #Major part of Cura's functionality.
"LocalContainerProvider", #Cura is useless without any profiles or setting definitions.
# Views:
"SimpleView", #Dependency of SolidView.
"SolidView", #Displays models. Cura is useless without it.
# Readers & Writers:
"GCodeWriter", #Cura is useless if it can't write its output.
"STLReader", #Most common model format, so disabling this makes Cura 90% useless.
"3MFWriter", #Required for writing project files.
# Tools:
"CameraTool", #Needed to see the scene. Cura is useless without it.
"SelectionTool", #Dependency of the rest of the tools.
"TranslateTool", #You'll need this for almost every print.
])
self._i18n_catalog = i18nCatalog("cura")
self._update_platform_activity_timer = QTimer()
@ -609,6 +619,16 @@ class CuraApplication(QtApplication):
def _onEngineCreated(self):
self._qml_engine.addImageProvider("print_job_preview", PrintJobPreviewImageProvider.PrintJobPreviewImageProvider())
version = Version(self.getVersion())
if hasattr(sys, "frozen") and version.hasPostFix() and "beta" not in version.getPostfixType():
self._qml_engine.rootObjects()[0].setTitle(f"{ApplicationMetadata.CuraAppDisplayName} {ApplicationMetadata.CuraVersion}")
message = Message(
self._i18n_catalog.i18nc("@info:warning",
f"This version is not intended for production use. If you encounter any issues, please report them on our GitHub page, mentioning the full version {self.getVersion()}"),
lifetime = 0,
title = self._i18n_catalog.i18nc("@info:title", "Nightly build"),
message_type = Message.MessageType.WARNING)
message.show()
@pyqtProperty(bool)
def needToShowUserAgreement(self) -> bool:
@ -792,6 +812,7 @@ class CuraApplication(QtApplication):
self._plugin_registry.addType("profile_reader", self._addProfileReader)
self._plugin_registry.addType("profile_writer", self._addProfileWriter)
self._plugin_registry.addType("backend_plugin", self._addBackendPlugin)
if Platform.isLinux():
lib_suffixes = {"", "64", "32", "x32"} # A few common ones on different distributions.
@ -828,6 +849,8 @@ class CuraApplication(QtApplication):
def run(self):
super().run()
self._log_hardware_info()
if len(ApplicationMetadata.DEPENDENCY_INFO) > 0:
Logger.debug("Using Conan managed dependencies: " + ", ".join(
[dep["recipe"]["id"] for dep in ApplicationMetadata.DEPENDENCY_INFO["installed"] if dep["recipe"]["version"] != "latest"]))
@ -901,6 +924,14 @@ class CuraApplication(QtApplication):
self.exec()
def _log_hardware_info(self):
hardware_info = platform.uname()
Logger.info(f"System: {hardware_info.system}")
Logger.info(f"Release: {hardware_info.release}")
Logger.info(f"Version: {hardware_info.version}")
Logger.info(f"Processor name: {hardware_info.processor}")
Logger.info(f"CPU Cores: {os.cpu_count()}")
def __setUpSingleInstanceServer(self):
if self._use_single_instance:
self._single_instance.startServer()
@ -1427,6 +1458,13 @@ class CuraApplication(QtApplication):
# Single build plate
@pyqtSlot()
def arrangeAll(self) -> None:
self._arrangeAll(grid_arrangement = False)
@pyqtSlot()
def arrangeAllInGrid(self) -> None:
self._arrangeAll(grid_arrangement = True)
def _arrangeAll(self, *, grid_arrangement: bool) -> None:
nodes_to_arrange = []
active_build_plate = self.getMultiBuildPlateModel().activeBuildPlate
locked_nodes = []
@ -1456,17 +1494,17 @@ class CuraApplication(QtApplication):
locked_nodes.append(node)
else:
nodes_to_arrange.append(node)
self.arrange(nodes_to_arrange, locked_nodes)
self.arrange(nodes_to_arrange, locked_nodes, grid_arrangement = grid_arrangement)
def arrange(self, nodes: List[SceneNode], fixed_nodes: List[SceneNode]) -> None:
def arrange(self, nodes: List[SceneNode], fixed_nodes: List[SceneNode], *, grid_arrangement: bool = False) -> None:
"""Arrange a set of nodes given a set of fixed nodes
:param nodes: nodes that we have to place
:param fixed_nodes: nodes that are placed in the arranger before finding spots for nodes
:param grid_arrangement: If set to true if objects are to be placed in a grid
"""
min_offset = self.getBuildVolume().getEdgeDisallowedSize() + 2 # Allow for some rounding errors
job = ArrangeObjectsJob(nodes, fixed_nodes, min_offset = max(min_offset, 8))
job = ArrangeObjectsJob(nodes, fixed_nodes, min_offset = max(min_offset, 8), grid_arrange = grid_arrangement)
job.start()
@pyqtSlot()
@ -1730,6 +1768,13 @@ class CuraApplication(QtApplication):
def _addProfileWriter(self, profile_writer):
pass
def _addBackendPlugin(self, backend_plugin: "BackendPlugin") -> None:
self._container_registry.addAdditionalSettingDefinitionsAppender(backend_plugin)
self._backend_plugins.append(backend_plugin)
def getBackendPlugins(self) -> List["BackendPlugin"]:
return self._backend_plugins
@pyqtSlot("QSize")
def setMinimumWindowSize(self, size):
main_window = self.getMainWindow()
@ -1953,7 +1998,8 @@ class CuraApplication(QtApplication):
if select_models_on_load:
Selection.add(node)
try:
arrange(nodes_to_arrange, self.getBuildVolume(), fixed_nodes)
arranger = Nest2DArrange(nodes_to_arrange, self.getBuildVolume(), fixed_nodes)
arranger.arrange()
except:
Logger.logException("e", "Failed to arrange the models")

View file

@ -71,15 +71,15 @@ class IntentSelectionModel(ListModel):
def _update(self) -> None:
Logger.log("d", "Updating {model_class_name}.".format(model_class_name = self.__class__.__name__))
global_stack = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack()
cura_application = cura.CuraApplication.CuraApplication.getInstance()
global_stack = cura_application.getGlobalContainerStack()
if global_stack is None:
self.setItems([])
Logger.log("d", "No active GlobalStack, set quality profile model as empty.")
return
# Check for material compatibility
if not cura.CuraApplication.CuraApplication.getInstance().getMachineManager().activeMaterialsCompatible():
if not cura_application.getMachineManager().activeMaterialsCompatible():
Logger.log("d", "No active material compatibility, set quality profile model as empty.")
self.setItems([])
return
@ -101,17 +101,18 @@ class IntentSelectionModel(ListModel):
else:
# There can be multiple intents with the same category, use one of these
# intent-metadata's for the icon/description defintions for the intent
intent_metadata = cura.CuraApplication.CuraApplication \
.getInstance() \
.getContainerRegistry() \
.findContainersMetadata(type="intent", definition=global_stack.definition.getId(),
intent_category=category)[0]
intent_metadata = cura_application.getContainerRegistry().findContainersMetadata(type="intent",
definition=global_stack.findInstanceContainerDefinitionId(global_stack.definition),
intent_category=category)[0]
intent_name = intent_metadata.get("name", category.title())
icon = intent_metadata.get("icon", None)
description = intent_metadata.get("description", None)
if icon is not None:
if icon is not None and icon != '':
try:
icon = QUrl.fromLocalFile(
Resources.getPath(cura.CuraApplication.CuraApplication.ResourceTypes.ImageFiles, icon))

View file

@ -110,22 +110,22 @@ class MachineListModel(ListModel):
for abstract_machine in abstract_machine_stacks:
definition_id = abstract_machine.definition.getId()
online_machine_stacks = machines_manager.getMachinesWithDefinition(definition_id, online_only = True)
connected_machine_stacks = machines_manager.getMachinesWithDefinition(definition_id, online_only = False)
online_machine_stacks = list(filter(lambda machine: machine.hasNetworkedConnection(), online_machine_stacks))
online_machine_stacks.sort(key=lambda machine: machine.getName().upper())
connected_machine_stacks = list(filter(lambda machine: machine.hasNetworkedConnection(), connected_machine_stacks))
connected_machine_stacks.sort(key=lambda machine: machine.getName().upper())
if abstract_machine in other_machine_stacks:
other_machine_stacks.remove(abstract_machine)
if abstract_machine in online_machine_stacks:
online_machine_stacks.remove(abstract_machine)
if abstract_machine in connected_machine_stacks:
connected_machine_stacks.remove(abstract_machine)
# Create a list item for abstract machine
self.addItem(abstract_machine, True, len(online_machine_stacks))
self.addItem(abstract_machine, True, len(connected_machine_stacks))
# Create list of machines that are children of the abstract machine
for stack in online_machine_stacks:
for stack in connected_machine_stacks:
if self._show_cloud_printers:
self.addItem(stack, True)
# Remove this machine from the other stack list

View file

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

View file

@ -111,11 +111,7 @@ class ConvexHullDecorator(SceneNodeDecorator):
# Parent can be None if node is just loaded.
if self._isSingularOneAtATimeNode():
hull = self.getConvexHullHeadFull()
if hull is None:
return None
hull = self._add2DAdhesionMargin(hull)
return hull
return self.getConvexHullHeadFull()
return self._compute2DConvexHull()
@ -323,6 +319,7 @@ class ConvexHullDecorator(SceneNodeDecorator):
def _compute2DConvexHeadFull(self) -> Optional[Polygon]:
convex_hull = self._compute2DConvexHull()
convex_hull = self._add2DAdhesionMargin(convex_hull)
if convex_hull:
return convex_hull.getMinkowskiHull(self._getHeadAndFans())
return None

View file

@ -359,7 +359,7 @@ class CuraContainerStack(ContainerStack):
return self.definition
@classmethod
def _findInstanceContainerDefinitionId(cls, machine_definition: DefinitionContainerInterface) -> str:
def findInstanceContainerDefinitionId(cls, machine_definition: DefinitionContainerInterface) -> str:
"""Find the ID that should be used when searching for instance containers for a specified definition.
This handles the situation where the definition specifies we should use a different definition when
@ -379,7 +379,7 @@ class CuraContainerStack(ContainerStack):
Logger.log("w", "Unable to find parent definition {parent} for machine {machine}", parent = quality_definition, machine = machine_definition.id) #type: ignore
return machine_definition.id #type: ignore
return cls._findInstanceContainerDefinitionId(definitions[0])
return cls.findInstanceContainerDefinitionId(definitions[0])
def getExtruderPositionValueWithDefault(self, key):
"""getProperty for extruder positions, with translation from -1 to default extruder number"""

View file

@ -99,12 +99,21 @@ class CuraFormulaFunctions:
# Get the first extruder that adheres to a specific (boolean) property, like 'material_is_support_material'.
def getAnyExtruderPositionWithOrDefault(self, filter_key: str,
context: Optional["PropertyEvaluationContext"] = None) -> str:
context: Optional["PropertyEvaluationContext"] = None) -> str:
for extruder in self._getActiveExtruders(context):
value = extruder.getRawProperty(filter_key, "value", context=context)
if value is None or not value:
continue
return str(extruder.position)
# Get the first extruder with material that adheres to a specific (boolean) property, like 'material_is_support_material'.
def getExtruderPositionWithMaterial(self, filter_key: str,
context: Optional["PropertyEvaluationContext"] = None) -> str:
for extruder in self._getActiveExtruders(context):
material_container = extruder.material
value = material_container.getProperty(filter_key, "value", context)
if value is not None:
return str(extruder.position)
return self.getDefaultExtruderPosition()
# Get the resolve value or value for a given key.

View file

@ -54,7 +54,10 @@ There are also a few extra things that can be used in these expressions:
* The function `extruderValue(extruder, key)` will evaluate a particular setting for a particular extruder.
* The function `resolveOrValue(key)` will perform the full setting evaluation as described in this document for the current context (so if this setting is being evaluated for the second extruder it would perform it as if coming from the second extruder).
* The function `defaultExtruderPosition()` will get the first extruder that is not disabled. For instance, if a printer has three extruders but the first is disabled, this would return `1` to indicate the second extruder (0-indexed).
* The function `anyExtruderNrWithOrDefault(key)` will filter the list of extruders on the key, and then give the first index for which it is true, or if none of them are, the default one as specified by the 'default extruder position' function above.
* The function `anyExtruderNrWithOrDefault(key)` will filter the list of extruders on the key, and then give the first
index for which it is true, or if none of them are, the default one as specified by the 'default extruder position'
function above.
* The function `anyExtruderWithMaterial(key)` will filter the list of extruders on the key of material quality, and then give the first index for which it is true, or if none of them are, the default one as specified by the 'default extruder position' function above.
* The function `valueFromContainer(key, index)` will get a setting value from the global stack, but skip the first few containers in that stack. It will skip until it reaches a particular index in the container stack.
* The function `extruderValueFromContainer(key, index)` will get a setting value from the current extruder stack, but skip the first few containers in that stack. It will skip until it reaches a particular index in the container stack.

View file

@ -0,0 +1,141 @@
version: 1
AppDir:
path: {{ app_dir }}
app_info:
id: com.ultimaker.cura
name: UltiMaker Cura
icon: {{ icon }}
version: {{ version }}
exec: UltiMaker-Cura
exec_args: $@
apt:
arch:
- amd64
allow_unauthenticated: true
sources:
- sourceline: deb http://nl.archive.ubuntu.com/ubuntu/ jammy main restricted
- sourceline: deb http://nl.archive.ubuntu.com/ubuntu/ jammy-updates main restricted
- sourceline: deb http://nl.archive.ubuntu.com/ubuntu/ jammy universe
- sourceline: deb http://nl.archive.ubuntu.com/ubuntu/ jammy-updates universe
- sourceline: deb http://nl.archive.ubuntu.com/ubuntu/ jammy multiverse
- sourceline: deb http://nl.archive.ubuntu.com/ubuntu/ jammy-updates multiverse
- sourceline: deb http://nl.archive.ubuntu.com/ubuntu/ jammy-backports main restricted
universe multiverse
- sourceline: deb http://security.ubuntu.com/ubuntu jammy-security main restricted
- sourceline: deb http://security.ubuntu.com/ubuntu jammy-security universe
- sourceline: deb http://security.ubuntu.com/ubuntu jammy-security multiverse
- sourceline: deb https://releases.jfrog.io/artifactory/jfrog-debs xenial contrib
- sourceline: deb http://apt.llvm.org/jammy/ llvm-toolchain-jammy-14 main
- sourceline: deb https://ppa.launchpadcontent.net/ubuntu-toolchain-r/test/ubuntu/
jammy main
- sourceline: deb https://ppa.launchpadcontent.net/deadsnakes/ppa/ubuntu/ jammy
main
- sourceline: deb [arch=amd64] https://packages.microsoft.com/repos/ms-teams stable
main
- sourceline: deb https://ppa.launchpadcontent.net/ppa-verse/cling/ubuntu/ jammy
main
- sourceline: deb [arch=amd64] https://dl.google.com/linux/chrome/deb/ stable
main
- sourceline: deb [signed-by=/usr/share/keyrings/nodesource.gpg] https://deb.nodesource.com/node_14.x
jammy main
- sourceline: deb [arch=amd64 signed-by=/usr/share/keyrings/transip-stack.gpg]
https://mirror.transip.net/stack/software/deb/Ubuntu_22.04/ ./
- sourceline: deb http://repository.spotify.com stable non-free
- sourceline: deb [arch=amd64,arm64,armhf] http://packages.microsoft.com/repos/code
stable main
- sourceline: deb https://packagecloud.io/slacktechnologies/slack/debian/ jessie
main
include:
- libc6:amd64
- xdg-desktop-portal-kde:amd64
- libcap2:amd64
- libcom-err2:amd64
- libdbus-1-3:amd64
- libgpg-error0:amd64
- libgtk-3-common
- libkeyutils1:amd64
- libllvm13
- liblzma5:amd64
- libpcre3:amd64
- libqt6gui6
- libqt6qml6
- libqt6qmlworkerscript6
- libqt6quick6
- libselinux1:amd64
- libtinfo6:amd64
- qml6-module-qtqml-workerscript:amd64
- qml6-module-qtquick:amd64
- qt6-gtk-platformtheme:amd64
- qt6-qpa-plugins:amd64
# x11
- libx11-6
- libx11-xcb1
- libxcb1
- libxcb-render0
- libxcb-xfixes0
- libxcb-shape0
- libxcb-dri2-0
- libxcb-shm0
- libxcb-glx0
- libxcb-present0
- libxcb-dri3-0
# graphic libraries interface (safe graphics bundle including drivers, acceleration may not work in some systems)
- libglvnd0
- libglx0
- libglapi-mesa
- libgl1
- libegl1
- libgbm1
- libdrm2
- libglx-mesa0
- libgl1-amber-dri
- libgl1-mesa-dri
- mesa-utils
- libgl1-mesa-glx
- libdrm-amdgpu1
- libdrm-nouveau2
exclude:
- hicolor-icon-theme
- adwaita-icon-theme
- humanity-icon-theme
files:
include: []
exclude:
- usr/share/man
- usr/share/doc/*/README.*
- usr/share/doc/*/changelog.*
- usr/share/doc/*/NEWS.*
- usr/share/doc/*/TODO.*
runtime:
env:
APPDIR_LIBRARY_PATH: "$APPDIR/usr/lib/x86_64-linux-gnu:$APPDIR/lib/x86_64-linux-gnu:$APPDIR/usr/lib:$APPDIR/usr/lib/x86_64-linux-gnu/gdk-pixbuf-2.0/2.10.0/loaders"
PYTHONPATH: "$APPDIR"
QT_PLUGIN_PATH: "$APPDIR/qt/plugins"
QML2_IMPORT_PATH: "$APPDIR/qt/qml"
QT_QPA_PLATFORMTHEME: xdgdesktopportal
test:
fedora-30:
image: appimagecrafters/tests-env:fedora-30
command: ./AppRun
use_host_x: True
debian-stable:
image: appimagecrafters/tests-env:debian-stable
command: ./AppRun
use_host_x: True
archlinux-latest:
image: appimagecrafters/tests-env:archlinux-latest
command: ./AppRun
use_host_x: True
centos-7:
image: appimagecrafters/tests-env:centos-7
command: ./AppRun
use_host_x: True
ubuntu-xenial:
image: appimagecrafters/tests-env:ubuntu-xenial
command: ./AppRun
use_host_x: True
AppImage:
arch: {{ arch }}
file_name: {{ file_name }}
update-information: guess

View file

@ -0,0 +1,102 @@
# Copyright (c) 2023 UltiMaker
# Cura is released under the terms of the LGPLv3 or higher.
import argparse
import os
import shutil
import subprocess
from pathlib import Path
from jinja2 import Template
def prepare_workspace(dist_path, appimage_filename):
"""
Prepare the workspace for building the AppImage.
:param dist_path: Path to the distribution of Cura created with pyinstaller.
:param appimage_filename: name of the AppImage file.
:return:
"""
if not os.path.exists(dist_path):
raise RuntimeError(f"The dist_path {dist_path} does not exist.")
if os.path.exists(os.path.join(dist_path, appimage_filename)):
os.remove(os.path.join(dist_path, appimage_filename))
if not os.path.exists("AppDir"):
shutil.move(dist_path, "AppDir")
else:
print(f"AppDir already exists, assuming it is already prepared.")
copy_files("AppDir")
def build_appimage(dist_path, version, appimage_filename):
"""
Creates an AppImage file from the build artefacts created so far.
"""
generate_appimage_builder_config(dist_path, version, appimage_filename)
create_appimage()
sign_appimage(dist_path, appimage_filename)
def generate_appimage_builder_config(dist_path, version, appimage_filename):
with open(os.path.join(Path(__file__).parent, "AppImageBuilder.yml.jinja"), "r") as appimage_builder_file:
appimage_builder = appimage_builder_file.read()
template = Template(appimage_builder)
appimage_builder = template.render(app_dir = "./AppDir",
icon = "cura-icon.png",
version = version,
arch = "x86_64",
file_name = appimage_filename)
with open(os.path.join(Path(__file__).parent, "AppImageBuilder.yml"), "w") as appimage_builder_file:
appimage_builder_file.write(appimage_builder)
def copy_files(dist_path):
"""
Copy metadata files for the metadata of the AppImage.
"""
copied_files = {
os.path.join("..", "icons", "cura-icon.svg"): os.path.join("usr", "share", "icons", "hicolor", "scalable", "apps", "cura-icon.svg"),
os.path.join("..", "icons", "cura-icon_64x64.png"): os.path.join("usr", "share", "icons", "hicolor", "64x64", "apps", "cura-icon.png"),
os.path.join("..", "icons", "cura-icon_128x128.png"): os.path.join("usr", "share", "icons", "hicolor", "128x128", "apps", "cura-icon.png"),
os.path.join("..", "icons", "cura-icon_256x256.png"): os.path.join("usr", "share", "icons", "hicolor", "256x256", "apps", "cura-icon.png"),
os.path.join("..", "icons", "cura-icon_256x256.png"): "cura-icon.png",
}
# TODO: openssl.cnf ???
packaging_dir = os.path.dirname(__file__)
for source, dest in copied_files.items():
dest_file_path = os.path.join(dist_path, dest)
os.makedirs(os.path.dirname(dest_file_path), exist_ok = True)
shutil.copyfile(os.path.join(packaging_dir, source), dest_file_path)
def create_appimage():
appimagetool = os.getenv("APPIMAGEBUILDER_LOCATION", "appimage-builder-x86_64.AppImage")
command = [appimagetool, "--recipe", os.path.join(Path(__file__).parent, "AppImageBuilder.yml"), "--skip-test"]
result = subprocess.call(command)
if result != 0:
raise RuntimeError(f"The AppImageTool command returned non-zero: {result}")
def sign_appimage(dist_path, appimage_filename):
command = ["gpg", "--yes", "--armor", "--detach-sig", appimage_filename]
result = subprocess.call(command)
if result != 0:
raise RuntimeError(f"The GPG command returned non-zero: {result}")
if __name__ == "__main__":
parser = argparse.ArgumentParser(description = "Create AppImages of Cura.")
parser.add_argument("dist_path", type = str, help = "Path to where PyInstaller installed the distribution of Cura.")
parser.add_argument("version", type = str, help = "Full version number of Cura (e.g. '5.1.0-beta')")
parser.add_argument("filename", type = str, help = "Filename of the AppImage (e.g. 'UltiMaker-Cura-5.1.0-beta-Linux-X64.AppImage')")
args = parser.parse_args()
prepare_workspace(args.dist_path, args.filename)
build_appimage(args.dist_path, args.version, args.filename)

View file

@ -21,6 +21,7 @@ def build_dmg(source_path: str, dist_path: str, filename: str, app_name: str) ->
"--icon", app_name, "169", "272",
"--eula", f"{source_path}/packaging/cura_license.txt",
"--background", f"{source_path}/packaging/MacOs/cura_background_dmg.png",
"--hdiutil-quiet",
f"{dist_path}/{filename}",
f"{dist_path}/{app_name}"]
@ -138,18 +139,20 @@ def create_dmg(filename: str, dist_path: str, source_path: str, app_name: str) -
if __name__ == "__main__":
parser = argparse.ArgumentParser(description = "Create installer for Cura.")
parser.add_argument("source_path", type = str, help = "Path to Pyinstaller source folder")
parser.add_argument("dist_path", type = str, help = "Path to Pyinstaller dist folder")
parser.add_argument("cura_conan_version", type = str, help="The version of cura")
parser.add_argument("filename", type = str, help = "Filename of the pkg/dmg (e.g. 'UltiMaker-Cura-5.1.0-beta-Macos-X64.pkg' or 'UltiMaker-Cura-5.1.0-beta-Macos-X64.dmg')")
parser.add_argument("app_name", type = str, help = "Filename of the .app that will be contained within the dmg/pkg")
parser.add_argument("--source_path", required = True, type = str, help = "Path to Pyinstaller source folder")
parser.add_argument("--dist_path", required = True, type = str, help = "Path to Pyinstaller dist folder")
parser.add_argument("--cura_conan_version", required = True, type = str, help = "The version of cura")
parser.add_argument("--filename", required = True, type = str, help = "Filename of the pkg/dmg (e.g. 'UltiMaker-Cura-5.5.0-Macos-X64' or 'UltiMaker-Cura-5.5.0-beta.1-Macos-ARM64')")
parser.add_argument("--build_pkg", action="store_true", default = False, help = "build the pkg")
parser.add_argument("--build_dmg", action="store_true", default = True, help = "build the dmg")
parser.add_argument("--app_name", required = True, type = str, help = "Filename of the .app that will be contained within the dmg/pkg")
args = parser.parse_args()
cura_version = args.cura_conan_version.split("/")[-1]
app_name = f"{args.app_name}.app"
if Path(args.filename).suffix == ".pkg":
create_pkg_installer(args.filename, args.dist_path, cura_version, app_name)
else:
create_dmg(args.filename, args.dist_path, args.source_path, app_name)
if args.build_pkg:
create_pkg_installer(f"{args.filename}.pkg", args.dist_path, cura_version, app_name)
if args.build_dmg:
create_dmg(f"{args.filename}.dmg", args.dist_path, args.source_path, app_name)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 381 KiB

After

Width:  |  Height:  |  Size: 417 KiB

Before After
Before After

View file

@ -20,10 +20,15 @@
...but we can use this longer `substring` expression instead (see https://github.com/wixtoolset/issues/issues/5609 )
-->
<xsl:key
name="ExeToRemove"
match="wix:Component[ substring( wix:File/@Source, string-length( wix:File/@Source ) - 3 ) = '.exe' ]"
name="UltiMaker_Cura_exe_ToRemove"
match="wix:Component[ substring( wix:File/@Source, string-length( wix:File/@Source ) - 17 ) = 'UltiMaker-Cura.exe' ]"
use="@Id"
/> <!-- Get the last 4 characters of a string using `substring( s, len(s) - 3 )`, it uses -3 and not -4 because XSLT uses 1-based indexes, not 0-based indexes. -->
/>
<xsl:key
name="CuraEngine_exe_ToRemove"
match="wix:Component[ substring( wix:File/@Source, string-length( wix:File/@Source ) - 17 ) = 'CuraEngine.exe' ]"
use="@Id"
/>
<!-- By default, copy all elements and nodes into the output... -->
<xsl:template match="@*|node()">
@ -32,6 +37,7 @@
</xsl:copy>
</xsl:template>
<!-- ...but if the element has the "ExeToRemove" key then don't render anything (i.e. removing it from the output) -->
<xsl:template match="*[ self::wix:Component or self::wix:ComponentRef ][ key( 'ExeToRemove', @Id ) ]"/>
<!-- ...but if the element has the "UltiMaker_Cura_exe_ToRemove" or "CuraEngine_exe_ToRemove" key then don't render anything (i.e. removing it from the output) -->
<xsl:template match="*[ self::wix:Component or self::wix:ComponentRef ][ key( 'UltiMaker_Cura_exe_ToRemove', @Id ) ]"/>
<xsl:template match="*[ self::wix:Component or self::wix:ComponentRef ][ key( 'CuraEngine_exe_ToRemove', @Id ) ]"/>
</xsl:stylesheet>

View file

@ -56,7 +56,8 @@ class ThreeMFReader(MeshReader):
def emptyFileHintSet(self) -> bool:
return self._empty_project
def _createMatrixFromTransformationString(self, transformation: str) -> Matrix:
@staticmethod
def _createMatrixFromTransformationString(transformation: str) -> Matrix:
if transformation == "":
return Matrix()
@ -90,7 +91,8 @@ class ThreeMFReader(MeshReader):
return temp_mat
def _convertSavitarNodeToUMNode(self, savitar_node: Savitar.SceneNode, file_name: str = "") -> Optional[SceneNode]:
@staticmethod
def _convertSavitarNodeToUMNode(savitar_node: Savitar.SceneNode, file_name: str = "") -> Optional[SceneNode]:
"""Convenience function that converts a SceneNode object (as obtained from libSavitar) to a scene node.
:returns: Scene node.
@ -119,7 +121,7 @@ class ThreeMFReader(MeshReader):
pass
um_node.setName(node_name)
um_node.setId(node_id)
transformation = self._createMatrixFromTransformationString(savitar_node.getTransformation())
transformation = ThreeMFReader._createMatrixFromTransformationString(savitar_node.getTransformation())
um_node.setTransformation(transformation)
mesh_builder = MeshBuilder()
@ -138,7 +140,7 @@ class ThreeMFReader(MeshReader):
um_node.setMeshData(mesh_data)
for child in savitar_node.getChildren():
child_node = self._convertSavitarNodeToUMNode(child)
child_node = ThreeMFReader._convertSavitarNodeToUMNode(child)
if child_node:
um_node.addChild(child_node)
@ -214,7 +216,7 @@ class ThreeMFReader(MeshReader):
CuraApplication.getInstance().getController().getScene().setMetaDataEntry(key, value)
for node in scene_3mf.getSceneNodes():
um_node = self._convertSavitarNodeToUMNode(node, file_name)
um_node = ThreeMFReader._convertSavitarNodeToUMNode(node, file_name)
if um_node is None:
continue
@ -300,8 +302,23 @@ class ThreeMFReader(MeshReader):
if unit is None:
unit = "millimeter"
elif unit not in conversion_to_mm:
Logger.log("w", "Unrecognised unit {unit} used. Assuming mm instead.".format(unit = unit))
Logger.log("w", "Unrecognised unit {unit} used. Assuming mm instead.".format(unit=unit))
unit = "millimeter"
scale = conversion_to_mm[unit]
return Vector(scale, scale, scale)
@staticmethod
def stringToSceneNodes(scene_string: str) -> List[SceneNode]:
parser = Savitar.ThreeMFParser()
scene = parser.parse(scene_string)
# Convert the scene to scene nodes
nodes = []
for savitar_node in scene.getSceneNodes():
scene_node = ThreeMFReader._convertSavitarNodeToUMNode(savitar_node, "file_name")
if scene_node is None:
continue
nodes.append(scene_node)
return nodes

View file

@ -408,26 +408,27 @@ class WorkspaceDialog(QObject):
@pyqtSlot()
def showMissingMaterialsWarning(self) -> None:
result_message = Message(
i18n_catalog.i18nc("@info:status", "The material used in this project relies on some material definitions not available in Cura, this might produce undesirable print results. We highly recommend installing the full material package from the Marketplace."),
i18n_catalog.i18nc("@info:status",
"Some of the packages used in the project file are currently not installed in Cura, this might produce undesirable print results. We highly recommend installing the all required packages from the Marketplace."),
lifetime=0,
title=i18n_catalog.i18nc("@info:title", "Material profiles not installed"),
title=i18n_catalog.i18nc("@info:title", "Some required packages are not installed"),
message_type=Message.MessageType.WARNING
)
result_message.addAction(
"learn_more",
name=i18n_catalog.i18nc("@action:button", "Learn more"),
icon="",
description="Learn more about project materials.",
button_align=Message.ActionButtonAlignment.ALIGN_LEFT,
button_style=Message.ActionButtonStyle.LINK
"learn_more",
name=i18n_catalog.i18nc("@action:button", "Learn more"),
icon="",
description=i18n_catalog.i18nc("@label", "Learn more about project packages."),
button_align=Message.ActionButtonAlignment.ALIGN_LEFT,
button_style=Message.ActionButtonStyle.LINK
)
result_message.addAction(
"install_materials",
name=i18n_catalog.i18nc("@action:button", "Install Materials"),
icon="",
description="Install missing materials from project file.",
button_align=Message.ActionButtonAlignment.ALIGN_RIGHT,
button_style=Message.ActionButtonStyle.DEFAULT
"install_packages",
name=i18n_catalog.i18nc("@action:button", "Install Packages"),
icon="",
description=i18n_catalog.i18nc("@label", "Install missing packages from project file."),
button_align=Message.ActionButtonAlignment.ALIGN_RIGHT,
button_style=Message.ActionButtonStyle.DEFAULT
)
result_message.actionTriggered.connect(self._onMessageActionTriggered)
result_message.show()

View file

@ -364,7 +364,7 @@ UM.Dialog
UM.Label
{
id: warningText
text: catalog.i18nc("@label", "The material used in this project is currently not installed in Cura.<br/>Install the material profile and reopen the project.")
text: catalog.i18nc("@label", "This project contains materials or plugins that are currently not installed in Cura.<br/>Install the missing packages and reopen the project.")
}
}
@ -404,7 +404,7 @@ UM.Dialog
Cura.PrimaryButton
{
visible: warning
text: catalog.i18nc("@action:button", "Install missing material")
text: catalog.i18nc("@action:button", "Install missing packages")
onClicked: manager.installMissingPackages()
}
]

View file

@ -40,7 +40,9 @@ class ThreeMFWorkspaceWriter(WorkspaceWriter):
# Indicate that the 3mf mesh writer should not close the archive just yet (we still need to add stuff to it).
mesh_writer.setStoreArchive(True)
mesh_writer.write(stream, nodes, mode)
if not mesh_writer.write(stream, nodes, mode):
self.setInformation(mesh_writer.getInformation())
return False
archive = mesh_writer.getArchive()
if archive is None: # This happens if there was no mesh data to write.
@ -98,7 +100,7 @@ class ThreeMFWorkspaceWriter(WorkspaceWriter):
Logger.error("No permission to write workspace to this stream.")
return False
except EnvironmentError as e:
self.setInformation(catalog.i18nc("@error:zip", "The operating system does not allow saving a project file to this location or with this file name."))
self.setInformation(catalog.i18nc("@error:zip", str(e)))
Logger.error("EnvironmentError when writing workspace to this stream: {err}".format(err = str(e)))
return False
mesh_writer.setStoreArchive(False)

View file

@ -1,8 +1,9 @@
# Copyright (c) 2015-2022 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
import json
import re
from typing import Optional, cast, List, Dict
from typing import Optional, cast, List, Dict, Pattern, Set
from UM.Mesh.MeshWriter import MeshWriter
from UM.Math.Vector import Vector
@ -17,6 +18,7 @@ from UM.Settings.EmptyInstanceContainer import EmptyInstanceContainer
from cura.CuraApplication import CuraApplication
from cura.CuraPackageManager import CuraPackageManager
from cura.Settings import CuraContainerStack
from cura.Utils.Threading import call_on_qt_thread
from cura.Snapshot import Snapshot
@ -55,11 +57,12 @@ class ThreeMFWriter(MeshWriter):
"cura": "http://software.ultimaker.com/xml/cura/3mf/2015/10"
}
self._unit_matrix_string = self._convertMatrixToString(Matrix())
self._unit_matrix_string = ThreeMFWriter._convertMatrixToString(Matrix())
self._archive: Optional[zipfile.ZipFile] = None
self._store_archive = False
def _convertMatrixToString(self, matrix):
@staticmethod
def _convertMatrixToString(matrix):
result = ""
result += str(matrix._data[0, 0]) + " "
result += str(matrix._data[1, 0]) + " "
@ -83,7 +86,8 @@ class ThreeMFWriter(MeshWriter):
"""
self._store_archive = store_archive
def _convertUMNodeToSavitarNode(self, um_node, transformation = Matrix()):
@staticmethod
def _convertUMNodeToSavitarNode(um_node, transformation=Matrix()):
"""Convenience function that converts an Uranium SceneNode object to a SavitarSceneNode
:returns: Uranium Scene node.
@ -100,7 +104,7 @@ class ThreeMFWriter(MeshWriter):
node_matrix = um_node.getLocalTransformation()
matrix_string = self._convertMatrixToString(node_matrix.preMultiply(transformation))
matrix_string = ThreeMFWriter._convertMatrixToString(node_matrix.preMultiply(transformation))
savitar_node.setTransformation(matrix_string)
mesh_data = um_node.getMeshData()
@ -133,7 +137,7 @@ class ThreeMFWriter(MeshWriter):
# only save the nodes on the active build plate
if child_node.callDecoration("getBuildPlateNumber") != active_build_plate_nr:
continue
savitar_child_node = self._convertUMNodeToSavitarNode(child_node)
savitar_child_node = ThreeMFWriter._convertUMNodeToSavitarNode(child_node)
if savitar_child_node is not None:
savitar_node.addChild(savitar_child_node)
@ -175,13 +179,15 @@ class ThreeMFWriter(MeshWriter):
archive.writestr(thumbnail_file, thumbnail_buffer.data())
# Add PNG to content types file
thumbnail_type = ET.SubElement(content_types, "Default", Extension = "png", ContentType = "image/png")
thumbnail_type = ET.SubElement(content_types, "Default", Extension="png", ContentType="image/png")
# Add thumbnail relation to _rels/.rels file
thumbnail_relation_element = ET.SubElement(relations_element, "Relationship", Target = "/" + THUMBNAIL_PATH, Id = "rel1", Type = "http://schemas.openxmlformats.org/package/2006/relationships/metadata/thumbnail")
thumbnail_relation_element = ET.SubElement(relations_element, "Relationship",
Target="/" + THUMBNAIL_PATH, Id="rel1",
Type="http://schemas.openxmlformats.org/package/2006/relationships/metadata/thumbnail")
# Write material metadata
material_metadata = self._getMaterialPackageMetadata()
self._storeMetadataJson({"packages": material_metadata}, archive, PACKAGE_METADATA_PATH)
packages_metadata = self._getMaterialPackageMetadata() + self._getPluginPackageMetadata()
self._storeMetadataJson({"packages": packages_metadata}, archive, PACKAGE_METADATA_PATH)
savitar_scene = Savitar.Scene()
@ -221,7 +227,7 @@ class ThreeMFWriter(MeshWriter):
for node in nodes:
if node == root_node:
for root_child in node.getChildren():
savitar_node = self._convertUMNodeToSavitarNode(root_child, transformation_matrix)
savitar_node = ThreeMFWriter._convertUMNodeToSavitarNode(root_child, transformation_matrix)
if savitar_node:
savitar_scene.addSceneNode(savitar_node)
else:
@ -235,9 +241,9 @@ class ThreeMFWriter(MeshWriter):
archive.writestr(model_file, scene_string)
archive.writestr(content_types_file, b'<?xml version="1.0" encoding="UTF-8"?> \n' + ET.tostring(content_types))
archive.writestr(relations_file, b'<?xml version="1.0" encoding="UTF-8"?> \n' + ET.tostring(relations_element))
except Exception as e:
except Exception as error:
Logger.logException("e", "Error writing zip file")
self.setInformation(catalog.i18nc("@error:zip", "Error writing 3mf file."))
self.setInformation(str(error))
return False
finally:
if not self._store_archive:
@ -253,7 +259,64 @@ class ThreeMFWriter(MeshWriter):
metadata_file = zipfile.ZipInfo(path)
# We have to set the compress type of each file as well (it doesn't keep the type of the entire archive)
metadata_file.compress_type = zipfile.ZIP_DEFLATED
archive.writestr(metadata_file, json.dumps(metadata, separators=(", ", ": "), indent=4, skipkeys=True, ensure_ascii=False))
archive.writestr(metadata_file,
json.dumps(metadata, separators=(", ", ": "), indent=4, skipkeys=True, ensure_ascii=False))
@staticmethod
def _getPluginPackageMetadata() -> List[Dict[str, str]]:
"""Get metadata for all backend plugins that are used in the project.
:return: List of material metadata dictionaries.
"""
backend_plugin_enum_value_regex = re.compile(
r"PLUGIN::(?P<plugin_id>\w+)@(?P<version>\d+.\d+.\d+)::(?P<value>\w+)")
# This regex parses enum values to find if they contain custom
# backend engine values. These custom enum values are in the format
# PLUGIN::<plugin_id>@<version>::<value>
# where
# - plugin_id is the id of the plugin
# - version is in the semver format
# - value is the value of the enum
plugin_ids = set()
def addPluginIdsInStack(stack: CuraContainerStack) -> None:
for key in stack.getAllKeys():
value = str(stack.getProperty(key, "value"))
for plugin_id, _version, _value in backend_plugin_enum_value_regex.findall(value):
plugin_ids.add(plugin_id)
# Go through all stacks and find all the plugin id contained in the project
global_stack = CuraApplication.getInstance().getMachineManager().activeMachine
addPluginIdsInStack(global_stack)
for container in global_stack.getContainers():
addPluginIdsInStack(container)
for extruder_stack in global_stack.extruderList:
addPluginIdsInStack(extruder_stack)
for container in extruder_stack.getContainers():
addPluginIdsInStack(container)
metadata = {}
package_manager = cast(CuraPackageManager, CuraApplication.getInstance().getPackageManager())
for plugin_id in plugin_ids:
package_data = package_manager.getInstalledPackageInfo(plugin_id)
metadata[plugin_id] = {
"id": plugin_id,
"display_name": package_data.get("display_name") if package_data.get("display_name") else "",
"package_version": package_data.get("package_version") if package_data.get("package_version") else "",
"sdk_version_semver": package_data.get("sdk_version_semver") if package_data.get(
"sdk_version_semver") else "",
"type": "plugin",
}
# Storing in a dict and fetching values to avoid duplicates
return list(metadata.values())
@staticmethod
def _getMaterialPackageMetadata() -> List[Dict[str, str]]:
@ -278,7 +341,8 @@ class ThreeMFWriter(MeshWriter):
# Don't export bundled materials
continue
package_id = package_manager.getMaterialFilePackageId(extruder.material.getFileName(), extruder.material.getMetaDataEntry("GUID"))
package_id = package_manager.getMaterialFilePackageId(extruder.material.getFileName(),
extruder.material.getMetaDataEntry("GUID"))
package_data = package_manager.getInstalledPackageInfo(package_id)
# We failed to find the package for this material
@ -286,10 +350,14 @@ class ThreeMFWriter(MeshWriter):
Logger.info(f"Could not find package for material in extruder {extruder.id}, skipping.")
continue
material_metadata = {"id": package_id,
"display_name": package_data.get("display_name") if package_data.get("display_name") else "",
"package_version": package_data.get("package_version") if package_data.get("package_version") else "",
"sdk_version_semver": package_data.get("sdk_version_semver") if package_data.get("sdk_version_semver") else ""}
material_metadata = {
"id": package_id,
"display_name": package_data.get("display_name") if package_data.get("display_name") else "",
"package_version": package_data.get("package_version") if package_data.get("package_version") else "",
"sdk_version_semver": package_data.get("sdk_version_semver") if package_data.get(
"sdk_version_semver") else "",
"type": "material",
}
metadata[package_id] = material_metadata
@ -303,9 +371,19 @@ class ThreeMFWriter(MeshWriter):
Logger.log("w", "Can't create snapshot when renderer not initialized.")
return None
try:
snapshot = Snapshot.snapshot(width = 300, height = 300)
snapshot = Snapshot.snapshot(width=300, height=300)
except:
Logger.logException("w", "Failed to create snapshot image")
return None
return snapshot
@staticmethod
def sceneNodesToString(scene_nodes: [SceneNode]) -> str:
savitar_scene = Savitar.Scene()
for scene_node in scene_nodes:
savitar_node = ThreeMFWriter._convertUMNodeToSavitarNode(scene_node)
savitar_scene.addSceneNode(savitar_node)
parser = Savitar.ThreeMFParser()
scene_string = parser.sceneToString(savitar_scene)
return scene_string

View file

@ -0,0 +1,60 @@
import sys
import os.path
from typing import Dict, Optional
import pytest
from unittest.mock import patch, MagicMock, PropertyMock
from UM.PackageManager import PackageManager
from cura.CuraApplication import CuraApplication
sys.path.append(os.path.join(os.path.dirname(os.path.abspath(__file__)), ".."))
import ThreeMFWriter
PLUGIN_ID = "my_plugin"
DISPLAY_NAME = "MyPlugin"
PACKAGE_VERSION = "0.0.1"
SDK_VERSION = "8.0.0"
@pytest.fixture
def package_manager() -> MagicMock:
pm = MagicMock(spec=PackageManager)
pm.getInstalledPackageInfo.return_value = {
"display_name": DISPLAY_NAME,
"package_version": PACKAGE_VERSION,
"sdk_version_semver": SDK_VERSION
}
return pm
@pytest.fixture
def machine_manager() -> MagicMock:
mm = MagicMock(spec=PackageManager)
active_machine = MagicMock()
active_machine.getAllKeys.return_value = ["infill_pattern", "layer_height", "material_bed_temperature"]
active_machine.getProperty.return_value = f"PLUGIN::{PLUGIN_ID}@{PACKAGE_VERSION}::custom_value"
active_machine.getContainers.return_value = []
active_machine.extruderList = []
mm.activeMachine = active_machine
return mm
@pytest.fixture
def application(package_manager, machine_manager):
app = MagicMock()
app.getPackageManager.return_value = package_manager
app.getMachineManager.return_value = machine_manager
return app
def test_enumParsing(application):
with patch("cura.CuraApplication.CuraApplication.getInstance", MagicMock(return_value=application)):
packages_metadata = ThreeMFWriter.ThreeMFWriter._getPluginPackageMetadata()[0]
assert packages_metadata.get("id") == PLUGIN_ID
assert packages_metadata.get("display_name") == DISPLAY_NAME
assert packages_metadata.get("package_version") == PACKAGE_VERSION
assert packages_metadata.get("sdk_version_semver") == SDK_VERSION
assert packages_metadata.get("type") == "plugin"

View file

@ -8,12 +8,31 @@ message ObjectList
repeated Setting settings = 2; // meshgroup settings (for one-at-a-time printing)
}
enum SlotID {
SETTINGS_BROADCAST = 0;
SIMPLIFY_MODIFY = 100;
POSTPROCESS_MODIFY = 101;
INFILL_MODIFY = 102;
GCODE_PATHS_MODIFY = 103;
INFILL_GENERATE = 200;
}
message EnginePlugin
{
SlotID id = 1;
string address = 2;
uint32 port = 3;
string plugin_name = 4;
string plugin_version = 5;
}
message Slice
{
repeated ObjectList object_lists = 1; // The meshgroups to be printed one after another
SettingList global_settings = 2; // The global settings used for the whole print job
repeated Extruder extruders = 3; // The settings sent to each extruder object
repeated SettingExtruder limit_to_extruder = 4; // From which stack the setting would inherit if not defined per object
repeated EnginePlugin engine_plugins = 5;
}
message Extruder

View file

@ -46,6 +46,19 @@ catalog = i18nCatalog("cura")
class CuraEngineBackend(QObject, Backend):
backendError = Signal()
printDurationMessage = Signal()
"""Emitted when we get a message containing print duration and material amount.
This also implies the slicing has finished.
:param time: The amount of time the print will take.
:param material_amount: The amount of material the print will use.
"""
slicingStarted = Signal()
"""Emitted when the slicing process starts."""
slicingCancelled = Signal()
"""Emitted when the slicing process is aborted forcefully."""
def __init__(self) -> None:
"""Starts the back-end plug-in.
@ -70,7 +83,7 @@ class CuraEngineBackend(QObject, Backend):
os.path.join(CuraApplication.getInstallPrefix(), "bin"),
os.path.dirname(os.path.abspath(sys.executable)),
]
self._last_backend_plugin_port = self._port + 1000
for path in search_path:
engine_path = os.path.join(path, executable_name)
if os.path.isfile(engine_path):
@ -86,9 +99,9 @@ class CuraEngineBackend(QObject, Backend):
self._default_engine_location = execpath
break
application = CuraApplication.getInstance() #type: CuraApplication
self._multi_build_plate_model = None #type: Optional[MultiBuildPlateModel]
self._machine_error_checker = None #type: Optional[MachineErrorChecker]
application: CuraApplication = CuraApplication.getInstance()
self._multi_build_plate_model: Optional[MultiBuildPlateModel] = None
self._machine_error_checker: Optional[MachineErrorChecker] = None
if not self._default_engine_location:
raise EnvironmentError("Could not find CuraEngine")
@ -99,13 +112,15 @@ class CuraEngineBackend(QObject, Backend):
application.getPreferences().addPreference("backend/location", self._default_engine_location)
# Workaround to disable layer view processing if layer view is not active.
self._layer_view_active = False #type: bool
self._layer_view_active: bool = False
self._onActiveViewChanged()
self._stored_layer_data = [] # type: List[Arcus.PythonMessage]
self._stored_optimized_layer_data = {} # type: Dict[int, List[Arcus.PythonMessage]] # key is build plate number, then arrays are stored until they go to the ProcessSlicesLayersJob
self._stored_layer_data: List[Arcus.PythonMessage] = []
self._scene = application.getController().getScene() #type: Scene
# key is build plate number, then arrays are stored until they go to the ProcessSlicesLayersJob
self._stored_optimized_layer_data: Dict[int, List[Arcus.PythonMessage]] = {}
self._scene: Scene = application.getController().getScene()
self._scene.sceneChanged.connect(self._onSceneChanged)
# Triggers for auto-slicing. Auto-slicing is triggered as follows:
@ -116,7 +131,7 @@ class CuraEngineBackend(QObject, Backend):
# If there is an error check, stop the auto-slicing timer, and only wait for the error check to be finished
# to start the auto-slicing timer again.
#
self._global_container_stack = None #type: Optional[ContainerStack]
self._global_container_stack: Optional[ContainerStack] = None
# Listeners for receiving messages from the back-end.
self._message_handlers["cura.proto.Layer"] = self._onLayerMessage
@ -128,31 +143,34 @@ class CuraEngineBackend(QObject, Backend):
self._message_handlers["cura.proto.PrintTimeMaterialEstimates"] = self._onPrintTimeMaterialEstimates
self._message_handlers["cura.proto.SlicingFinished"] = self._onSlicingFinishedMessage
self._start_slice_job = None #type: Optional[StartSliceJob]
self._start_slice_job_build_plate = None #type: Optional[int]
self._slicing = False #type: bool # Are we currently slicing?
self._restart = False #type: bool # Back-end is currently restarting?
self._tool_active = False #type: bool # If a tool is active, some tasks do not have to do anything
self._always_restart = True #type: bool # Always restart the engine when starting a new slice. Don't keep the process running. TODO: Fix engine statelessness.
self._process_layers_job = None #type: Optional[ProcessSlicedLayersJob] # The currently active job to process layers, or None if it is not processing layers.
self._build_plates_to_be_sliced = [] #type: List[int] # what needs slicing?
self._engine_is_fresh = True #type: bool # Is the newly started engine used before or not?
self._start_slice_job: Optional[StartSliceJob] = None
self._start_slice_job_build_plate: Optional[int] = None
self._slicing: bool = False # Are we currently slicing?
self._restart: bool = False # Back-end is currently restarting?
self._tool_active: bool = False # If a tool is active, some tasks do not have to do anything
self._always_restart: bool = True # Always restart the engine when starting a new slice. Don't keep the process running. TODO: Fix engine statelessness.
self._process_layers_job: Optional[ProcessSlicedLayersJob] = None # The currently active job to process layers, or None if it is not processing layers.
self._build_plates_to_be_sliced: List[int] = [] # what needs slicing?
self._engine_is_fresh: bool = True # Is the newly started engine used before or not?
self._backend_log_max_lines = 20000 #type: int # Maximum number of lines to buffer
self._error_message = None #type: Optional[Message] # Pop-up message that shows errors.
self._last_num_objects = defaultdict(int) #type: Dict[int, int] # Count number of objects to see if there is something changed
self._postponed_scene_change_sources = [] #type: List[SceneNode] # scene change is postponed (by a tool)
self._backend_log_max_lines: int = 20000 # Maximum number of lines to buffer
self._error_message: Optional[Message] = None # Pop-up message that shows errors.
self._time_start_process = None #type: Optional[float]
self._is_disabled = False #type: bool
# Count number of objects to see if there is something changed
self._last_num_objects: Dict[int, int] = defaultdict(int)
self._postponed_scene_change_sources: List[SceneNode] = [] # scene change is postponed (by a tool)
self._time_start_process: Optional[float] = None
self._is_disabled: bool = False
application.getPreferences().addPreference("general/auto_slice", False)
self._use_timer = False #type: bool
# When you update a setting and other settings get changed through inheritance, many propertyChanged signals are fired.
# This timer will group them up, and only slice for the last setting changed signal.
self._use_timer: bool = False
# When you update a setting and other settings get changed through inheritance, many propertyChanged
# signals are fired. This timer will group them up, and only slice for the last setting changed signal.
# TODO: Properly group propertyChanged signals by whether they are triggered by the same user interaction.
self._change_timer = QTimer() #type: QTimer
self._change_timer: QTimer = QTimer()
self._change_timer.setSingleShot(True)
self._change_timer.setInterval(500)
self.determineAutoSlicing()
@ -172,10 +190,34 @@ class CuraEngineBackend(QObject, Backend):
self._slicing_error_message.actionTriggered.connect(self._reportBackendError)
self._resetLastSliceTimeStats()
self._snapshot = None #type: Optional[QImage]
self._snapshot: Optional[QImage] = None
application.initializationFinished.connect(self.initialize)
def startPlugins(self) -> None:
"""
Ensure that all backend plugins are started
It assigns unique ports to each plugin to avoid conflicts.
:return:
"""
self.stopPlugins()
backend_plugins = CuraApplication.getInstance().getBackendPlugins()
for backend_plugin in backend_plugins:
# Set the port to prevent plugins from using the same one.
if backend_plugin.getPort() < 1:
backend_plugin.setPort(self._last_backend_plugin_port)
self._last_backend_plugin_port += 1
backend_plugin.start()
def stopPlugins(self) -> None:
"""
Ensure that all backend plugins will be terminated.
"""
backend_plugins = CuraApplication.getInstance().getBackendPlugins()
for backend_plugin in backend_plugins:
if backend_plugin.isRunning():
backend_plugin.stop()
def _resetLastSliceTimeStats(self) -> None:
self._time_start_process = None
self._time_send_message = None
@ -202,7 +244,8 @@ class CuraEngineBackend(QObject, Backend):
application.getMachineManager().globalContainerChanged.connect(self._onGlobalStackChanged)
self._onGlobalStackChanged()
# extruder enable / disable. Actually wanted to use machine manager here, but the initialization order causes it to crash
# Extruder enable / disable. Actually wanted to use machine manager here,
# but the initialization order causes it to crash
ExtruderManager.getInstance().extrudersChanged.connect(self._extruderChanged)
self.backendQuit.connect(self._onBackendQuit)
@ -239,26 +282,14 @@ class CuraEngineBackend(QObject, Backend):
command += ["connect", "127.0.0.1:{0}".format(self._port), ""]
parser = argparse.ArgumentParser(prog = "cura", add_help = False)
parser.add_argument("--debug", action = "store_true", default = False, help = "Turn on the debug mode by setting this option.")
parser.add_argument("--debug", action = "store_true", default = False,
help = "Turn on the debug mode by setting this option.")
known_args = vars(parser.parse_known_args()[0])
if known_args["debug"]:
command.append("-vvv")
return command
printDurationMessage = Signal()
"""Emitted when we get a message containing print duration and material amount.
This also implies the slicing has finished.
:param time: The amount of time the print will take.
:param material_amount: The amount of material the print will use.
"""
slicingStarted = Signal()
"""Emitted when the slicing process starts."""
slicingCancelled = Signal()
"""Emitted when the slicing process is aborted forcefully."""
@pyqtSlot()
def stopSlicing(self) -> None:
self.setState(BackendState.NotStarted)
@ -266,7 +297,8 @@ class CuraEngineBackend(QObject, Backend):
self._terminate()
self._createSocket()
if self._process_layers_job is not None: # We were processing layers. Stop that, the layers are going to change soon.
if self._process_layers_job is not None:
# We were processing layers. Stop that, the layers are going to change soon.
Logger.log("i", "Aborting process layers job...")
self._process_layers_job.abort()
self._process_layers_job = None
@ -281,7 +313,7 @@ class CuraEngineBackend(QObject, Backend):
self.markSliceAll()
self.slice()
@call_on_qt_thread # must be called from the main thread because of OpenGL
@call_on_qt_thread # Must be called from the main thread because of OpenGL
def _createSnapshot(self) -> None:
self._snapshot = None
if not CuraApplication.getInstance().isVisible:
@ -290,7 +322,7 @@ class CuraEngineBackend(QObject, Backend):
Logger.log("i", "Creating thumbnail image (just before slice)...")
try:
self._snapshot = Snapshot.snapshot(width = 300, height = 300)
except:
except Exception:
Logger.logException("w", "Failed to create snapshot image")
self._snapshot = None # Failing to create thumbnail should not fail creation of UFP
@ -302,6 +334,8 @@ class CuraEngineBackend(QObject, Backend):
self._createSnapshot()
self.startPlugins()
Logger.log("i", "Starting to slice...")
self._time_start_process = time()
if not self._build_plates_to_be_sliced:
@ -315,7 +349,8 @@ class CuraEngineBackend(QObject, Backend):
return
if not hasattr(self._scene, "gcode_dict"):
self._scene.gcode_dict = {} #type: ignore #Because we are creating the missing attribute here.
self._scene.gcode_dict = {} # type: ignore
# We need to ignore type because we are creating the missing attribute here.
# see if we really have to slice
application = CuraApplication.getInstance()
@ -326,9 +361,9 @@ class CuraEngineBackend(QObject, Backend):
self._stored_layer_data = []
if build_plate_to_be_sliced not in num_objects or num_objects[build_plate_to_be_sliced] == 0:
self._scene.gcode_dict[build_plate_to_be_sliced] = [] #type: ignore #Because we created this attribute above.
self._scene.gcode_dict[build_plate_to_be_sliced] = [] # type: ignore
# We need to ignore the type because we created this attribute above.
Logger.log("d", "Build plate %s has no objects to be sliced, skipping", build_plate_to_be_sliced)
if self._build_plates_to_be_sliced:
self.slice()
@ -337,7 +372,7 @@ class CuraEngineBackend(QObject, Backend):
if application.getPrintInformation() and build_plate_to_be_sliced == active_build_plate:
application.getPrintInformation().setToZeroPrintInformation(build_plate_to_be_sliced)
if self._process is None: # type: ignore
if self._process is None: # type: ignore
self._createSocket()
self.stopSlicing()
self._engine_is_fresh = False # Yes we're going to use the engine
@ -345,7 +380,7 @@ class CuraEngineBackend(QObject, Backend):
self.processingProgress.emit(0.0)
self.backendStateChange.emit(BackendState.NotStarted)
self._scene.gcode_dict[build_plate_to_be_sliced] = [] #type: ignore #[] indexed by build plate number
self._scene.gcode_dict[build_plate_to_be_sliced] = [] # type: ignore #[] indexed by build plate number
self._slicing = True
self.slicingStarted.emit()
@ -370,6 +405,8 @@ class CuraEngineBackend(QObject, Backend):
if self._start_slice_job is not None:
self._start_slice_job.cancel()
self.stopPlugins()
self.slicingCancelled.emit()
self.processingProgress.emit(0)
Logger.log("d", "Attempting to kill the engine process")
@ -377,14 +414,15 @@ class CuraEngineBackend(QObject, Backend):
if CuraApplication.getInstance().getUseExternalBackend():
return
if self._process is not None: # type: ignore
if self._process is not None: # type: ignore
Logger.log("d", "Killing engine process")
try:
self._process.terminate() # type: ignore
Logger.log("d", "Engine process is killed. Received return code %s", self._process.wait()) # type: ignore
self._process = None # type: ignore
self._process.terminate() # type: ignore
Logger.log("d", "Engine process is killed. Received return code %s", self._process.wait()) # type: ignore
self._process = None # type: ignore
except Exception as e: # terminating a process that is already terminating causes an exception, silently ignore this.
except Exception as e:
# Terminating a process that is already terminating causes an exception, silently ignore this.
Logger.log("d", "Exception occurred while trying to kill the engine %s", str(e))
def _onStartSliceCompleted(self, job: StartSliceJob) -> None:
@ -429,14 +467,14 @@ class CuraEngineBackend(QObject, Backend):
Logger.log("w", "Global container stack not assigned to CuraEngineBackend!")
return
extruders = ExtruderManager.getInstance().getActiveExtruderStacks()
error_keys = [] #type: List[str]
error_keys: List[str] = []
for extruder in extruders:
error_keys.extend(extruder.getErrorKeys())
if not extruders:
error_keys = self._global_container_stack.getErrorKeys()
error_labels = set()
for key in error_keys:
for stack in [self._global_container_stack] + extruders: #Search all container stacks for the definition of this setting. Some are only in an extruder stack.
for stack in [self._global_container_stack] + extruders: #Search all container stacks for the definition of this setting. Some are only in an extruder stack.
definitions = cast(DefinitionContainerInterface, stack.getBottom()).findDefinitions(key = key)
if definitions:
break #Found it! No need to continue search.
@ -524,7 +562,7 @@ class CuraEngineBackend(QObject, Backend):
# Preparation completed, send it to the backend.
self._socket.sendMessage(job.getSliceMessage())
# Notify the user that it's now up to the backend to do it's job
# Notify the user that it's now up to the backend to do its job
self.setState(BackendState.Processing)
# Handle time reporting.
@ -551,7 +589,8 @@ class CuraEngineBackend(QObject, Backend):
self._is_disabled = True
gcode_list = node.callDecoration("getGCodeList")
if gcode_list is not None:
self._scene.gcode_dict[node.callDecoration("getBuildPlateNumber")] = gcode_list #type: ignore #Because we generate this attribute dynamically.
self._scene.gcode_dict[node.callDecoration("getBuildPlateNumber")] = gcode_list # type: ignore
# We need to ignore type because we generate this attribute dynamically.
if self._use_timer == enable_timer:
return self._use_timer
@ -566,7 +605,7 @@ class CuraEngineBackend(QObject, Backend):
def _numObjectsPerBuildPlate(self) -> Dict[int, int]:
"""Return a dict with number of objects per build plate"""
num_objects = defaultdict(int) #type: Dict[int, int]
num_objects: Dict[int, int] = defaultdict(int)
for node in DepthFirstIterator(self._scene.getRoot()):
# Only count sliceable objects
if node.callDecoration("isSliceable"):
@ -646,11 +685,13 @@ class CuraEngineBackend(QObject, Backend):
self._terminate()
self._createSocket()
if error.getErrorCode() not in [Arcus.ErrorCode.BindFailedError, Arcus.ErrorCode.ConnectionResetError, Arcus.ErrorCode.Debug]:
if error.getErrorCode() not in [Arcus.ErrorCode.BindFailedError,
Arcus.ErrorCode.ConnectionResetError,
Arcus.ErrorCode.Debug]:
Logger.log("w", "A socket error caused the connection to be reset")
# _terminate()' function sets the job status to 'cancel', after reconnecting to another Port the job status
# needs to be updated. Otherwise backendState is "Unable To Slice"
# needs to be updated. Otherwise, backendState is "Unable To Slice"
if error.getErrorCode() == Arcus.ErrorCode.BindFailedError and self._start_slice_job is not None:
self._start_slice_job.setIsCancelled(False)
@ -672,7 +713,7 @@ class CuraEngineBackend(QObject, Backend):
for node in DepthFirstIterator(self._scene.getRoot()):
if node.callDecoration("getLayerData"):
if not build_plate_numbers or node.callDecoration("getBuildPlateNumber") in build_plate_numbers:
# We can assume that all nodes have a parent as we're looping through the scene (and filter out root)
# We can assume that all nodes have a parent as we're looping through the scene and filter out root
cast(SceneNode, node.getParent()).removeChild(node)
def markSliceAll(self) -> None:
@ -701,7 +742,7 @@ class CuraEngineBackend(QObject, Backend):
:param instance: The setting instance that has changed.
:param property: The property of the setting instance that has changed.
"""
if property == "value": # Only reslice if the value has changed.
if property == "value": # Only re-slice if the value has changed.
self.needsSlicing()
self._onChanged()
@ -765,13 +806,17 @@ class CuraEngineBackend(QObject, Backend):
:param message: The protobuf message signalling that slicing is finished.
"""
self.stopPlugins()
self.setState(BackendState.Done)
self.processingProgress.emit(1.0)
self._time_end_slice = time()
try:
gcode_list = self._scene.gcode_dict[self._start_slice_job_build_plate] #type: ignore #Because we generate this attribute dynamically.
except KeyError: # Can occur if the g-code has been cleared while a slice message is still arriving from the other end.
gcode_list = self._scene.gcode_dict[self._start_slice_job_build_plate] #type: ignore
# We need to ignore the type because it was generated dynamically.
except KeyError:
# Can occur if the g-code has been cleared while a slice message is still arriving from the other end.
gcode_list = []
application = CuraApplication.getInstance()
for index, line in enumerate(gcode_list):
@ -816,7 +861,8 @@ class CuraEngineBackend(QObject, Backend):
try:
self._scene.gcode_dict[self._start_slice_job_build_plate].append(message.data.decode("utf-8", "replace")) #type: ignore #Because we generate this attribute dynamically.
except KeyError: # Can occur if the g-code has been cleared while a slice message is still arriving from the other end.
except KeyError:
# Can occur if the g-code has been cleared while a slice message is still arriving from the other end.
pass # Throw the message away.
def _onGCodePrefixMessage(self, message: Arcus.PythonMessage) -> None:
@ -828,7 +874,8 @@ class CuraEngineBackend(QObject, Backend):
try:
self._scene.gcode_dict[self._start_slice_job_build_plate].insert(0, message.data.decode("utf-8", "replace")) #type: ignore #Because we generate this attribute dynamically.
except KeyError: # Can occur if the g-code has been cleared while a slice message is still arriving from the other end.
except KeyError:
# Can occur if the g-code has been cleared while a slice message is still arriving from the other end.
pass # Throw the message away.
def _onSliceUUIDMessage(self, message: Arcus.PythonMessage) -> None:
@ -955,7 +1002,8 @@ class CuraEngineBackend(QObject, Backend):
view = CuraApplication.getInstance().getController().getActiveView()
if view:
active_build_plate = CuraApplication.getInstance().getMultiBuildPlateModel().activeBuildPlate
if view.getPluginId() == "SimulationView": # If switching to layer view, we should process the layers if that hasn't been done yet.
if view.getPluginId() == "SimulationView":
# If switching to layer view, we should process the layers if that hasn't been done yet.
self._layer_view_active = True
# There is data and we're not slicing at the moment
# if we are slicing, there is no need to re-calculate the data as it will be invalid in a moment.
@ -974,7 +1022,6 @@ class CuraEngineBackend(QObject, Backend):
We should reset our state and start listening for new connections.
"""
if not self._restart:
if self._process: # type: ignore
return_code = self._process.wait()
@ -985,6 +1032,7 @@ class CuraEngineBackend(QObject, Backend):
self.stopSlicing()
else:
Logger.log("d", "Backend finished slicing. Resetting process and socket.")
self.stopPlugins()
self._process = None # type: ignore
def _reportBackendError(self, _message_id: str, _action_id: str) -> None:
@ -1007,7 +1055,8 @@ class CuraEngineBackend(QObject, Backend):
self._global_container_stack = CuraApplication.getInstance().getMachineManager().activeMachine
if self._global_container_stack:
self._global_container_stack.propertyChanged.connect(self._onSettingChanged) # Note: Only starts slicing when the value changed.
# Note: Only starts slicing when the value changed.
self._global_container_stack.propertyChanged.connect(self._onSettingChanged)
self._global_container_stack.containersChanged.connect(self._onChanged)
for extruder in self._global_container_stack.extruderList:

View file

@ -1,5 +1,6 @@
# Copyright (c) 2021-2022 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
import os
import numpy
from string import Formatter
@ -301,6 +302,21 @@ class StartSliceJob(Job):
for extruder_stack in global_stack.extruderList:
self._buildExtruderMessage(extruder_stack)
for plugin in CuraApplication.getInstance().getBackendPlugins():
for slot in plugin.getSupportedSlots():
# Right now we just send the message for every slot that we support. A single plugin can support
# multiple slots
# In the future the frontend will need to decide what slots that a plugin actually supports should
# also be used. For instance, if you have two plugins and each of them support a_generate and b_generate
# only one of each can actually be used (eg; plugin 1 does both, plugin 1 does a_generate and 2 does
# b_generate, etc).
plugin_message = self._slice_message.addRepeatedMessage("engine_plugins")
plugin_message.id = slot
plugin_message.address = plugin.getAddress()
plugin_message.port = plugin.getPort()
plugin_message.plugin_name = plugin.getPluginId()
plugin_message.plugin_version = plugin.getVersion()
for group in filtered_object_groups:
group_message = self._slice_message.addRepeatedMessage("object_lists")
parent = group[0].getParent()

View file

@ -20,7 +20,6 @@ class MissingPackageList(RemotePackageList):
def __init__(self, packages_metadata: List[Dict[str, str]], parent: Optional["QObject"] = None) -> None:
super().__init__(parent)
self._packages_metadata: List[Dict[str, str]] = packages_metadata
self._package_type_filter = "material"
self._search_type = "package_ids"
self._requested_search_string = ",".join(map(lambda package: package["id"], packages_metadata))
@ -38,7 +37,14 @@ class MissingPackageList(RemotePackageList):
for package_metadata in self._packages_metadata:
if package_metadata["id"] not in returned_packages_ids:
package = PackageModel.fromIncompletePackageInformation(package_metadata["display_name"], package_metadata["package_version"], self._package_type_filter)
package_type = package_metadata["type"] if "type" in package_metadata else "material"
# When this feature was originally introduced only missing materials were detected. With the inclusion
# of backend plugins this system was extended to also detect missing plugins. With that change the type
# of the package was added to the metadata. Project files before this change do not have this type. So
# if the type is not present we assume it is a material.
package = PackageModel.fromIncompletePackageInformation(package_metadata["display_name"],
package_metadata["package_version"],
package_type)
self.appendItem({"package": package})
self.itemsChanged.emit()

View file

@ -87,12 +87,22 @@ class PackageModel(QObject):
self._is_missing_package_information = False
@classmethod
def fromIncompletePackageInformation(cls, display_name: str, package_version: str, package_type: str) -> "PackageModel":
def fromIncompletePackageInformation(cls, display_name: str, package_version: str,
package_type: str) -> "PackageModel":
description = ""
match package_type:
case "material":
description = catalog.i18nc("@label:label Ultimaker Marketplace is a brand name, don't translate",
"The material package associated with the Cura project could not be found on the Ultimaker Marketplace. Use the partial material profile definition stored in the Cura project file at your own risk.")
case "plugin":
description = catalog.i18nc("@label:label Ultimaker Marketplace is a brand name, don't translate",
"The plugin associated with the Cura project could not be found on the Ultimaker Marketplace. As the plugin may be required to slice the project it might not be possible to correctly slice the file.")
package_data = {
"display_name": display_name,
"package_version": package_version,
"package_type": package_type,
"description": catalog.i18nc("@label:label Ultimaker Marketplace is a brand name, don't translate", "The material package associated with the Cura project could not be found on the Ultimaker Marketplace. Use the partial material profile definition stored in the Cura project file at your own risk.")
"description": description,
}
package_model = cls(package_data)
package_model.setIsMissingPackageInformation(True)

View file

@ -21,6 +21,7 @@ catalog = i18nCatalog("cura")
class RemotePackageList(PackageList):
ITEMS_PER_PAGE = 20 # Pagination of number of elements to download at once.
SORT_TYPE = "last_updated" # Default value to send for sort_by filter.
def __init__(self, parent: Optional["QObject"] = None) -> None:
super().__init__(parent)
@ -28,6 +29,7 @@ class RemotePackageList(PackageList):
self._package_type_filter = ""
self._requested_search_string = ""
self._current_search_string = ""
self._search_sort = "sort_by"
self._search_type = "search"
self._request_url = self._initialRequestUrl()
self._ongoing_requests["get_packages"] = None
@ -102,6 +104,8 @@ class RemotePackageList(PackageList):
request_url += f"&package_type={self._package_type_filter}"
if self._current_search_string != "":
request_url += f"&{self._search_type}={self._current_search_string}"
if self.SORT_TYPE:
request_url += f"&{self._search_sort}={self.SORT_TYPE}"
return request_url
def _parseResponse(self, reply: "QNetworkReply") -> None:

View file

@ -12,7 +12,7 @@ import Cura 1.6 as Cura
Marketplace
{
modality: Qt.ApplicationModal
title: catalog.i18nc("@title", "Install missing Materials")
title: catalog.i18nc("@title", "Install missing packages")
pageContentsSource: "MissingPackages.qml"
showSearchHeader: false
showOnboadBanner: false

View file

@ -280,7 +280,7 @@ Window
onClicked:
{
marketplaceDialog.hide();
CuraApplication.closeApplication();
CuraApplication.checkAndExitApplication();
}
}
}

View file

@ -5,7 +5,7 @@ import UM 1.4 as UM
Packages
{
pageTitle: catalog.i18nc("@header", "Install Materials")
pageTitle: catalog.i18nc("@header", "Install Packages")
bannerVisible: false
showUpdateButton: false

View file

@ -1,5 +1,5 @@
# Copyright (c) 2018 Jaime van Kessel, Ultimaker B.V.
# The PostProcessingPlugin is released under the terms of the AGPLv3 or higher.
# The PostProcessingPlugin is released under the terms of the LGPLv3 or higher.
import configparser # The script lists are stored in metadata as serialised config files.
import importlib.util

View file

@ -1,5 +1,5 @@
// Copyright (c) 2022 Jaime van Kessel, Ultimaker B.V.
// The PostProcessingPlugin is released under the terms of the AGPLv3 or higher.
// The PostProcessingPlugin is released under the terms of the LGPLv3 or higher.
import QtQuick 2.2
import QtQuick.Controls 2.15

View file

@ -1,6 +1,6 @@
# Copyright (c) 2015 Jaime van Kessel
# Copyright (c) 2018 Ultimaker B.V.
# The PostProcessingPlugin is released under the terms of the AGPLv3 or higher.
# The PostProcessingPlugin is released under the terms of the LGPLv3 or higher.
from typing import Optional, Any, Dict, TYPE_CHECKING, List
from UM.Signal import Signal, signalemitter

View file

@ -1,7 +1,7 @@
# ChangeAtZ script - Change printing parameters at a given height
# This script is the successor of the TweakAtZ plugin for legacy Cura.
# It contains code from the TweakAtZ plugin V1.0-V4.x and from the ExampleScript by Jaime van Kessel, Ultimaker B.V.
# It runs with the PostProcessingPlugin which is released under the terms of the AGPLv3 or higher.
# It runs with the PostProcessingPlugin which is released under the terms of the LGPLv3 or higher.
# This script is licensed under the Creative Commons - Attribution - Share Alike (CC BY-SA) terms
# Authors of the ChangeAtZ plugin / script:

View file

@ -1,6 +1,6 @@
# ColorMix script - 2-1 extruder color mix and blending
# This script is specific for the Geeetech A10M dual extruder but should work with other Marlin printers.
# It runs with the PostProcessingPlugin which is released under the terms of the AGPLv3 or higher.
# It runs with the PostProcessingPlugin which is released under the terms of the LGPLv3 or higher.
# This script is licensed under the Creative Commons - Attribution - Share Alike (CC BY-SA) terms
#Authors of the 2-1 ColorMix plug-in / script:

View file

@ -1,5 +1,5 @@
# Copyright (c) 2023 Ultimaker B.V.
# The PostProcessingPlugin is released under the terms of the AGPLv3 or higher.
# The PostProcessingPlugin is released under the terms of the LGPLv3 or higher.
# Modification 06.09.2020
# add checkbox, now you can choose and use configuration from the firmware itself.

View file

@ -0,0 +1,326 @@
# Limit XY Accel: Authored by: Greg Foresi (GregValiant)
# July 2023
# Sometimes bed-slinger printers need different Accel and Jerk values for the Y but Cura always makes them the same.
# This script changes the Accel and/or Jerk from the beginning of the 'Start Layer' to the end of the 'End Layer'.
# The existing M201 Max Accel will be changed to limit the Y (and/or X) accel at the printer. If you have Accel enabled in Cura and the XY Accel is set to 3000 then setting the Y limit to 1000 will result in the printer limiting the Y to 1000. This can keep tall skinny prints from breaking loose of the bed and failing. The script was not tested with Junction Deviation.
# If enabled - the Jerk setting is changed line-by-line within the gcode as there is no "limit" on Jerk.
# if 'Gradual ACCEL change' is enabled then the Accel is changed gradually from the Start to the End layer and that will be the final Accel setting in the file. If 'Gradual' is enabled then the Jerk settings will continue to be changed to the end of the file (rather than ending at the End layer).
# This post is intended for printers with moving beds (bed slingers) so UltiMaker printers are excluded.
# When setting an accel limit on multi-extruder printers ALL extruders are effected.
# This post does not distinguish between Print Accel and Travel Accel. The limit is the limit for all regardless. Example: Skin Accel = 1000 and Outer Wall accel = 500. If the limit is set to 300 then both Skin and Outer Wall will be Accel = 300.
from ..Script import Script
from cura.CuraApplication import CuraApplication
import re
from UM.Message import Message
class LimitXYAccelJerk(Script):
def initialize(self) -> None:
super().initialize()
# Get the Accel and Jerk and set the values in the setting boxes--
mycura = CuraApplication.getInstance().getGlobalContainerStack()
extruder = mycura.extruderList
accel_print = extruder[0].getProperty("acceleration_print", "value")
accel_travel = extruder[0].getProperty("acceleration_travel", "value")
jerk_print_old = extruder[0].getProperty("jerk_print", "value")
jerk_travel_old = extruder[0].getProperty("jerk_travel", "value")
self._instance.setProperty("x_accel_limit", "value", round(accel_print))
self._instance.setProperty("y_accel_limit", "value", round(accel_print))
self._instance.setProperty("x_jerk", "value", jerk_print_old)
self._instance.setProperty("y_jerk", "value", jerk_print_old)
ext_count = int(mycura.getProperty("machine_extruder_count", "value"))
machine_name = str(mycura.getProperty("machine_name", "value"))
# Warn the user if the printer is an Ultimaker-------------------------
if "Ultimaker" in machine_name:
Message(text = "<NOTICE> [Limit the X-Y Accel/Jerk] DID NOT RUN because Ultimaker printers don't have sliding beds.").show()
# Warn the user if the printer is multi-extruder------------------
if ext_count > 1:
Message(text = "<NOTICE> 'Limit the X-Y Accel/Jerk': The post processor treats all extruders the same. If you have multiple extruders they will all be subject to the same Accel and Jerk limits imposed. If you have different Travel and Print Accel they will also be subject to the same limits. If that is not acceptable then you should not use this Post Processor.").show()
def getSettingDataString(self):
return """{
"name": "Limit the X-Y Accel/Jerk (all extruders equal)",
"key": "LimitXYAccelJerk",
"metadata": {},
"version": 2,
"settings":
{
"type_of_change":
{
"label": "Immediate or Gradual change",
"description": "An 'Immediate' change will insert the new numbers immediately at the Start Layer. A 'Gradual' change will transition from the starting Accel to the new Accel limit across a range of layers.",
"type": "enum",
"options": {
"immediate_change": "Immediate",
"gradual_change": "Gradual"},
"default_value": "immediate_change"
},
"x_accel_limit":
{
"label": "X MAX Acceleration",
"description": "If this number is lower than the 'X Print Accel' in Cura then this will limit the Accel on the X axis. Enter the Maximum Acceleration value for the X axis. This will affect both Print and Travel Accel. If you enable an End Layer then at the end of that layer the Accel Limit will be reset (unless you choose 'Gradual' in which case the new limit goes to the top layer).",
"type": "int",
"enabled": true,
"minimum_value": 50,
"unit": "mm/sec² ",
"default_value": 500
},
"y_accel_limit":
{
"label": "Y MAX Acceleration",
"description": "If this number is lower than the Y accel in Cura then this will limit the Accel on the Y axis. Enter the Maximum Acceleration value for the Y axis. This will affect both Print and Travel Accel. If you enable an End Layer then at the end of that layer the Accel Limit will be reset (unless you choose 'Gradual' in which case the new limit goes to the top layer).",
"type": "int",
"enabled": true,
"minimum_value": 50,
"unit": "mm/sec² ",
"default_value": 500
},
"jerk_enable":
{
"label": "Change the Jerk",
"description": "Whether to change the Jerk values.",
"type": "bool",
"enabled": true,
"default_value": false
},
"x_jerk":
{
"label": " X jerk",
"description": "Enter the Jerk value for the X axis. Enter '0' to use the existing X Jerk. This setting will affect both the Print and Travel jerk.",
"type": "int",
"enabled": "jerk_enable",
"unit": "mm/sec ",
"default_value": 8
},
"y_jerk":
{
"label": " Y jerk",
"description": "Enter the Jerk value for the Y axis. Enter '0' to use the existing Y Jerk. This setting will affect both the Print and Travel jerk.",
"type": "int",
"enabled": "jerk_enable",
"unit": "mm/sec ",
"default_value": 8
},
"start_layer":
{
"label": "From Start of Layer:",
"description": "Use the Cura Preview numbers. Enter the Layer to start the changes at. The minimum is Layer 1.",
"type": "int",
"default_value": 1,
"minimum_value": 1,
"unit": "Lay# ",
"enabled": "type_of_change == 'immediate_change'"
},
"end_layer":
{
"label": "To End of Layer",
"description": "Use the Cura Preview numbers. Enter '-1' for the entire file or enter a layer number. The changes will end at your 'End Layer' and revert back to the original numbers.",
"type": "int",
"default_value": -1,
"minimum_value": -1,
"unit": "Lay# ",
"enabled": "type_of_change == 'immediate_change'"
},
"gradient_start_layer":
{
"label": " Gradual From Layer:",
"description": "Use the Cura Preview numbers. Enter the Layer to start the changes at. The minimum is Layer 1.",
"type": "int",
"default_value": 1,
"minimum_value": 1,
"unit": "Lay# ",
"enabled": "type_of_change == 'gradual_change'"
},
"gradient_end_layer":
{
"label": " Gradual To Layer",
"description": "Use the Cura Preview numbers. Enter '-1' for the top layer or enter a layer number. The last 'Gradual' change will continue to the end of the file.",
"type": "int",
"default_value": -1,
"minimum_value": -1,
"unit": "Lay# ",
"enabled": "type_of_change == 'gradual_change'"
}
}
}"""
def execute(self, data):
mycura = CuraApplication.getInstance().getGlobalContainerStack()
extruder = mycura.extruderList
machine_name = str(mycura.getProperty("machine_name", "value"))
print_sequence = str(mycura.getProperty("print_sequence", "value"))
# Exit if 'one_at_a_time' is enabled-------------------------
if print_sequence == "one_at_a_time":
Message(text = "<NOTICE> [Limit the X-Y Accel/Jerk] DID NOT RUN. This post processor is not compatible with 'One-at-a-Time' mode.").show()
data[0] += "; [LimitXYAccelJerk] DID NOT RUN because Cura is set to 'One-at-a-Time' mode.\n"
return data
# Exit if the printer is an Ultimaker-------------------------
if "Ultimaker" in machine_name:
Message(text = "<NOTICE> [Limit the X-Y Accel/Jerk] DID NOT RUN. This post processor is for bed slinger printers only.").show()
data[0] += "; [LimitXYAccelJerk] DID NOT RUN because the printer doesn't have a sliding bed.\n"
return data
type_of_change = str(self.getSettingValueByKey("type_of_change"))
accel_print_enabled = bool(extruder[0].getProperty("acceleration_enabled", "value"))
accel_travel_enabled = bool(extruder[0].getProperty("acceleration_travel_enabled", "value"))
accel_print = extruder[0].getProperty("acceleration_print", "value")
accel_travel = extruder[0].getProperty("acceleration_travel", "value")
jerk_print_enabled = str(extruder[0].getProperty("jerk_enabled", "value"))
jerk_travel_enabled = str(extruder[0].getProperty("jerk_travel_enabled", "value"))
jerk_print_old = extruder[0].getProperty("jerk_print", "value")
jerk_travel_old = extruder[0].getProperty("jerk_travel", "value")
if int(accel_print) >= int(accel_travel):
accel_old = accel_print
else:
accel_old = accel_travel
jerk_travel = str(extruder[0].getProperty("jerk_travel", "value"))
if int(jerk_print_old) >= int(jerk_travel_old):
jerk_old = jerk_print_old
else:
jerk_old = jerk_travel_old
#Set the new Accel values----------------------------------------------------------
x_accel = str(self.getSettingValueByKey("x_accel_limit"))
y_accel = str(self.getSettingValueByKey("y_accel_limit"))
x_jerk = int(self.getSettingValueByKey("x_jerk"))
y_jerk = int(self.getSettingValueByKey("y_jerk"))
# Put the strings together-------------------------------------------
m201_limit_new = "M201 X" + x_accel + " Y" + y_accel
m201_limit_old = "M201 X" + str(round(accel_old)) + " Y" + str(round(accel_old))
if x_jerk == 0:
m205_jerk_pattern = "Y(\d*)"
m205_jerk_new = "Y" + str(y_jerk)
if y_jerk == 0:
m205_jerk_pattern = "X(\d*)"
m205_jerk_new = "X" + str(x_jerk)
if x_jerk != 0 and y_jerk != 0:
m205_jerk_pattern = "M205 X(\d*) Y(\d*)"
m205_jerk_new = "M205 X" + str(x_jerk) + " Y" + str(y_jerk)
m205_jerk_old = "M205 X" + str(jerk_old) + " Y" + str(jerk_old)
type_of_change = self.getSettingValueByKey("type_of_change")
#Get the indexes of the start and end layers----------------------------------------
if type_of_change == 'immediate_change':
start_layer = int(self.getSettingValueByKey("start_layer"))-1
end_layer = int(self.getSettingValueByKey("end_layer"))
else:
start_layer = int(self.getSettingValueByKey("gradient_start_layer"))-1
end_layer = int(self.getSettingValueByKey("gradient_end_layer"))
start_index = 2
end_index = len(data)-2
for num in range(2,len(data)-1):
if ";LAYER:" + str(start_layer) + "\n" in data[num]:
start_index = num
break
if int(end_layer) > 0:
for num in range(3,len(data)-1):
try:
if ";LAYER:" + str(end_layer) + "\n" in data[num]:
end_index = num
break
except:
end_index = len(data)-2
#Add Accel limit and new Jerk at start layer-----------------------------------------------------
if type_of_change == "immediate_change":
layer = data[start_index]
lines = layer.split("\n")
for index, line in enumerate(lines):
if lines[index].startswith(";LAYER:"):
lines.insert(index+1,m201_limit_new)
if self.getSettingValueByKey("jerk_enable"):
lines.insert(index+2,m205_jerk_new)
data[start_index] = "\n".join(lines)
break
#Alter any existing jerk lines. Accel lines can be ignored-----------------------------------
for num in range(start_index,end_index,1):
layer = data[num]
lines = layer.split("\n")
for index, line in enumerate(lines):
if line.startswith("M205"):
lines[index] = re.sub(m205_jerk_pattern, m205_jerk_new, line)
data[num] = "\n".join(lines)
if end_layer != -1:
try:
layer = data[end_index-1]
lines = layer.split("\n")
lines.insert(len(lines)-2,m201_limit_old)
lines.insert(len(lines)-2,m205_jerk_old)
data[end_index-1] = "\n".join(lines)
except:
pass
else:
data[len(data)-1] = m201_limit_old + "\n" + m205_jerk_old + "\n" + data[len(data)-1]
return data
elif type_of_change == "gradual_change":
layer_spread = end_index - start_index
if accel_old >= int(x_accel):
x_accel_hyst = round((accel_old - int(x_accel)) / layer_spread)
else:
x_accel_hyst = round((int(x_accel) - accel_old) / layer_spread)
if accel_old >= int(y_accel):
y_accel_hyst = round((accel_old - int(y_accel)) / layer_spread)
else:
y_accel_hyst = round((int(y_accel) - accel_old) / layer_spread)
if accel_old >= int(x_accel):
x_accel_start = round(round((accel_old - x_accel_hyst)/25)*25)
else:
x_accel_start = round(round((x_accel_hyst + accel_old)/25)*25)
if accel_old >= int(y_accel):
y_accel_start = round(round((accel_old - y_accel_hyst)/25)*25)
else:
y_accel_start = round(round((y_accel_hyst + accel_old)/25)*25)
m201_limit_new = "M201 X" + str(x_accel_start) + " Y" + str(y_accel_start)
#Add Accel limit and new Jerk at start layer-------------------------------------------------------------
layer = data[start_index]
lines = layer.split("\n")
for index, line in enumerate(lines):
if lines[index].startswith(";LAYER:"):
lines.insert(index+1,m201_limit_new)
if self.getSettingValueByKey("jerk_enable"):
lines.insert(index+2,m205_jerk_new)
data[start_index] = "\n".join(lines)
break
for num in range(start_index + 1, end_index,1):
layer = data[num]
lines = layer.split("\n")
if accel_old >= int(x_accel):
x_accel_start -= x_accel_hyst
if x_accel_start < int(x_accel): x_accel_start = int(x_accel)
else:
x_accel_start += x_accel_hyst
if x_accel_start > int(x_accel): x_accel_start = int(x_accel)
if accel_old >= int(y_accel):
y_accel_start -= y_accel_hyst
if y_accel_start < int(y_accel): y_accel_start = int(y_accel)
else:
y_accel_start += y_accel_hyst
if y_accel_start > int(y_accel): y_accel_start = int(y_accel)
m201_limit_new = "M201 X" + str(round(round(x_accel_start/25)*25)) + " Y" + str(round(round(y_accel_start/25)*25))
for index, line in enumerate(lines):
if line.startswith(";LAYER:"):
lines.insert(index+1, m201_limit_new)
continue
data[num] = "\n".join(lines)
#Alter any existing jerk lines. Accel lines can be ignored---------------
if self.getSettingValueByKey("jerk_enable"):
for num in range(start_index,len(data)-1,1):
layer = data[num]
lines = layer.split("\n")
for index, line in enumerate(lines):
if line.startswith("M205"):
lines[index] = re.sub(m205_jerk_pattern, m205_jerk_new, line)
data[num] = "\n".join(lines)
data[len(data)-1] = m201_limit_old + "\n" + m205_jerk_old + "\n" + data[len(data)-1]
return data

View file

@ -1,5 +1,5 @@
# Copyright (c) 2023 UltiMaker B.V.
# The PostProcessingPlugin is released under the terms of the AGPLv3 or higher.
# The PostProcessingPlugin is released under the terms of the LGPLv3 or higher.
from ..Script import Script

View file

@ -1,5 +1,5 @@
# Copyright (c) 2017 Ghostkeeper
# The PostProcessingPlugin is released under the terms of the AGPLv3 or higher.
# The PostProcessingPlugin is released under the terms of the LGPLv3 or higher.
import re #To perform the search and replace.

View file

@ -1,4 +1,4 @@
# This PostProcessingPlugin script is released under the terms of the AGPLv3 or higher.
# This PostProcessingPlugin script is released under the terms of the LGPLv3 or higher.
"""
Copyright (c) 2017 Christophe Baribaud 2017
Python implementation of https://github.com/electrocbd/post_stretch

View file

@ -1,6 +1,7 @@
# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
import os
import os.path
from UM.Application import Application
@ -143,38 +144,44 @@ class RemovableDriveOutputDevice(OutputDevice):
def _onFinished(self, job):
if self._stream:
# Explicitly closing the stream flushes the write-buffer
error = job.getError()
try:
# Explicitly closing the stream flushes the write-buffer
self._stream.close()
self._stream = None
except:
Logger.logException("w", "An exception occurred while trying to write to removable drive.")
message = Message(catalog.i18nc("@info:status", "Could not save to removable drive {0}: {1}").format(self.getName(),str(job.getError())),
title = catalog.i18nc("@info:title", "Error"),
message_type = Message.MessageType.ERROR)
except Exception as e:
if not error:
# Only log new error if there was no previous one
error = e
self._stream = None
self._writing = False
self.writeFinished.emit(self)
if not error:
message = Message(
catalog.i18nc("@info:status", "Saved to Removable Drive {0} as {1}").format(self.getName(),
os.path.basename(
job.getFileName())),
title=catalog.i18nc("@info:title", "File Saved"),
message_type=Message.MessageType.POSITIVE)
message.addAction("eject", catalog.i18nc("@action:button", "Eject"), "eject",
catalog.i18nc("@action", "Eject removable device {0}").format(self.getName()))
message.actionTriggered.connect(self._onActionTriggered)
message.show()
self.writeSuccess.emit(self)
else:
try:
os.remove(job.getFileName())
except Exception as e:
Logger.logException("e", "Exception when trying to remove incomplete exported file %s",
str(job.getFileName()))
message = Message(catalog.i18nc("@info:status",
"Could not save to removable drive {0}: {1}").format(self.getName(),
str(job.getError())),
title=catalog.i18nc("@info:title", "Error"),
message_type=Message.MessageType.ERROR)
message.show()
self.writeError.emit(self)
return
self._writing = False
self.writeFinished.emit(self)
if job.getResult():
message = Message(catalog.i18nc("@info:status", "Saved to Removable Drive {0} as {1}").format(self.getName(), os.path.basename(job.getFileName())),
title = catalog.i18nc("@info:title", "File Saved"),
message_type = Message.MessageType.POSITIVE)
message.addAction("eject", catalog.i18nc("@action:button", "Eject"), "eject", catalog.i18nc("@action", "Eject removable device {0}").format(self.getName()))
message.actionTriggered.connect(self._onActionTriggered)
message.show()
self.writeSuccess.emit(self)
else:
message = Message(catalog.i18nc("@info:status",
"Could not save to removable drive {0}: {1}").format(self.getName(),
str(job.getError())),
title = catalog.i18nc("@info:title", "Error"),
message_type = Message.MessageType.ERROR)
message.show()
self.writeError.emit(self)
job.getStream().close()
def _onActionTriggered(self, message, action):
if action == "eject":

View file

@ -37,24 +37,13 @@ class NewPrinterDetectedMessage(Message):
def finalize(self, new_devices_added, new_output_devices):
self.setProgress(None)
num_devices_added = len(new_devices_added)
max_disp_devices = 3
if num_devices_added > max_disp_devices:
num_hidden = num_devices_added - max_disp_devices
device_name_list = ["<li>{} ({})</li>".format(device.name, device.printerTypeName) for device in
new_output_devices[0: max_disp_devices]]
device_name_list.append(
"<li>" + self.i18n_catalog.i18ncp("info:{0} gets replaced by a number of printers", "... and {0} other",
"... and {0} others", num_hidden) + "</li>")
device_names = "".join(device_name_list)
else:
device_names = "".join(
["<li>{} ({})</li>".format(device.name, device.printerTypeName) for device in new_devices_added])
if new_devices_added:
message_text = self.i18n_catalog.i18nc("info:status",
"Printers added from Digital Factory:") + f"<ul>{device_names}</ul>"
device_names = ""
for device in new_devices_added:
device_names = device_names + "<li>{} ({})</li>".format(device.name, device.printerTypeName)
message_title = self.i18n_catalog.i18nc("info:status", "Printers added from Digital Factory:")
message_text = f"{message_title}<ul>{device_names}</ul>"
self.setText(message_text)
else:
self.hide()

View file

@ -21,7 +21,7 @@ class AutoDetectBaudJob(Job):
self._all_baud_rates = [115200, 250000, 500000, 230400, 76800, 57600, 38400, 19200, 9600]
def run(self) -> None:
Logger.log("d", "Auto detect baud rate started.")
Logger.debug(f"Auto detect baud rate started for {self._serial_port}")
wait_response_timeouts = [3, 15, 30]
wait_bootloader_times = [1.5, 5, 15]
write_timeout = 3
@ -46,8 +46,7 @@ class AutoDetectBaudJob(Job):
wait_bootloader = wait_bootloader_times[retry]
else:
wait_bootloader = wait_bootloader_times[-1]
Logger.log("d", "Checking {serial} if baud rate {baud_rate} works. Retry nr: {retry}. Wait timeout: {timeout}".format(
serial = self._serial_port, baud_rate = baud_rate, retry = retry, timeout = wait_response_timeout))
Logger.debug(f"Checking {self._serial_port} if baud rate {baud_rate} works. Retry nr: {retry}. Wait timeout: {wait_response_timeout}")
if serial is None:
try:
@ -61,7 +60,9 @@ class AutoDetectBaudJob(Job):
serial.baudrate = baud_rate
except ValueError:
continue
sleep(wait_bootloader) # Ensure that we are not talking to the boot loader. 1.5 seconds seems to be the magic number
# Ensure that we are not talking to the boot loader. 1.5 seconds seems to be the magic number
sleep(wait_bootloader)
serial.write(b"\n") # Ensure we clear out previous responses
serial.write(b"M105\n")
@ -83,4 +84,5 @@ class AutoDetectBaudJob(Job):
serial.write(b"M105\n")
sleep(15) # Give the printer some time to init and try again.
Logger.debug(f"Unable to find a working baudrate for {serial}")
self.setResult(None) # Unable to detect the correct baudrate.

View file

@ -38,26 +38,26 @@ PyQt6-NetworkAuth-Qt6==6.4.2 \
--hash=sha256:d96d557fe61edb9b68d189f270f0393d6579c8d308e6b0d41bc0699371d7cb4e
certifi==2023.5.7; \
--hash=sha256:c6c2e98f5c7869efca1f8916fed228dd91539f9f1b444c314c06eef02980c716
cryptography==3.4.8; \
--hash=sha256:0a7dcbcd3f1913f664aca35d47c1331fce738d44ec34b7be8b9d332151b0b01e \
--hash=sha256:1eb7bb0df6f6f583dd8e054689def236255161ebbcf62b226454ab9ec663746b \
--hash=sha256:21ca464b3a4b8d8e86ba0ee5045e103a1fcfac3b39319727bc0fc58c09c6aff7 \
--hash=sha256:34dae04a0dce5730d8eb7894eab617d8a70d0c97da76b905de9efb7128ad7085 \
--hash=sha256:3520667fda779eb788ea00080124875be18f2d8f0848ec00733c0ec3bb8219fc \
--hash=sha256:3c4129fc3fdc0fa8e40861b5ac0c673315b3c902bbdc05fc176764815b43dd1d \
--hash=sha256:3fa3a7ccf96e826affdf1a0a9432be74dc73423125c8f96a909e3835a5ef194a \
--hash=sha256:5b0fbfae7ff7febdb74b574055c7466da334a5371f253732d7e2e7525d570498 \
--hash=sha256:695104a9223a7239d155d7627ad912953b540929ef97ae0c34c7b8bf30857e89 \
--hash=sha256:8695456444f277af73a4877db9fc979849cd3ee74c198d04fc0776ebc3db52b9 \
--hash=sha256:94cc5ed4ceaefcbe5bf38c8fba6a21fc1d365bb8fb826ea1688e3370b2e24a1c \
--hash=sha256:94fff993ee9bc1b2440d3b7243d488c6a3d9724cc2b09cdb297f6a886d040ef7 \
--hash=sha256:9965c46c674ba8cc572bc09a03f4c649292ee73e1b683adb1ce81e82e9a6a0fb \
--hash=sha256:a00cf305f07b26c351d8d4e1af84ad7501eca8a342dedf24a7acb0e7b7406e14 \
--hash=sha256:a305600e7a6b7b855cd798e00278161b681ad6e9b7eca94c721d5f588ab212af \
--hash=sha256:cd65b60cfe004790c795cc35f272e41a3df4631e2fb6b35aa7ac6ef2859d554e \
--hash=sha256:d2a6e5ef66503da51d2110edf6c403dc6b494cc0082f85db12f54e9c5d4c3ec5 \
--hash=sha256:d9ec0e67a14f9d1d48dd87a2531009a9b251c02ea42851c060b25c782516ff06 \
--hash=sha256:f44d141b8c4ea5eb4dbc9b3ad992d45580c1d22bf5e24363f2fbf50c2d7ae8a7
cryptography==41.0.1 \
--hash=sha256:059e348f9a3c1950937e1b5d7ba1f8e968508ab181e75fc32b879452f08356db \
--hash=sha256:1a5472d40c8f8e91ff7a3d8ac6dfa363d8e3138b961529c996f3e2df0c7a411a \
--hash=sha256:1a8e6c2de6fbbcc5e14fd27fb24414507cb3333198ea9ab1258d916f00bc3039 \
--hash=sha256:1fee5aacc7367487b4e22484d3c7e547992ed726d14864ee33c0176ae43b0d7c \
--hash=sha256:5d092fdfedaec4cbbffbf98cddc915ba145313a6fdaab83c6e67f4e6c218e6f3 \
--hash=sha256:5f0ff6e18d13a3de56f609dd1fd11470918f770c6bd5d00d632076c727d35485 \
--hash=sha256:7bfc55a5eae8b86a287747053140ba221afc65eb06207bedf6e019b8934b477c \
--hash=sha256:7fa01527046ca5facdf973eef2535a27fec4cb651e4daec4d043ef63f6ecd4ca \
--hash=sha256:8dde71c4169ec5ccc1087bb7521d54251c016f126f922ab2dfe6649170a3b8c5 \
--hash=sha256:8f4ab7021127a9b4323537300a2acfb450124b2def3756f64dc3a3d2160ee4b5 \
--hash=sha256:948224d76c4b6457349d47c0c98657557f429b4e93057cf5a2f71d603e2fc3a3 \
--hash=sha256:9a6c7a3c87d595608a39980ebaa04d5a37f94024c9f24eb7d10262b92f739ddb \
--hash=sha256:b46e37db3cc267b4dea1f56da7346c9727e1209aa98487179ee8ebed09d21e43 \
--hash=sha256:b4ceb5324b998ce2003bc17d519080b4ec8d5b7b70794cbd2836101406a9be31 \
--hash=sha256:cb33ccf15e89f7ed89b235cff9d49e2e62c6c981a6061c9c8bb47ed7951190bc \
--hash=sha256:d198820aba55660b4d74f7b5fd1f17db3aa5eb3e6893b0a41b75e84e4f9e0e4b \
--hash=sha256:d34579085401d3f49762d2f7d6634d6b6c2ae1242202e860f4d26b046e3a1006 \
--hash=sha256:eb8163f5e549a22888c18b0d53d6bb62a20510060a22fd5a995ec8a05268df8a \
--hash=sha256:f73bff05db2a3e5974a6fd248af2566134d8981fd7ab012e5dd4ddb1d9a70699
zeroconf==0.31.0 \
--hash=sha256:53a180248471c6f81bd1fffcbce03ed93d7d8eaf10905c9121ac1ea996d19844 \
--hash=sha256:5a468da018bc3f04bbce77ae247924d802df7aeb4c291bbbb5a9616d128800b0
@ -381,9 +381,9 @@ typing-extensions==3.10.0.2 \
jeepney==0.7.1; \
--hash=sha256:1b5a0ea5c0e7b166b2f5895b91a08c14de8915afda4407fb5022a195224958ac \
--hash=sha256:fa9e232dfa0c498bd0b8a3a73b8d8a31978304dcef0515adc859d4e096f96f4f
SecretStorage==3.3.1 \
--hash=sha256:422d82c36172d88d6a0ed5afdec956514b189ddbfb72fefab0c8a1cee4eaf71f \
--hash=sha256:fd666c51a6bf200643495a04abb261f83229dcb6fd8472ec393df7ffc8b6f195
SecretStorage==3.3.3 \
--hash=sha256:2403533ef369eca6d2ba81718576c5e0f564d5cca1b58f73a8b23e7d4eeebd77 \
--hash=sha256:f356e6628222568e3af06f2eba8df495efa13b3b63081dafd4f7d9a7b7bc9f99
keyring==23.0.1 \
--hash=sha256:045703609dd3fccfcdb27da201684278823b72af515aedec1a8515719a038cb8 \
--hash=sha256:8f607d7d1cc502c43a932a275a56fe47db50271904513a379d39df1af277ac48

View file

@ -59,7 +59,7 @@
"machine_width": { "default_value": 280 },
"material_diameter": { "default_value": 1.75 },
"material_initial_print_temperature": { "value": "material_print_temperature" },
"prime_tower_min_volume": { "value": "((reveOrValue('layer_height'))/2" },
"prime_tower_min_volume": { "value": "((resolveOrValue('layer_height'))/2" },
"prime_tower_position_x": { "value": "240" },
"prime_tower_position_y": { "value": "190" },
"prime_tower_size": { "value": "30" },

View file

@ -10,57 +10,18 @@
"file_formats": "text/x-gcode",
"platform": "alya_platform.3mf",
"exclude_materials": [
"chromatik_pla",
"dsm_arnitel2045_175",
"dsm_novamid1070_175",
"fabtotum_abs",
"fabtotum_nylon",
"fabtotum_pla",
"fabtotum_tpu",
"fiberlogy_hd_pla",
"filo3d_pla",
"filo3d_pla_green",
"filo3d_pla_red",
"dsm_arnitel2045",
"dsm_novamid1070",
"generic_abs",
"generic_abs_175",
"generic_bam",
"generic_cpe",
"generic_cpe_175",
"generic_cpe_plus",
"generic_hips",
"generic_hips_175",
"generic_nylon",
"generic_nylon_175",
"generic_pc",
"generic_pc_175",
"generic_petg",
"generic_petg_175",
"generic_pp",
"generic_pva",
"generic_pva_175",
"generic_tough_pla",
"generic_tpu",
"generic_tpu_175",
"imade3d_petg_green",
"imade3d_petg_pink",
"imade3d_pla_green",
"imade3d_pla_pink",
"innofill_innoflex60_175",
"octofiber_pla",
"polyflex_pla",
"polymax_pla",
"polyplus_pla",
"polywood_pla",
"verbatim_bvoh_175",
"Vertex_Delta_ABS",
"Vertex_Delta_PET",
"Vertex_Delta_PLA",
"Vertex_Delta_TPU",
"zyyx_pro_flex",
"zyyx_pro_pla",
"tizyx_pla",
"tizyx_abs",
"tizyx_pla_bois"
"innofill_innoflex60",
"verbatim_bvoh"
],
"has_machine_quality": true,
"has_materials": true,

View file

@ -10,57 +10,18 @@
"file_formats": "text/x-gcode",
"platform": "alya_nx_platform.3mf",
"exclude_materials": [
"chromatik_pla",
"dsm_arnitel2045_175",
"dsm_novamid1070_175",
"fabtotum_abs",
"fabtotum_nylon",
"fabtotum_pla",
"fabtotum_tpu",
"fiberlogy_hd_pla",
"filo3d_pla",
"filo3d_pla_green",
"filo3d_pla_red",
"generic_abs",
"generic_abs_175",
"generic_bam",
"generic_cpe",
"generic_cpe_175",
"generic_cpe_plus",
"generic_hips",
"generic_hips_175",
"generic_nylon",
"generic_nylon_175",
"generic_pc",
"generic_pc_175",
"generic_petg",
"generic_petg_175",
"generic_pp",
"generic_pva",
"generic_pva_175",
"generic_tough_pla",
"generic_tpu",
"generic_tpu_175",
"imade3d_petg_green",
"imade3d_petg_pink",
"imade3d_pla_green",
"imade3d_pla_pink",
"innofill_innoflex60_175",
"octofiber_pla",
"polyflex_pla",
"polymax_pla",
"polyplus_pla",
"polywood_pla",
"verbatim_bvoh_175",
"Vertex_Delta_ABS",
"Vertex_Delta_PET",
"Vertex_Delta_PLA",
"Vertex_Delta_TPU",
"zyyx_pro_flex",
"zyyx_pro_pla",
"tizyx_pla",
"tizyx_abs",
"tizyx_pla_bois"
"verbatim_bvoh_175"
],
"has_machine_quality": true,
"has_materials": true,

View file

@ -0,0 +1,29 @@
{
"version": 2,
"name": "Anycubic Kobra Plus",
"inherits": "fdmprinter",
"metadata":
{
"visible": true,
"author": "Jordon Brooks",
"manufacturer": "Anycubic",
"file_formats": "text/x-gcode",
"has_machine_quality": true,
"has_materials": true,
"machine_extruder_trains": { "0": "anycubic_kobra_plus_extruder_0" },
"preferred_material": "generic_pla",
"preferred_quality_type": "normal",
"quality_definition": "anycubic_kobra_plus"
},
"overrides":
{
"machine_depth": { "default_value": 302 },
"machine_end_gcode": { "default_value": "M104 S0\nM140 S0\n;Retract the filament\nG92 E1\nG1 E-1 F300\nG28 X0 Y0\nM84\nM355 S0; led off" },
"machine_gcode_flavor": { "default_value": "RepRap (Marlin/Sprinter)" },
"machine_heated_bed": { "default_value": true },
"machine_height": { "default_value": 352 },
"machine_name": { "default_value": "Anycubic Kobra Plus" },
"machine_start_gcode": { "default_value": "G28 ;Home\nG1 Z15.0 F6000 ;Move the platform down 15mm\n;Prime the extruder\nG92 E0\nM355 S1; Turn LED on\n; Add Custom purge lines\nG1 Z2.0 F3000 ; Move Z Axis up little to prevent scratching of Heat Bed\nG1 X1.0 Y30 Z0.3 F5000.0 ; Move to start position\nG1 X1.0 Y100.0 Z0.3 F1500.0 E15 ; Draw the first line\nG1 X1.3 Y100.0 Z0.3 F5000.0 ; Move to side a little\nG1 X1.3 Y30 Z0.3 F1500.0 E30 ; Draw the second line\nG92 E0 ; Reset Extruder\nG1 E-2 F500 ; Retract a little \nG1 X50 F500 ; wipe away from the filament line\nG1 X100 F9000 ; Quickly wipe away from the filament line\nG1 Z5.0 F3000 ; Move Z Axis up little to prevent scratching of Heat Bed\n; End custom purge lines" },
"machine_width": { "default_value": 302 }
}
}

View file

@ -18,51 +18,6 @@
{
"machine_center_is_zero": { "default_value": true },
"machine_depth": { "default_value": 180 },
"machine_disallowed_areas":
{
"default_value": [
[
[-50, -85],
[-85, -85],
[-90, -90]
],
[
[-85, -85],
[-85, -50],
[-90, -90]
],
[
[50, -85],
[85, -85],
[90, -90]
],
[
[85, -85],
[85, -50],
[90, -90]
],
[
[-50, 85],
[-85, 85],
[-90, 90]
],
[
[-85, 85],
[-85, 50],
[-90, 90]
],
[
[50, 85],
[85, 85],
[90, 90]
],
[
[85, 85],
[85, 50],
[90, 90]
]
]
},
"machine_end_gcode": { "default_value": "M400 ;Free buffer\nG91 ;relative positioning\nG1 E-1 F300 ;retract the filament a bit before lifting the nozzle, to release some of the pressure\nG1 F{speed_travel} Z+1 E-5 ;move Z up a bit and retract filament even more\nG90 ;absolute positioning\nM104 S0 ;extruder heater off\nM140 S0 ;heated bed heater off\nM107 ;fan off\nM84 ;steppers off\nG28 ;move to endstop\nM84 ;steppers off" },
"machine_gcode_flavor": { "default_value": "RepRap (Marlin/Sprinter)" },
"machine_heated_bed": { "default_value": true },

View file

@ -16,51 +16,6 @@
"overrides":
{
"machine_depth": { "default_value": 240 },
"machine_disallowed_areas":
{
"default_value": [
[
[-50, -115],
[-115, -115],
[-90, -90]
],
[
[-115, -115],
[-115, -50],
[-90, -90]
],
[
[50, -115],
[115, -115],
[90, -90]
],
[
[115, -115],
[115, -50],
[90, -90]
],
[
[-50, 115],
[-115, 115],
[-90, 90]
],
[
[-115, 115],
[-115, 50],
[-90, 90]
],
[
[50, 115],
[115, 115],
[90, 90]
],
[
[115, 115],
[115, 50],
[90, 90]
]
]
},
"machine_name": { "default_value": "Anycubic Kossel Linear Plus" },
"machine_width": { "default_value": 240 }
}

View file

@ -9,51 +9,10 @@
"manufacturer": "Artillery",
"file_formats": "text/x-gcode",
"exclude_materials": [
"Vertex_Delta_ABS",
"Vertex_Delta_PET",
"Vertex_Delta_PLA",
"Vertex_Delta_TPU",
"chromatik_pla",
"dsm_arnitel2045_175",
"dsm_novamid1070_175",
"fabtotum_abs",
"fabtotum_nylon",
"fabtotum_pla",
"fabtotum_tpu",
"fiberlogy_hd_pla",
"filo3d_pla",
"filo3d_pla_green",
"filo3d_pla_red",
"generic_bam",
"generic_cffcpe",
"generic_cffpa",
"generic_cpe",
"generic_cpe_plus",
"generic_gffcpe",
"generic_gffpa",
"generic_hips",
"generic_nylon",
"generic_pc",
"generic_pp",
"generic_pva",
"generic_tough_pla",
"imade3d_petg_green",
"imade3d_petg_pink",
"imade3d_pla_green",
"imade3d_pla_pink",
"innofill_innoflex60_175",
"octofiber_pla",
"polyflex_pla",
"polymax_pla",
"polyplus_pla",
"polywood_pla",
"structur3d_dap100silicone",
"tizyx_abs",
"tizyx_pla",
"tizyx_pla_bois",
"verbatim_bvoh_175",
"zyyx_pro_flex",
"zyyx_pro_pla"
"verbatim_bvoh_175"
],
"first_start_actions": [ "MachineSettingsAction" ],
"has_machine_quality": true,

View file

@ -9,73 +9,13 @@
"manufacturer": "ATMAT sp. z o.o.",
"file_formats": "text/x-gcode",
"exclude_materials": [
"chromatik_pla",
"dsm_arnitel2045_175",
"dsm_novamid1070_175",
"emotiontech_abs",
"emotiontech_petg",
"emotiontech_pla",
"emotiontech_pva-m",
"emotiontech_pva-oks",
"emotiontech_pva-s",
"emotiontech_tpu98a",
"emotiontech_asax",
"emotiontech_hips",
"fiberlogy_hd_pla",
"fabtotum_abs",
"fabtotum_nylon",
"fabtotum_pla",
"fabtotum_tpu",
"filo3d_pla",
"filo3d_pla_green",
"filo3d_pla_red",
"generic_abs",
"generic_bam",
"generic_cffcpe",
"generic_cffpa",
"dsm_arnitel2045",
"dsm_novamid1070",
"generic_cpe",
"generic_cpe_plus",
"generic_gffcpe",
"generic_gffpa",
"generic_hips",
"generic_nylon",
"generic_pc",
"generic_petg",
"generic_pla",
"generic_pp",
"generic_pva",
"generic_tough_pla",
"generic_tpu",
"generic_cpe_175",
"imade3d_petg_175",
"imade3d_pla_175",
"innofill_innoflex60_175",
"leapfrog_abs_natural",
"leapfrog_epla_natural",
"leapfrog_pva_natural",
"octofiber_pla",
"polyflex_pla",
"polymax_pla",
"polyplus_pla",
"polywood_pla",
"structur3d_dap100silicone",
"tizyx_abs",
"tizyx_flex",
"tizyx_petg",
"tizyx_pla",
"tizyx_pla_bois",
"tizyx_pva",
"verbatim_bvoh_175",
"Vertex_Delta_ABS",
"Vertex_Delta_PET",
"Vertex_Delta_PLA",
"Vertex_Delta_PLA_Glitter",
"Vertex_Delta_PLA_Mat",
"Vertex_Delta_PLA_Satin",
"Vertex_Delta_PLA_Wood",
"Vertex_Delta_TPU",
"zyyx_pro_flex",
"zyyx_pro_pla"
"imade3d_petg",
"imade3d_pla",
"innofill_innoflex60",
"verbatim_bvoh"
],
"has_machine_quality": true,
"has_materials": true,

View file

@ -9,55 +9,10 @@
"manufacturer": "Creality3D",
"file_formats": "text/x-gcode",
"exclude_materials": [
"Vertex_Delta_ABS",
"Vertex_Delta_PET",
"Vertex_Delta_PLA",
"Vertex_Delta_TPU",
"chromatik_pla",
"dsm_arnitel2045_175",
"dsm_novamid1070_175",
"fabtotum_abs",
"fabtotum_nylon",
"fabtotum_pla",
"fabtotum_tpu",
"fiberlogy_hd_pla",
"filo3d_pla",
"filo3d_pla_green",
"filo3d_pla_red",
"generic_abs",
"generic_bam",
"generic_cffcpe",
"generic_cffpa",
"generic_cpe",
"generic_cpe_plus",
"generic_gffcpe",
"generic_gffpa",
"generic_hips",
"generic_nylon",
"generic_pc",
"generic_petg",
"generic_pla",
"generic_pp",
"generic_pva",
"generic_tough_pla",
"generic_tpu",
"imade3d_petg_green",
"imade3d_petg_pink",
"imade3d_pla_green",
"imade3d_pla_pink",
"innofill_innoflex60_175",
"octofiber_pla",
"polyflex_pla",
"polymax_pla",
"polyplus_pla",
"polywood_pla",
"structur3d_dap100silicone",
"tizyx_abs",
"tizyx_pla",
"tizyx_pla_bois",
"verbatim_bvoh_175",
"zyyx_pro_flex",
"zyyx_pro_pla"
"dsm_arnitel2045",
"dsm_novamid1070",
"innofill_innoflex60",
"verbatim_bvoh"
],
"first_start_actions": [ "MachineSettingsAction" ],
"has_machine_quality": true,

View file

@ -23,7 +23,7 @@
},
"machine_height": { "default_value": 250 },
"machine_name": { "default_value": "Creality Ender-3 Pro" },
"machine_start_gcode": { "default_value": "; Ender 3 Custom Start G-code\nG92 E0 ; Reset Extruder\nG28 ; Home all axes\nM104 S{material_standby_temperature} ; Start heating up the nozzle most of the way\nM190 S{material_bed_temperature_layer_0} ; Start heating the bed, wait until target temperature reached\nM109 S{material_print_temperature_layer_0} ; Finish heating the nozzle\nG1 Z2.0 F3000 ; Move Z Axis up little to prevent scratching of Heat Bed\nG1 X0.1 Y20 Z0.3 F5000.0 ; Move to start position\nG1 X0.1 Y200.0 Z0.3 F1500.0 E15 ; Draw the first line\nG1 X0.4 Y200.0 Z0.3 F5000.0 ; Move to side a little\nG1 X0.4 Y20 Z0.3 F1500.0 E30 ; Draw the second line\nG92 E0 ; Reset Extruder\nG1 Z2.0 F3000 ; Move Z Axis up little to prevent scratching of Heat Bed\nG1 X5 Y20 Z0.3 F5000.0 ; Move over to prevent blob squish" },
"machine_start_gcode": { "default_value": "; Ender 3 Custom Start G-code\nG92 E0 ; Reset Extruder\nG28 ; Home all axes\nG1 Z5.0 F3000 ; Move Z Axis up a bit during heating to not damage bed\nM104 S{material_standby_temperature} ; Start heating up the nozzle most of the way\nM190 S{material_bed_temperature_layer_0} ; Start heating the bed, wait until target temperature reached\nM109 S{material_print_temperature_layer_0} ; Finish heating the nozzle\nG1 Z2.0 F3000 ; Move Z Axis up little to prevent scratching of Heat Bed\nG1 X0.1 Y20 Z0.3 F5000.0 ; Move to start position\nG1 X0.1 Y200.0 Z0.3 F1500.0 E15 ; Draw the first line\nG1 X0.4 Y200.0 Z0.3 F5000.0 ; Move to side a little\nG1 X0.4 Y20 Z0.3 F1500.0 E30 ; Draw the second line\nG92 E0 ; Reset Extruder\nG1 Z2.0 F3000 ; Move Z Axis up little to prevent scratching of Heat Bed\nG1 X5 Y20 Z0.3 F5000.0 ; Move over to prevent blob squish" },
"machine_width": { "default_value": 220 }
}
}

View file

@ -24,7 +24,7 @@
},
"machine_height": { "default_value": 270 },
"machine_name": { "default_value": "Creality Ender-3 S1" },
"machine_start_gcode": { "default_value": "; Ender 3 S1 Start G-code\n; M413 S0 ; Disable power loss recovery\nG92 E0 ; Reset Extruder\n\n; Prep surfaces before auto home for better accuracy\nM140 S{material_bed_temperature_layer_0}\nM104 S{material_print_temperature_layer_0}\n\nG28 O ; Home all untrusted axes\nG1 Z10.0 F3000 ; Move Z Axis up little to prevent scratching of Heat Bed\nG1 X0 Y0\n\nM190 S{material_bed_temperature_layer_0}\nM109 S{material_print_temperature_layer_0}\n\nG1 X0.1 Y20 Z0.3 F5000.0 ; Move to start position\nG1 X0.1 Y200.0 Z0.3 F1500.0 E15 ; Draw the first line\nG1 X0.4 Y200.0 Z0.3 F5000.0 ; Move to side a little\nG1 X0.4 Y20 Z0.3 F1500.0 E30 ; Draw the second line\nG92 E0 ; Reset Extruder\nG1 Z2.0 F3000 ; Move Z Axis up little to prevent scratching of Heat Bed\nG1 X5 Y20 Z0.3 F5000.0 ; Move over to prevent blob squish\n" },
"machine_start_gcode": { "default_value": "; Ender 3 S1 Start G-code\n; M413 S0 ; Disable power loss recovery\nG92 E0 ; Reset Extruder\n\n; Prep surfaces before auto home for better accuracy\nM140 S{material_bed_temperature_layer_0}\nM104 S{material_print_temperature_layer_0}\n\nG28 ; Home all axes\nG1 Z10.0 F3000 ; Move Z Axis up little to prevent scratching of Heat Bed\nG1 X0 Y0\n\nM190 S{material_bed_temperature_layer_0}\nM109 S{material_print_temperature_layer_0}\n\nG1 X0.1 Y20 Z0.3 F5000.0 ; Move to start position\nG1 X0.1 Y200.0 Z0.3 F1500.0 E15 ; Draw the first line\nG1 X0.4 Y200.0 Z0.3 F5000.0 ; Move to side a little\nG1 X0.4 Y20 Z0.3 F1500.0 E30 ; Draw the second line\nG92 E0 ; Reset Extruder\nG1 Z2.0 F3000 ; Move Z Axis up little to prevent scratching of Heat Bed\nG1 X5 Y20 Z0.3 F5000.0 ; Move over to prevent blob squish\n" },
"machine_width": { "default_value": 220 },
"retraction_amount": { "value": 0.8 },
"retraction_extrusion_window": { "value": 1.5 },

View file

@ -24,7 +24,7 @@
},
"machine_height": { "default_value": 300 },
"machine_name": { "default_value": "Creality Ender-3 S1 Plus" },
"machine_start_gcode": { "default_value": "; Ender 3 S1 Plus Start G-code\n; M413 S0 ; Disable power loss recovery\nG92 E0 ; Reset Extruder\n\n; Prep surfaces before auto home for better accuracy\nM140 S{material_bed_temperature_layer_0}\nM104 S{material_print_temperature_layer_0}\n\nG28 O ; Home all untrusted axes\nG1 Z10.0 F3000 ; Move Z Axis up little to prevent scratching of Heat Bed\nG1 X0 Y0\n\nM190 S{material_bed_temperature_layer_0}\nM109 S{material_print_temperature_layer_0}\n\nG1 X0.1 Y20 Z0.3 F5000.0 ; Move to start position\nG1 X0.1 Y200.0 Z0.3 F1500.0 E15 ; Draw the first line\nG1 X0.4 Y200.0 Z0.3 F5000.0 ; Move to side a little\nG1 X0.4 Y20 Z0.3 F1500.0 E30 ; Draw the second line\nG92 E0 ; Reset Extruder\nG1 Z2.0 F3000 ; Move Z Axis up little to prevent scratching of Heat Bed\nG1 X5 Y20 Z0.3 F5000.0 ; Move over to prevent blob squish\n" },
"machine_start_gcode": { "default_value": "; Ender 3 S1 Plus Start G-code\n; M413 S0 ; Disable power loss recovery\nG92 E0 ; Reset Extruder\n\n; Prep surfaces before auto home for better accuracy\nM140 S{material_bed_temperature_layer_0}\nM104 S{material_print_temperature_layer_0}\n\nG28 ; Home all axes\nG1 Z10.0 F3000 ; Move Z Axis up little to prevent scratching of Heat Bed\nG1 X0 Y0\n\nM190 S{material_bed_temperature_layer_0}\nM109 S{material_print_temperature_layer_0}\n\nG1 X0.1 Y20 Z0.3 F5000.0 ; Move to start position\nG1 X0.1 Y200.0 Z0.3 F1500.0 E15 ; Draw the first line\nG1 X0.4 Y200.0 Z0.3 F5000.0 ; Move to side a little\nG1 X0.4 Y20 Z0.3 F1500.0 E30 ; Draw the second line\nG92 E0 ; Reset Extruder\nG1 Z2.0 F3000 ; Move Z Axis up little to prevent scratching of Heat Bed\nG1 X5 Y20 Z0.3 F5000.0 ; Move over to prevent blob squish\n" },
"machine_width": { "default_value": 300 },
"retraction_amount": { "value": 0.8 },
"retraction_extrusion_window": { "value": 1.5 },

View file

@ -24,7 +24,7 @@
},
"machine_height": { "default_value": 270 },
"machine_name": { "default_value": "Creality Ender-3 S1 Pro" },
"machine_start_gcode": { "default_value": "; Ender 3 S1 Pro Start G-code\n; M413 S0 ; Disable power loss recovery\nG92 E0 ; Reset Extruder\n\n; Prep surfaces before auto home for better accuracy\nM140 S{material_bed_temperature_layer_0}\nM104 S{material_print_temperature_layer_0}\n\nG28 O ; Home all untrusted axes\nG1 Z10.0 F3000 ; Move Z Axis up little to prevent scratching of Heat Bed\nG1 X0 Y0\n\nM190 S{material_bed_temperature_layer_0}\nM109 S{material_print_temperature_layer_0}\n\nG1 X0.1 Y20 Z0.3 F5000.0 ; Move to start position\nG1 X0.1 Y200.0 Z0.3 F1500.0 E15 ; Draw the first line\nG1 X0.4 Y200.0 Z0.3 F5000.0 ; Move to side a little\nG1 X0.4 Y20 Z0.3 F1500.0 E30 ; Draw the second line\nG92 E0 ; Reset Extruder\nG1 Z2.0 F3000 ; Move Z Axis up little to prevent scratching of Heat Bed\nG1 X5 Y20 Z0.3 F5000.0 ; Move over to prevent blob squish\n" },
"machine_start_gcode": { "default_value": "; Ender 3 S1 Pro Start G-code\n; M413 S0 ; Disable power loss recovery\nG92 E0 ; Reset Extruder\n\n; Prep surfaces before auto home for better accuracy\nM140 S{material_bed_temperature_layer_0}\nM104 S{material_print_temperature_layer_0}\n\nG28 ; Home all axes\nG1 Z10.0 F3000 ; Move Z Axis up little to prevent scratching of Heat Bed\nG1 X0 Y0\n\nM190 S{material_bed_temperature_layer_0}\nM109 S{material_print_temperature_layer_0}\n\nG1 X0.1 Y20 Z0.3 F5000.0 ; Move to start position\nG1 X0.1 Y200.0 Z0.3 F1500.0 E15 ; Draw the first line\nG1 X0.4 Y200.0 Z0.3 F5000.0 ; Move to side a little\nG1 X0.4 Y20 Z0.3 F1500.0 E30 ; Draw the second line\nG92 E0 ; Reset Extruder\nG1 Z2.0 F3000 ; Move Z Axis up little to prevent scratching of Heat Bed\nG1 X5 Y20 Z0.3 F5000.0 ; Move over to prevent blob squish\n" },
"machine_width": { "default_value": 220 },
"retraction_amount": { "value": 0.8 },
"retraction_extrusion_window": { "value": 1.5 },

View file

@ -0,0 +1,37 @@
{
"version": 2,
"name": "Creality Ender-5 S1",
"inherits": "creality_base",
"metadata":
{
"visible": true,
"quality_definition": "creality_base"
},
"overrides":
{
"build_volume_temperature": { "value": 195 },
"cool_min_layer_time": { "value": 5 },
"gantry_height": { "value": 25 },
"machine_depth": { "default_value": 225 },
"machine_end_gcode": { "default_value": "G91 ;Relative positionning\nG1 E-2 F2700 ;Retract a bit\nG1 E-2 Z0.2 F2400 ;Retract and raise Z\nG1 X5 Y5 F3000 ;Wipe out\nG1 Z10 ;Raise Z more\nG90 ;Absolute positionning\n\nG1 X0 Y0 ;Present print\nM106 S0 ;Turn-off fan\nM104 S0 ;Turn-off hotend\nM140 S0 ;Turn-off bed\n\nM84 X Y E ;Disable all steppers but Z\n" },
"machine_head_with_fans_polygon":
{
"default_value": [
[-26, 34],
[-26, -32],
[32, -32],
[32, 34]
]
},
"machine_height": { "default_value": 305 },
"machine_name": { "default_value": "Creality Ender-5 S1" },
"machine_width": { "default_value": 225 },
"material_bed_temperature": { "value": 60 },
"retraction_amount": { "value": 2 },
"retraction_speed": { "value": 45 },
"speed_print": { "value": 80.0 },
"support_z_distance": { "value": 0.25 },
"wall_thickness": { "value": "line_width * 3" },
"z_seam_type": { "value": "'sharpest_corner'" }
}
}

View file

@ -9,26 +9,13 @@
"file_formats": "text/x-gcode",
"platform": "elegoo_platform.3mf",
"exclude_materials": [
"generic_nylon_175",
"generic_nylon",
"generic_hips_175",
"generic_hips",
"generic_gffcpe",
"generic_bvoh_175",
"generic_ccfpe",
"generic_cffcpe",
"generic_cpe_plus",
"generic_cpe_175",
"generic_bvoh",
"generic_cpe",
"generic_tpu",
"generic_pp",
"generic_pc",
"generic_pc_175",
"generic_pvc",
"generic_pva",
"generic_pva_175",
"generic_pvc_175",
"generic_pp"
"generic_pvc"
],
"has_machine_quality": true,
"has_materials": true,
@ -82,7 +69,7 @@
"material_print_temperature_layer_0": { "value": "210 if material_print_temperature < 210 else material_print_temperature" },
"min_infill_area": { "value": "5" },
"minimum_interface_area": { "default_value": 10 },
"minimum_support_area": { "default_value": 3 },
"minimum_support_area": { "value": "3 if support_structure == 'normal' else 0" },
"optimize_wall_printing_order": { "default_value": true },
"prime_tower_brim_enable": { "default_value": true },
"prime_tower_min_volume": { "value": "(layer_height) * (prime_tower_size / 2)**2 * 3 * 0.5 " },

View file

@ -0,0 +1,200 @@
{
"version": 2,
"name": "ENTINA TINA2",
"inherits": "weedo_base",
"metadata":
{
"visible": true,
"author": "ENTINA",
"manufacturer": "ENTINA",
"file_formats": "text/x-gcode",
"exclude_materials": [
"3D-Fuel_PLA_PRO_Black",
"3D-Fuel_PLA_SnapSupport",
"bestfilament_abs_skyblue",
"bestfilament_petg_orange",
"bestfilament_pla_green",
"leapfrog_abs_natural",
"leapfrog_epla_natural",
"leapfrog_pva_natural",
"generic_abs_175",
"generic_asa_175",
"generic_bvoh_175",
"generic_cpe_175",
"generic_hips_175",
"generic_nylon_175",
"generic_pc_175",
"generic_petg_175",
"generic_pva_175",
"goofoo_abs",
"goofoo_asa",
"goofoo_bronze_pla",
"goofoo_emarble_pla",
"goofoo_esilk_pla",
"goofoo_hips",
"goofoo_pa_cf",
"goofoo_pa",
"goofoo_pc",
"goofoo_peek",
"goofoo_petg",
"goofoo_pla",
"goofoo_pva",
"goofoo_tpe_83a",
"goofoo_tpu_87a",
"goofoo_tpu_95a",
"goofoo_wood_pla",
"emotiontech_abs",
"emotiontech_absx",
"emotiontech_acetate",
"emotiontech_asax",
"emotiontech_bvoh",
"emotiontech_copa",
"emotiontech_hips",
"emotiontech_nylon_1030",
"emotiontech_nylon_1030cf",
"emotiontech_nylon_1070",
"emotiontech_pc",
"emotiontech_pekk",
"emotiontech_petg",
"emotiontech_pla",
"emotiontech_pla_hr_870",
"emotiontech_pva-m",
"emotiontech_pva-s",
"emotiontech_tpu98a",
"eryone_petg",
"eryone_pla_glow",
"eryone_pla_matte",
"eryone_pla_wood",
"eryone_pla",
"eryone_tpu",
"eSUN_PETG_Black",
"eSUN_PETG_Grey",
"eSUN_PETG_Purple",
"eSUN_PLA_PRO_Black",
"eSUN_PLA_PRO_Grey",
"eSUN_PLA_PRO_Purple",
"eSUN_PLA_PRO_White",
"Extrudr_GreenTECPro_Anthracite_175",
"Extrudr_GreenTECPro_Black_175",
"Extrudr_GreenTECPro_Blue_175",
"Extrudr_GreenTECPro_Nature_175",
"Extrudr_GreenTECPro_Red_175",
"Extrudr_GreenTECPro_Silver_175",
"Extrudr_GreenTECPro_White_175",
"verbatim_bvoh_175",
"Vertex_Delta_ABS",
"Vertex_Delta_PET",
"Vertex_Delta_PLA",
"Vertex_Delta_TPU",
"chromatik_pla",
"dsm_arnitel2045_175",
"dsm_novamid1070_175",
"fabtotum_abs",
"fabtotum_nylon",
"fabtotum_pla",
"fabtotum_tpu",
"fdplast_abs_tomato",
"fdplast_petg_gray",
"fdplast_pla_olive",
"fiberlogy_hd_pla",
"filo3d_pla",
"filo3d_pla_green",
"filo3d_pla_red",
"imade3d_petg_green",
"imade3d_petg_pink",
"imade3d_pla_green",
"imade3d_pla_pink",
"imade3d_petg_175",
"imade3d_pla_175",
"innofill_innoflex60_175",
"layer_one_black_pla",
"layer_one_dark_gray_pla",
"layer_one_white_pla",
"octofiber_pla",
"polyflex_pla",
"polymax_pla",
"polyplus_pla",
"polywood_pla",
"redd_abs",
"redd_asa",
"redd_hips",
"redd_nylon",
"redd_petg",
"redd_pla",
"redd_tpe",
"tizyx_abs",
"tizyx_flex",
"tizyx_petg",
"tizyx_pla_bois",
"tizyx_pla",
"tizyx_pva",
"Vertex_Delta_ABS",
"Vertex_Delta_PET",
"Vertex_Delta_PLA_Glitter",
"Vertex_Delta_PLA_Mat",
"Vertex_Delta_PLA_Satin",
"Vertex_Delta_PLA_Wood",
"Vertex_Delta_PLA",
"Vertex_Delta_TPU",
"volumic_abs_ultra",
"volumic_arma_ultra",
"volumic_asa_ultra",
"volumic_br80_ultra",
"volumic_bumper_ultra",
"volumic_cu80_ultra",
"volumic_flex93_ultra",
"volumic_medical_ultra",
"volumic_nylon_ultra",
"volumic_pekk_carbone",
"volumic_petg_ultra",
"volumic_petgcarbone_ultra",
"volumic_pla_ultra",
"volumic_pp_ultra",
"volumic_strong_ultra",
"volumic_support_ultra",
"xyzprinting_abs",
"xyzprinting_antibact_pla",
"xyzprinting_carbon_fiber",
"xyzprinting_colorinkjet_pla",
"xyzprinting_flexible",
"xyzprinting_metallic_pla",
"xyzprinting_nylon",
"xyzprinting_petg",
"xyzprinting_pla",
"xyzprinting_tough_pla",
"xyzprinting_tpu",
"zyyx_pro_flex",
"zyyx_pro_pla"
],
"platform_offset": [
0,
0,
0
]
},
"overrides":
{
"infill_pattern": { "value": "'lines' if infill_sparse_density > 45 else 'grid'" },
"machine_depth": { "default_value": 120 },
"machine_end_gcode": { "default_value": ";(**** end.gcode for tina2****)\nM203 Z15\nM104 S0\nM107\nG92 E0 (Reset after prime)\nG0 E-1 F300\nG28 Z F300\nG28 X0 Y0\nG1 Y90 F1000" },
"machine_heated_bed": { "default_value": false },
"machine_height": { "default_value": 100 },
"machine_name": { "default_value": "ENTINA TINA2" },
"machine_start_gcode": { "default_value": ";MachineType:{machine_name}\n;FilamentType:{material_type}\n;InfillDensity:{infill_sparse_density}\n;Extruder0Temperature:{material_print_temperature}\n\n;(**** start.gcode for tina2****)\nM203 Z15\nM104 S150\nG28 Z\nG28 X Y; Home extruder\nG1 X55 Y55 F1000\nG29\nM107 ; Turn off fan\nG90 ; Absolute positioning\nM82 ; Extruder in absolute mode\nM109 S{material_print_temperature_layer_0}\nG92 E0 ; Reset extruder position\nG1 X90 Y6 Z0.27 F2000\nG1 X20 Y6 Z0.27 E15 F1000\nG92 E0 ; Reset extruder position\nM203 Z5" },
"machine_width": { "default_value": 100 },
"raft_base_thickness": { "value": 0.35 },
"speed_infill": { "value": 40.0 },
"speed_print": { "value": 40.0 },
"speed_print_layer_0": { "value": 22.0 },
"speed_roofing": { "value": 25.0 },
"speed_support_bottom": { "value": 30.0 },
"speed_support_infill": { "value": 45.0 },
"speed_support_roof": { "value": 30.0 },
"speed_topbottom": { "value": 30.0 },
"speed_travel": { "value": 65.0 },
"speed_travel_layer_0": { "value": 60 },
"speed_wall": { "value": 25.0 },
"speed_wall_0": { "value": 20.0 },
"speed_wall_x": { "value": 25.0 }
}
}

View file

@ -0,0 +1,201 @@
{
"version": 2,
"name": "ENTINA TINA2S",
"inherits": "weedo_base",
"metadata":
{
"visible": true,
"author": "ENTINA",
"manufacturer": "ENTINA",
"file_formats": "text/x-gcode",
"exclude_materials": [
"3D-Fuel_PLA_PRO_Black",
"3D-Fuel_PLA_SnapSupport",
"bestfilament_abs_skyblue",
"bestfilament_petg_orange",
"bestfilament_pla_green",
"leapfrog_abs_natural",
"leapfrog_epla_natural",
"leapfrog_pva_natural",
"generic_abs_175",
"generic_asa_175",
"generic_cpe_175",
"generic_nylon_175",
"generic_pc_175",
"generic_petg_175",
"generic_pva_175",
"goofoo_abs",
"goofoo_asa",
"goofoo_bronze_pla",
"goofoo_emarble_pla",
"goofoo_esilk_pla",
"goofoo_hips",
"goofoo_pa_cf",
"goofoo_pa",
"goofoo_pc",
"goofoo_peek",
"goofoo_petg",
"goofoo_pla",
"goofoo_pva",
"goofoo_tpe_83a",
"goofoo_tpu_87a",
"goofoo_tpu_95a",
"goofoo_wood_pla",
"emotiontech_abs",
"emotiontech_absx",
"emotiontech_acetate",
"emotiontech_asax",
"emotiontech_bvoh",
"emotiontech_copa",
"emotiontech_hips",
"emotiontech_nylon_1030",
"emotiontech_nylon_1030cf",
"emotiontech_nylon_1070",
"emotiontech_pc",
"emotiontech_pekk",
"emotiontech_petg",
"emotiontech_pla",
"emotiontech_pla_hr_870",
"emotiontech_pva-m",
"emotiontech_pva-s",
"emotiontech_tpu98a",
"eryone_petg",
"eryone_pla_glow",
"eryone_pla_matte",
"eryone_pla_wood",
"eryone_pla",
"eryone_tpu",
"eSUN_PETG_Black",
"eSUN_PETG_Grey",
"eSUN_PETG_Purple",
"eSUN_PLA_PRO_Black",
"eSUN_PLA_PRO_Grey",
"eSUN_PLA_PRO_Purple",
"eSUN_PLA_PRO_White",
"Extrudr_GreenTECPro_Anthracite_175",
"Extrudr_GreenTECPro_Black_175",
"Extrudr_GreenTECPro_Blue_175",
"Extrudr_GreenTECPro_Nature_175",
"Extrudr_GreenTECPro_Red_175",
"Extrudr_GreenTECPro_Silver_175",
"Extrudr_GreenTECPro_White_175",
"verbatim_bvoh_175",
"Vertex_Delta_ABS",
"Vertex_Delta_PET",
"Vertex_Delta_PLA",
"Vertex_Delta_TPU",
"chromatik_pla",
"dsm_arnitel2045_175",
"dsm_novamid1070_175",
"fabtotum_abs",
"fabtotum_nylon",
"fabtotum_pla",
"fabtotum_tpu",
"fdplast_abs_tomato",
"fdplast_petg_gray",
"fdplast_pla_olive",
"fiberlogy_hd_pla",
"filo3d_pla",
"filo3d_pla_green",
"filo3d_pla_red",
"imade3d_petg_green",
"imade3d_petg_pink",
"imade3d_pla_green",
"imade3d_pla_pink",
"imade3d_petg_175",
"imade3d_pla_175",
"innofill_innoflex60_175",
"layer_one_black_pla",
"layer_one_dark_gray_pla",
"layer_one_white_pla",
"octofiber_pla",
"polyflex_pla",
"polymax_pla",
"polyplus_pla",
"polywood_pla",
"redd_abs",
"redd_asa",
"redd_hips",
"redd_nylon",
"redd_petg",
"redd_pla",
"redd_tpe",
"tizyx_abs",
"tizyx_flex",
"tizyx_petg",
"tizyx_pla_bois",
"tizyx_pla",
"tizyx_pva",
"Vertex_Delta_ABS",
"Vertex_Delta_PET",
"Vertex_Delta_PLA_Glitter",
"Vertex_Delta_PLA_Mat",
"Vertex_Delta_PLA_Satin",
"Vertex_Delta_PLA_Wood",
"Vertex_Delta_PLA",
"Vertex_Delta_TPU",
"volumic_abs_ultra",
"volumic_arma_ultra",
"volumic_asa_ultra",
"volumic_br80_ultra",
"volumic_bumper_ultra",
"volumic_cu80_ultra",
"volumic_flex93_ultra",
"volumic_medical_ultra",
"volumic_nylon_ultra",
"volumic_pekk_carbone",
"volumic_petg_ultra",
"volumic_petgcarbone_ultra",
"volumic_pla_ultra",
"volumic_pp_ultra",
"volumic_strong_ultra",
"volumic_support_ultra",
"xyzprinting_abs",
"xyzprinting_antibact_pla",
"xyzprinting_carbon_fiber",
"xyzprinting_colorinkjet_pla",
"xyzprinting_flexible",
"xyzprinting_metallic_pla",
"xyzprinting_nylon",
"xyzprinting_petg",
"xyzprinting_pla",
"xyzprinting_tough_pla",
"xyzprinting_tpu",
"zyyx_pro_flex",
"zyyx_pro_pla"
],
"platform_offset": [
0,
0,
0
]
},
"overrides":
{
"machine_depth": { "default_value": 110 },
"machine_end_gcode": { "default_value": ";(**** end.gcode for tina2****)\nM203 Z15\nM104 S0\nM107\nG92 E0 (Reset after prime)\nG0 E-1 F300\nG28 Z F300\nG28 X0 Y0\nG1 Y90 F1000" },
"machine_height": { "default_value": 100 },
"machine_name": { "default_value": "ENTINA TINA2S" },
"machine_start_gcode": { "default_value": ";MachineType:{machine_name}\n;FilamentType:{material_type}\n;InfillDensity:{infill_sparse_density}\n;Extruder0Temperature:{material_print_temperature}\n;BedTemperature:{material_bed_temperature}\n\n;(**** start.gcode for tina2****)\nM203 Z15\nM104 S150\nG28 Z\nG28 X Y; Home extruder\nG1 X55 Y55 F1000\nG29\nM107 ; Turn off fan\nG90 ; Absolute positioning\nM82 ; Extruder in absolute mode\nM109 S{material_print_temperature_layer_0}\nG92 E0 ; Reset extruder position\nG1 X90 Y6 Z0.27 F2000\nG1 X20 Y6 Z0.27 E15 F1000\nG92 E0 ; Reset extruder position\nM203 Z5" },
"machine_width": { "default_value": 100 },
"material_bed_temperature":
{
"maximum_value": "65",
"maximum_value_warning": "60"
},
"raft_base_thickness": { "value": 0.35 },
"speed_infill": { "value": 40.0 },
"speed_print": { "value": 40.0 },
"speed_print_layer_0": { "value": 22.0 },
"speed_roofing": { "value": 25.0 },
"speed_support_bottom": { "value": 30.0 },
"speed_support_infill": { "value": 45.0 },
"speed_support_roof": { "value": 30.0 },
"speed_topbottom": { "value": 30.0 },
"speed_travel": { "value": 65.0 },
"speed_travel_layer_0": { "value": 60 },
"speed_wall": { "value": 25.0 },
"speed_wall_0": { "value": 20.0 },
"speed_wall_x": { "value": 25.0 }
}
}

View file

@ -1289,7 +1289,7 @@
"hole_xy_offset":
{
"label": "Hole Horizontal Expansion",
"description": "Amount of offset applied to all holes in each layer. Positive values increase the size of the holes, negative values reduce the size of the holes.",
"description": "When greater than zero, the Hole Horizontal Expansion is the amount of offset applied to all holes in each layer. Positive values increase the size of the holes, negative values reduce the size of the holes. When this setting is enabled it can be further tuned with Hole Horizontal Expansion Max Diameter.",
"unit": "mm",
"type": "float",
"minimum_value_warning": "-1",
@ -1664,17 +1664,27 @@
"small_skin_width":
{
"label": "Small Top/Bottom Width",
"description": "Small top/bottom regions are filled with walls instead of the default top/bottom pattern. This helps to avoids jerky motions.",
"value": "0",
"default_value": 0,
"description": "Small top/bottom regions are filled with walls instead of the default top/bottom pattern. This helps to avoids jerky motions. Off for the topmost (air-exposed) layer by default (see 'Small Top/Bottom On Surface').",
"value": "skin_line_width * 2",
"default_value": 1,
"minimum_value": "0",
"maximum_value_warning": "skin_line_width * 10",
"maximum_value_warning": "skin_line_width * 3",
"type": "float",
"enabled": false,
"enabled": "(top_layers > 0 or bottom_layers > 0) and top_bottom_pattern != 'concentric'",
"limit_to_extruder": "top_bottom_extruder_nr",
"settable_per_mesh": true,
"unit": "mm",
"comment": "Disabled for 5.4.x, as we're worried about micro-segments in the infill. Also disabled in the engine, so forcing this > 0 will not do anything at the moment."
"unit": "mm"
},
"small_skin_on_surface":
{
"label": "Small Top/Bottom On Surface",
"description": "Enable small (up to 'Small Top/Bottom Width') regions on the topmost skinned layer (exposed to air) to be filled with walls instead of the default pattern.",
"value": "False",
"default_value": false,
"type": "bool",
"enabled": "small_skin_width > 0 and top_layers > 0",
"limit_to_extruder": "top_bottom_extruder_nr",
"settable_per_mesh": true
},
"skin_no_small_gaps_heuristic":
{
@ -4490,7 +4500,7 @@
"type": "extruder",
"default_value": "0",
"enabled": "(support_enable or support_meshes_present) and extruders_enabled_count > 1",
"value": "int(defaultExtruderPosition())",
"value": "int(anyExtruderWithMaterial('material_is_support_material'))",
"settable_per_mesh": false,
"settable_per_extruder": false,
"children":
@ -5504,7 +5514,9 @@
"unit": "mm",
"type": "float",
"default_value": 0.0,
"maximum_value": "extruderValue(support_extruder_nr, 'support_offset')",
"maximum_value": "extruderValue(support_extruder_nr, 'support_offset') if support_structure == 'normal' else None",
"minimum_value_warning": "-1 * machine_nozzle_size",
"maximum_value_warning": "10 * machine_nozzle_size",
"limit_to_extruder": "support_interface_extruder_nr",
"enabled": "support_interface_enable and (support_enable or support_meshes_present)",
"settable_per_mesh": false,
@ -5519,7 +5531,9 @@
"type": "float",
"default_value": 0.0,
"value": "extruderValue(support_roof_extruder_nr, 'support_interface_offset')",
"maximum_value": "extruderValue(support_extruder_nr, 'support_offset')",
"maximum_value": "extruderValue(support_extruder_nr, 'support_offset') if support_structure == 'normal' else None",
"minimum_value_warning": "-1 * machine_nozzle_size",
"maximum_value_warning": "10 * machine_nozzle_size",
"limit_to_extruder": "support_roof_extruder_nr",
"enabled": "support_roof_enable and (support_enable or support_meshes_present)",
"settable_per_mesh": false,
@ -5533,7 +5547,9 @@
"type": "float",
"default_value": 0.0,
"value": "extruderValue(support_bottom_extruder_nr, 'support_interface_offset')",
"maximum_value": "extruderValue(support_extruder_nr, 'support_offset')",
"maximum_value": "extruderValue(support_extruder_nr, 'support_offset') if support_structure == 'normal' else None",
"minimum_value_warning": "-1 * machine_nozzle_size",
"maximum_value_warning": "10 * machine_nozzle_size",
"limit_to_extruder": "support_bottom_extruder_nr",
"enabled": "support_bottom_enable and (support_enable or support_meshes_present)",
"settable_per_mesh": false,
@ -5921,7 +5937,7 @@
"maximum_value_warning": "skirt_brim_line_width",
"enabled": "resolveOrValue('adhesion_type') == 'brim'",
"limit_to_extruder": "skirt_brim_extruder_nr",
"settable_per_mesh": true,
"settable_per_mesh": false,
"settable_per_extruder": true
},
"brim_replaces_support":
@ -6805,6 +6821,48 @@
"minimum_value_warning": "500",
"maximum_value_warning": "100000",
"settable_per_mesh": true
},
"meshfix_fluid_motion_enabled":
{
"label": "Enable Fluid Motion",
"description": "When enabled tool paths are corrected for printers with smooth motion planners. Small movements that deviate from the general tool path direction are smoothed to improve fluid motions.",
"type": "bool",
"default_value": true
},
"meshfix_fluid_motion_shift_distance":
{
"label": "Fluid Motion Shift Distance",
"description": "Distance points are shifted to smooth the path",
"enabled": "meshfix_fluid_motion_enabled",
"type": "float",
"unit": "mm",
"default_value": 0.1,
"minimum_value": "0.01",
"maximum_value": "1"
},
"meshfix_fluid_motion_small_distance":
{
"label": "Fluid Motion Small Distance",
"description": "Distance points are shifted to smooth the path",
"enabled": "meshfix_fluid_motion_enabled",
"unit": "mm",
"type": "float",
"default_value": 0.01,
"minimum_value": "0.01",
"maximum_value": "0.1"
},
"meshfix_fluid_motion_angle":
{
"label": "Fluid Motion Angle",
"description": "If a toolpath-segment deviates more than this angle from the general motion it is smoothed.",
"enabled": "meshfix_fluid_motion_enabled",
"type": "float",
"unit": "\u00b0",
"default_value": 15,
"maximum_value": "90",
"minimum_value": "0",
"minimum_value_warning": "1",
"maximum_value_warning": "35"
}
}
},

View file

@ -9,83 +9,12 @@
"manufacturer": "Flying Bear",
"file_formats": "text/x-gcode",
"exclude_materials": [
"3D-Fuel_PLA_PRO_Black",
"3D-Fuel_PLA_SnapSupport",
"chromatik_pla",
"dsm_arnitel2045_175",
"dsm_novamid1070_175",
"emotiontech_abs",
"emotiontech_absx",
"emotiontech_acetate",
"emotiontech_asax",
"emotiontech_bvoh",
"emotiontech_hips",
"emotiontech_petg",
"emotiontech_pla",
"emotiontech_pva-m",
"emotiontech_pva-s",
"emotiontech_tpu98a",
"fabtotum_abs",
"fabtotum_nylon",
"fabtotum_pla",
"fabtotum_tpu",
"fiberlogy_hd_pla",
"filo3d_pla",
"filo3d_pla_green",
"filo3d_pla_red",
"generic_abs",
"generic_bam",
"generic_cffcpe",
"generic_cffpa",
"generic_cpe",
"generic_cpe_plus",
"generic_gffcpe",
"generic_gffpa",
"generic_hips",
"generic_nylon",
"generic_pc",
"generic_petg",
"generic_pla",
"generic_pp",
"generic_pva",
"generic_tough_pla",
"generic_tpu",
"imade3d_petg_175",
"imade3d_pla_175",
"innofill_innoflex60_175",
"leapfrog_abs_natural",
"leapfrog_epla_natural",
"leapfrog_pva_natural",
"octofiber_pla",
"polyflex_pla",
"polymax_pla",
"polyplus_pla",
"polywood_pla",
"redd_abs",
"redd_asa",
"redd_hips",
"redd_nylon",
"redd_petg",
"redd_pla",
"redd_tpe",
"structur3d_dap100silicone",
"tizyx_abs",
"tizyx_flex",
"tizyx_petg",
"tizyx_pla",
"tizyx_pla_bois",
"tizyx_pva",
"verbatim_bvoh_175",
"Vertex_Delta_ABS",
"Vertex_Delta_PET",
"Vertex_Delta_PLA",
"Vertex_Delta_PLA_Glitter",
"Vertex_Delta_PLA_Mat",
"Vertex_Delta_PLA_Satin",
"Vertex_Delta_PLA_Wood",
"Vertex_Delta_TPU",
"zyyx_pro_flex",
"zyyx_pro_pla"
"dsm_arnitel2045",
"dsm_novamid1070",
"imade3d_petg",
"imade3d_pla",
"innofill_innoflex60",
"verbatim_bvoh"
],
"first_start_actions": [ "MachineSettingsAction" ],
"has_machine_quality": true,

View file

@ -9,144 +9,22 @@
"manufacturer": "Fusion3Design",
"file_formats": "text/x-gcode",
"exclude_materials": [
"bestfilament_abs_skyblue",
"bestfilament_petg_orange",
"bestfilament_pla_green",
"chromatik_pla",
"dsm_arnitel2045_175",
"dsm_novamid1070_175",
"emotiontech_abs",
"emotiontech_absx",
"emotiontech_acetate",
"emotiontech_asax",
"emotiontech_bvoh",
"emotiontech_copa",
"emotiontech_hips",
"emotiontech_nylon_1030",
"emotiontech_nylon_1030cf",
"emotiontech_nylon_1070",
"emotiontech_pc",
"emotiontech_pekk",
"emotiontech_petg",
"emotiontech_pla",
"emotiontech_pla_hr_870",
"emotiontech_pva-m",
"emotiontech_pva-s",
"emotiontech_tpu98a",
"eryone_petg",
"eryone_pla",
"eryone_pla_glow",
"eryone_pla_matte",
"eryone_pla_wood",
"eSUN_PETG_Black",
"eSUN_PETG_Grey",
"eSUN_PETG_Purple",
"Extrudr_GreenTECPro_Anthracite_175",
"Extrudr_GreenTECPro_Black_175",
"Extrudr_GreenTECPro_Blue_175",
"Extrudr_GreenTECPro_Nature_175",
"Extrudr_GreenTECPro_Red_175",
"Extrudr_GreenTECPro_Silver_175",
"Extrudr_GreenTECPro_White_175",
"fabtotum_abs",
"fabtotum_nylon",
"fabtotum_pla",
"fabtotum_tpu",
"fdplast_abs_tomato",
"fdplast_petg_gray",
"fdplast_pla_olive",
"fiberlogy_hd_pla",
"filo3d_pla",
"filo3d_pla_green",
"filo3d_pla_red",
"generic_bam",
"generic_bvoh_175",
"generic_gffcpe",
"generic_gffpa",
"dsm_arnitel2045",
"dsm_novamid1070",
"Extrudr_GreenTECPro_Anthracite",
"Extrudr_GreenTECPro_Black",
"Extrudr_GreenTECPro_Blue",
"Extrudr_GreenTECPro_Nature",
"Extrudr_GreenTECPro_Red",
"Extrudr_GreenTECPro_Silver",
"Extrudr_GreenTECPro_White",
"generic_bvoh",
"generic_hips",
"generic_hips_175",
"generic_pp",
"generic_pva",
"generic_pva_175",
"goofoo_abs",
"goofoo_asa",
"goofoo_bronze_pla",
"goofoo_emarble_pla",
"goofoo_esilk_pla",
"goofoo_hips",
"goofoo_pa",
"goofoo_pa_cf",
"goofoo_pc",
"goofoo_peek",
"goofoo_petg",
"goofoo_pla",
"goofoo_pva",
"goofoo_tpe_83a",
"goofoo_tpu_87a",
"goofoo_tpu_95a",
"goofoo_wood_pla",
"imade3d_petg_175",
"imade3d_pla_175",
"innofill_innoflex60_175",
"layer_one_black_pla",
"layer_one_dark_gray_pla",
"layer_one_white_pla",
"leapfrog_abs_natural",
"leapfrog_epla_natural",
"leapfrog_pva_natural",
"octofiber_pla",
"redd_abs",
"redd_asa",
"redd_hips",
"redd_nylon",
"redd_petg",
"redd_pla",
"redd_tpe",
"structur3d_dap100silicone",
"tizyx_abs",
"tizyx_flex",
"tizyx_petg",
"tizyx_pla",
"tizyx_pla_bois",
"tizyx_pva",
"verbatim_bvoh_175",
"Vertex_Delta_ABS",
"Vertex_Delta_PET",
"Vertex_Delta_PLA",
"Vertex_Delta_PLA_Glitter",
"Vertex_Delta_PLA_Mat",
"Vertex_Delta_PLA_Satin",
"Vertex_Delta_PLA_Wood",
"Vertex_Delta_TPU",
"volumic_abs_ultra",
"volumic_arma_ultra",
"volumic_asa_ultra",
"volumic_br80_ultra",
"volumic_bumper_ultra",
"volumic_cu80_ultra",
"volumic_flex93_ultra",
"volumic_medical_ultra",
"volumic_nylon_ultra",
"volumic_pekk_carbone",
"volumic_petgcarbone_ultra",
"volumic_petg_ultra",
"volumic_pla_ultra",
"volumic_pp_ultra",
"volumic_strong_ultra",
"volumic_support_ultra",
"xyzprinting_abs",
"xyzprinting_antibact_pla",
"xyzprinting_carbon_fiber",
"xyzprinting_colorinkjet_pla",
"xyzprinting_flexible",
"xyzprinting_metallic_pla",
"xyzprinting_nylon",
"xyzprinting_petg",
"xyzprinting_pla",
"xyzprinting_tough_pla",
"xyzprinting_tpu",
"zyyx_pro_flex",
"zyyx_pro_pla"
"imade3d_petg",
"imade3d_pla",
"innofill_innoflex60",
"verbatim_bvoh"
],
"first_start_actions": [ "MachineSettingsAction" ],
"has_machine_quality": true,

View file

@ -2,25 +2,7 @@
"version": 2,
"name": "Goofoo E-one",
"inherits": "goofoo_open",
"metadata":
{
"visible": true,
"exclude_materials": [
"goofoo_bronze_pla",
"goofoo_peek",
"goofoo_tpe_83a",
"goofoo_tpu_87a",
"goofoo_tpu_95a",
"goofoo_pa_cf",
"goofoo_pc",
"goofoo_pa",
"goofoo_asa",
"goofoo_abs",
"goofoo_pva",
"goofoo_hips",
"goofoo_pva"
]
},
"metadata": { "visible": true },
"overrides":
{
"machine_depth": { "default_value": 300 },

View file

@ -5,12 +5,6 @@
"metadata":
{
"visible": false,
"exclude_materials": [
"goofoo_bronze_pla",
"goofoo_tpe_83a",
"goofoo_tpu_87a",
"goofoo_tpu_95a"
],
"quality_definition": "goofoo_far"
}
}

View file

@ -5,18 +5,6 @@
"metadata":
{
"visible": false,
"exclude_materials": [
"goofoo_bronze_pla",
"goofoo_peek",
"goofoo_tpe_83a",
"goofoo_tpu_87a",
"goofoo_tpu_95a",
"goofoo_pa_cf",
"goofoo_pc",
"goofoo_pa",
"goofoo_asa",
"goofoo_abs"
],
"quality_definition": "goofoo_open"
}
}

View file

@ -5,20 +5,6 @@
"metadata":
{
"visible": false,
"exclude_materials": [
"goofoo_bronze_pla",
"goofoo_peek",
"goofoo_tpe_83a",
"goofoo_tpu_87a",
"goofoo_tpu_95a",
"goofoo_pa_cf",
"goofoo_pc",
"goofoo_pa",
"goofoo_asa",
"goofoo_abs",
"goofoo_pva",
"goofoo_hips"
],
"quality_definition": "goofoo_small"
}
}

View file

@ -5,21 +5,6 @@
"metadata":
{
"visible": true,
"exclude_materials": [
"goofoo_bronze_pla",
"goofoo_peek",
"goofoo_tpe_83a",
"goofoo_tpu_87a",
"goofoo_tpu_95a",
"goofoo_pa_cf",
"goofoo_pc",
"goofoo_pa",
"goofoo_asa",
"goofoo_abs",
"goofoo_pva",
"goofoo_hips",
"goofoo_pva"
],
"machine_extruder_trains":
{
"0": "goofoo_tone_1st",

View file

@ -10,79 +10,20 @@
"file_formats": "text/x-gcode",
"platform": "hms_platform.obj",
"exclude_materials": [
"chromatik_pla",
"dsm_arnitel2045_175",
"dsm_novamid1070_175",
"emotiontech_abs",
"emotiontech_asax",
"emotiontech_hips",
"emotiontech_petg",
"emotiontech_pla",
"emotiontech_pva-m",
"emotiontech_pva-oks",
"emotiontech_pva-s",
"emotiontech_tpu98a",
"emotiontech_absx",
"emotiontech_bvoh",
"eSUN_PETG_Black",
"eSUN_PETG_Grey",
"eSUN_PETG_Purple",
"eSUN_PLA_PRO_Black",
"eSUN_PLA_PRO_Grey",
"eSUN_PLA_PRO_Purple",
"eSUN_PLA_PRO_White",
"fabtotum_abs",
"fabtotum_nylon",
"fabtotum_pla",
"fabtotum_tpu",
"fiberlogy_hd_pla",
"filo3d_pla",
"filo3d_pla_green",
"filo3d_pla_red",
"dsm_arnitel2045",
"dsm_novamid1070",
"generic_abs",
"generic_abs_175",
"generic_cpe_175",
"generic_hips_175",
"generic_nylon_175",
"generic_pc_175",
"generic_petg_175",
"generic_pva_175",
"generic_tpu_175",
"imade3d_petg_175",
"imade3d_pla_175",
"innofill_innoflex60_175",
"leapfrog_abs_natural",
"leapfrog_epla_natural",
"leapfrog_pva_natural",
"octofiber_pla",
"polyflex_pla",
"polymax_pla",
"polyplus_pla",
"polywood_pla",
"redd_abs",
"redd_asa",
"redd_hips",
"redd_nylon",
"redd_petg",
"redd_pla",
"redd_tpe",
"verbatim_bvoh_175",
"Vertex_Delta_ABS",
"Vertex_Delta_PET",
"Vertex_Delta_PLA",
"Vertex_Delta_PLA_Glitter",
"Vertex_Delta_PLA_Mat",
"Vertex_Delta_PLA_Satin",
"Vertex_Delta_PLA_Wood",
"Vertex_Delta_TPU",
"tizyx_abs",
"tizyx_flex",
"tizyx_petg",
"tizyx_pla",
"tizyx_pla_bois",
"tizyx_pva",
"zyyx_pro_flex",
"zyyx_pro_pla"
"generic_cpe",
"generic_hips",
"generic_nylon",
"generic_pc",
"generic_petg",
"generic_pva",
"generic_tpu",
"imade3d_petg",
"imade3d_pla",
"innofill_innoflex60",
"verbatim_bvoh"
],
"first_start_actions": [ "MachineSettingsAction" ],
"has_machine_quality": true,

View file

@ -9,58 +9,18 @@
"manufacturer": "Imade3D",
"file_formats": "text/x-gcode",
"exclude_materials": [
"chromatik_pla",
"dsm_arnitel2045_175",
"dsm_novamid1070_175",
"fabtotum_abs",
"fabtotum_nylon",
"fabtotum_pla",
"fabtotum_tpu",
"fiberlogy_hd_pla",
"filo3d_pla_green",
"filo3d_pla_red",
"filo3d_pla",
"generic_abs_175",
"dsm_arnitel2045",
"dsm_novamid1070",
"generic_abs",
"generic_bam",
"generic_cpe_175",
"generic_cpe_plus",
"generic_cpe",
"generic_hips_175",
"generic_hips",
"generic_nylon_175",
"generic_nylon",
"generic_pc_175",
"generic_pc",
"generic_petg",
"generic_petg_175",
"generic_pla",
"generic_pla_175",
"generic_pp",
"generic_pva_175",
"generic_pva",
"generic_tough_pla",
"generic_tpu",
"imade3d_petg_green",
"imade3d_petg_pink",
"imade3d_pla_green",
"imade3d_pla_pink",
"innofill_innoflex60_175",
"octofiber_pla",
"polyflex_pla",
"polymax_pla",
"polyplus_pla",
"polywood_pla",
"tizyx_abs",
"tizyx_pla_bois",
"tizyx_pla",
"verbatim_bvoh_175",
"Vertex_Delta_ABS",
"Vertex_Delta_PET",
"Vertex_Delta_PLA",
"Vertex_Delta_TPU",
"zyyx_pro_flex",
"zyyx_pro_pla"
"innofill_innoflex60",
"verbatim_bvoh"
]
},
"overrides":

View file

@ -9,108 +9,17 @@
"manufacturer": "Kingroon",
"file_formats": "text/x-gcode",
"exclude_materials": [
"3D-Fuel_PLA_PRO_Black",
"3D-Fuel_PLA_SnapSupport",
"bestfilament_abs_skyblue",
"bestfilament_petg_orange",
"bestfilament_pla_green",
"chromatik_pla",
"dsm_arnitel2045_175",
"dsm_novamid1070_175",
"emotiontech_abs",
"emotiontech_absx",
"emotiontech_acetate",
"emotiontech_asax",
"emotiontech_copa",
"emotiontech_nylon_1030",
"emotiontech_nylon_1030cf",
"emotiontech_nylon_1070",
"emotiontech_pc",
"emotiontech_petg",
"emotiontech_pla",
"emotiontech_pla_hr_870",
"emotiontech_bvoh",
"emotiontech_hips",
"emotiontech_pva-m",
"emotiontech_pva-s",
"emotiontech_tpu98a",
"eSUN_PLA_PRO_White",
"eSUN_PETG_Black",
"eSUN_PETG_Grey",
"eSUN_PETG_Purple",
"eSUN_PLA_PRO_Black",
"eSUN_PLA_PRO_Grey",
"eSUN_PLA_PRO_Purple",
"fabtotum_abs",
"fabtotum_nylon",
"fabtotum_pla",
"fabtotum_tpu",
"fdplast_pla_olive",
"fdplast_abs_tomato",
"fdplast_petg_gray",
"fiberlogy_hd_pla",
"filo3d_pla",
"filo3d_pla_green",
"filo3d_pla_red",
"generic_cpe_175",
"generic_hips_175",
"generic_nylon_175",
"generic_pc_175",
"generic_pva_175",
"imade3d_petg_175",
"imade3d_pla_175",
"innofill_innoflex60_175",
"layer_one_white_pla",
"layer_one_black_pla",
"layer_one_dark_gray_pla",
"leapfrog_pva_natural",
"leapfrog_abs_natural",
"leapfrog_epla_natural",
"octofiber_pla",
"polyflex_pla",
"polymax_pla",
"polyplus_pla",
"polywood_pla",
"redd_tpe",
"redd_abs",
"redd_asa",
"redd_hips",
"redd_nylon",
"redd_petg",
"redd_pla",
"tizyx_flex",
"tizyx_petg",
"tizyx_pla",
"tizyx_pla_bois",
"tizyx_pva",
"tizyx_abs",
"verbatim_bvoh_175",
"Vertex_Delta_TPU",
"Vertex_Delta_ABS",
"Vertex_Delta_PET",
"Vertex_Delta_PLA",
"Vertex_Delta_PLA_Glitter",
"Vertex_Delta_PLA_Mat",
"Vertex_Delta_PLA_Satin",
"Vertex_Delta_PLA_Wood",
"volumic_support_ultra",
"volumic_abs_ultra",
"volumic_arma_ultra",
"volumic_asa_ultra",
"volumic_br80_ultra",
"volumic_bumper_ultra",
"volumic_cu80_ultra",
"volumic_flex93_ultra",
"volumic_medical_ultra",
"volumic_nylon_ultra",
"volumic_pekk_carbone",
"volumic_petg_ultra",
"volumic_petgcarbone_ultra",
"volumic_pla_ultra",
"volumic_pp_ultra",
"volumic_strong_ultra",
"zyyx_pro_flex",
"zyyx_pro_pla"
"dsm_arnitel2045",
"dsm_novamid1070",
"generic_cpe",
"generic_hips",
"generic_nylon",
"generic_pc",
"generic_pva",
"imade3d_petg",
"imade3d_pla",
"innofill_innoflex60",
"verbatim_bvoh"
],
"first_start_actions": [ "MachineSettingsAction" ],
"has_machine_quality": true,

Some files were not shown because too many files have changed in this diff Show more