Merge branch 'Ultimaker:main' into dxu_umo_definition

This commit is contained in:
Anson Liu 2022-10-05 13:03:02 -04:00 committed by GitHub
commit 66ddf297b7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
996 changed files with 8966 additions and 4833 deletions

View File

@ -109,7 +109,7 @@ jobs:
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 sudo apt install build-essential checkinstall libegl-dev zlib1g-dev libssl-dev ninja-build autoconf libx11-dev libx11-xcb-dev libfontenc-dev libice-dev libsm-dev libxau-dev libxaw7-dev libxcomposite-dev libxcursor-dev libxdamage-dev libxdmcp-dev libxext-dev libxfixes-dev libxi-dev libxinerama-dev libxkbfile-dev libxmu-dev libxmuu-dev libxpm-dev libxrandr-dev libxrender-dev libxres-dev libxss-dev libxt-dev libxtst-dev libxv-dev libxvmc-dev libxxf86vm-dev xtrans-dev libxcb-render0-dev libxcb-render-util0-dev libxcb-xkb-dev libxcb-icccm4-dev libxcb-image0-dev libxcb-keysyms1-dev libxcb-randr0-dev libxcb-shape0-dev libxcb-sync-dev libxcb-xfixes0-dev libxcb-xinerama0-dev xkb-data libxcb-dri3-dev uuid-dev libxcb-util-dev libxkbcommon-x11-dev pkg-config -y
- name: Install GCC-12 on ubuntu-22.04 - name: Install GCC-12 on ubuntu-22.04
if: ${{ matrix.os == 'ubuntu-22.04' }} if: ${{ startsWith(inputs.runs_on, 'ubuntu-22.04') }}
run: | run: |
sudo apt install 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/gcc gcc /usr/bin/gcc-12 12

View File

@ -47,13 +47,20 @@ on:
- '[1-9].[0-9].[0-9]+' - '[1-9].[0-9].[0-9]+'
- '[1-9].[0-9][0-9].[0-9]+' - '[1-9].[0-9][0-9].[0-9]+'
permissions: {}
jobs: jobs:
conan-recipe-version: conan-recipe-version:
permissions:
contents: read
uses: ultimaker/cura/.github/workflows/conan-recipe-version.yml@main uses: ultimaker/cura/.github/workflows/conan-recipe-version.yml@main
with: with:
project_name: cura project_name: cura
conan-package-export: conan-package-export:
permissions:
contents: read
needs: [ conan-recipe-version ] needs: [ conan-recipe-version ]
uses: ultimaker/cura/.github/workflows/conan-recipe-export.yml@main uses: ultimaker/cura/.github/workflows/conan-recipe-export.yml@main
with: with:
@ -65,6 +72,9 @@ jobs:
secrets: inherit secrets: inherit
conan-package-create-linux: conan-package-create-linux:
permissions:
contents: read
if: ${{ (github.event_name == 'push' && (github.ref_name == 'main' || github.ref_name == 'master' || needs.conan-recipe-version.outputs.is_release_branch == 'true')) || (github.event_name == 'workflow_dispatch' && inputs.create_binaries_linux) }} if: ${{ (github.event_name == 'push' && (github.ref_name == 'main' || github.ref_name == 'master' || needs.conan-recipe-version.outputs.is_release_branch == 'true')) || (github.event_name == 'workflow_dispatch' && inputs.create_binaries_linux) }}
needs: [ conan-recipe-version, conan-package-export ] needs: [ conan-recipe-version, conan-package-export ]

View File

@ -53,9 +53,18 @@ jobs:
steps: steps:
- name: Checkout repo - name: Checkout repo
uses: actions/checkout@v3 uses: actions/checkout@v3
if: ${{ github.event.pull_request.head.repo.full_name == github.repository }}
with: with:
ref: ${{ github.head_ref }}
fetch-depth: 0 fetch-depth: 0
ref: ${{ github.head_ref }}
- name: Checkout repo PR
uses: actions/checkout@v3
if: ${{ github.event.pull_request.head.repo.full_name != github.repository }}
with:
fetch-depth: 0
ref: ${{ github.event.pull_request.head.ref }}
repository: ${{ github.event.pull_request.head.repo.full_name }}
- name: Setup Python and pip - name: Setup Python and pip
uses: actions/setup-python@v4 uses: actions/setup-python@v4
@ -93,6 +102,7 @@ jobs:
is_release_branch = True is_release_branch = True
channel = "_" channel = "_"
user = "_" user = "_"
actual_version = f"{branch_version}"
else: else:
try: try:
branch_version = tools.Version(repo.active_branch.name) branch_version = tools.Version(repo.active_branch.name)
@ -106,48 +116,58 @@ jobs:
else: else:
channel = repo.active_branch.name.split("_")[0].replace("-", "_").lower() channel = repo.active_branch.name.split("_")[0].replace("-", "_").lower()
if event_name == "pull_request": if "pull_request" in event_name:
channel = f"pr_{issue_number}" channel = f"pr_{issue_number}"
# %% Get the actual version # %% Get the actual version
latest_branch_version = tools.Version("0.0.0") latest_branch_version = tools.Version("0.0.0")
latest_branch_tag = None latest_branch_tag = None
for tag in repo.git.tag(merged = True).splitlines(): for tag in repo.git.tag(merged = True).splitlines():
try: if str(tag).startswith("firmware") or str(tag).startswith("master"):
version = tools.Version(tag) continue # Quick-fix for the versioning scheme name of the embedded team in fdm_materials(_private) repo
except ConanException: try:
continue version = tools.Version(tag)
if version > latest_branch_version: except ConanException:
latest_branch_version = version continue
latest_branch_tag = repo.tag(tag) if version > latest_branch_version:
latest_branch_version = version
# %% Get the actual version latest_branch_tag = repo.tag(tag)
no_commits = 0
for commit in repo.iter_commits("HEAD"): if latest_branch_tag:
if commit == latest_branch_tag.commit: # %% Get the actual version
break no_commits = 0
no_commits += 1 for commit in repo.iter_commits("HEAD"):
if commit == latest_branch_tag.commit:
if no_commits == 0: break
# This is a release no_commits += 1
actual_version = f"{latest_branch_version.major}.{latest_branch_version.minor}.{latest_branch_version.patch}" latest_branch_version_prerelease = latest_branch_version.prerelease
if channel == "stable": if latest_branch_version.prerelease and not "." in latest_branch_version.prerelease:
user = "_" # The prerealese did not contain a version number, default it to 1
channel = "_" latest_branch_version_prerelease = f"{latest_branch_version.prerelease}.1"
else: if event_name == "pull_request":
if event_name == "pull_request": actual_version = f"{latest_branch_version.major}.{latest_branch_version.minor}.{latest_branch_version.patch}-{latest_branch_version_prerelease.lower()}+{buildmetadata}pr_{issue_number}_{no_commits}"
actual_version = f"{latest_branch_version.major}.{latest_branch_version.minor}.{latest_branch_version.patch}-{latest_branch_version.prerelease.lower()}+{buildmetadata}pr_{issue_number}_{no_commits}"
else:
if channel in ("stable", "_", ""):
channel_metadata = f"{no_commits}"
else:
channel_metadata = f"{channel}_{no_commits}" channel_metadata = f"{channel}_{no_commits}"
# FIXME: for when we create a new release branch
if latest_branch_version.prerelease == "":
bump_up_minor = int(latest_branch_version.minor) + 1
actual_version = f"{latest_branch_version.major}.{bump_up_minor}.{latest_branch_version.patch}-alpha+{buildmetadata}{channel_metadata}"
else: else:
actual_version = f"{latest_branch_version.major}.{latest_branch_version.minor}.{latest_branch_version.patch}-{latest_branch_version.prerelease.lower()}+{buildmetadata}{channel_metadata}" if channel in ("stable", "_", ""):
channel_metadata = f"{no_commits}"
else:
channel_metadata = f"{channel}_{no_commits}"
if is_release_branch:
if latest_branch_version.prerelease == "":
# An actual full release has been created, we are working on patch
bump_up_patch = int(latest_branch_version.patch) + 1
actual_version = f"{latest_branch_version.major}.{latest_branch_version.minor}.{bump_up_patch}-beta.1+{buildmetadata}{channel_metadata}"
else:
# An beta release has been created we are working toward a next beta or full release
bump_up_release_tag = int(latest_branch_version.prerelease.split('.')[1]) + 1
actual_version = f"{latest_branch_version.major}.{latest_branch_version.minor}.{latest_branch_version.patch}-{latest_branch_version.prerelease.split('.')[0]}.{bump_up_release_tag}+{buildmetadata}{channel_metadata}"
else:
bump_up_minor = int(latest_branch_version.minor) + 1
reset_patch = 0
actual_version = f"{latest_branch_version.major}.{bump_up_minor}.{reset_patch}-alpha+{buildmetadata}{channel_metadata}"
else:
# FIXME: for external PR's
actual_version = f"5.3.0-alpha+{buildmetadata}pr_{issue_number}"
# %% print to output # %% print to output
cmd_name = ["echo", f"::set-output name=name::{project_name}"] cmd_name = ["echo", f"::set-output name=name::{project_name}"]

View File

@ -1,4 +1,5 @@
name: Cura Installer name: Cura Installer
run-name: Cura binaries ${{ inputs.cura_conan_version }} by @${{ github.actor }}
on: on:
workflow_dispatch: workflow_dispatch:

View File

@ -1,2 +1,2 @@
conan!=1.51.0,!=1.51.1,!=1.51.2,!=1.51.3 conan!=1.51.0,!=1.51.1,!=1.51.2,!=1.51.3,!=1.52.0
sip==6.5.1 sip

View File

@ -60,6 +60,9 @@ env:
CONAN_LOGGING_LEVEL: info CONAN_LOGGING_LEVEL: info
CONAN_NON_INTERACTIVE: 1 CONAN_NON_INTERACTIVE: 1
permissions:
contents: read
jobs: jobs:
conan-recipe-version: conan-recipe-version:
uses: ultimaker/cura/.github/workflows/conan-recipe-version.yml@main uses: ultimaker/cura/.github/workflows/conan-recipe-version.yml@main
@ -103,7 +106,18 @@ jobs:
- name: Install Linux system requirements - name: Install Linux system requirements
if: ${{ runner.os == 'Linux' }} if: ${{ runner.os == 'Linux' }}
run: 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 -y run: |
sudo add-apt-repository ppa:ubuntu-toolchain-r/test -y
sudo apt update
sudo apt upgrade
sudo apt install build-essential checkinstall libegl-dev zlib1g-dev libssl-dev ninja-build autoconf libx11-dev libx11-xcb-dev libfontenc-dev libice-dev libsm-dev libxau-dev libxaw7-dev libxcomposite-dev libxcursor-dev libxdamage-dev libxdmcp-dev libxext-dev libxfixes-dev libxi-dev libxinerama-dev libxkbfile-dev libxmu-dev libxmuu-dev libxpm-dev libxrandr-dev libxrender-dev libxres-dev libxss-dev libxt-dev libxtst-dev libxv-dev libxvmc-dev libxxf86vm-dev xtrans-dev libxcb-render0-dev libxcb-render-util0-dev libxcb-xkb-dev libxcb-icccm4-dev libxcb-image0-dev libxcb-keysyms1-dev libxcb-randr0-dev libxcb-shape0-dev libxcb-sync-dev libxcb-xfixes0-dev libxcb-xinerama0-dev xkb-data libxcb-dri3-dev uuid-dev libxcb-util-dev libxkbcommon-x11-dev pkg-config -y
- name: Install GCC-12 on ubuntu-22.04
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: Get Conan configuration - name: Get Conan configuration
run: conan config install https://github.com/Ultimaker/conan-config.git run: conan config install https://github.com/Ultimaker/conan-config.git
@ -133,6 +147,11 @@ jobs:
path: "tests/**/*.xml" path: "tests/**/*.xml"
publish-test-results: publish-test-results:
permissions:
contents: read # to fetch code (actions/checkout)
checks: write
pull-requests: write # to comment on pull request
runs-on: ubuntu-20.04 runs-on: ubuntu-20.04
needs: [ testing ] needs: [ testing ]
if: success() || failure() if: success() || failure()

View File

@ -1,6 +1,8 @@
# Copyright (c) 2022 Ultimaker B.V. # Copyright (c) 2022 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
# NOTE: This is only being used for translation scripts.
# For MSVC flags, will be ignored on non-Windows OS's and this project in general. Only needed for cura-build-environment. # For MSVC flags, will be ignored on non-Windows OS's and this project in general. Only needed for cura-build-environment.
cmake_policy(SET CMP0091 NEW) cmake_policy(SET CMP0091 NEW)
project(cura) project(cura)
@ -13,47 +15,8 @@ list(APPEND CMAKE_MODULE_PATH ${CMAKE_SOURCE_DIR}/cmake)
set(URANIUM_DIR "${CMAKE_SOURCE_DIR}/../Uranium" CACHE PATH "The location of the Uranium repository") set(URANIUM_DIR "${CMAKE_SOURCE_DIR}/../Uranium" CACHE PATH "The location of the Uranium repository")
set(URANIUM_SCRIPTS_DIR "${URANIUM_DIR}/scripts" CACHE PATH "The location of the scripts directory of the Uranium repository") set(URANIUM_SCRIPTS_DIR "${URANIUM_DIR}/scripts" CACHE PATH "The location of the scripts directory of the Uranium repository")
option(CURA_DEBUGMODE "Enable debug dialog and other debug features" OFF)
if(CURA_DEBUGMODE)
set(_cura_debugmode "ON")
endif()
option(GENERATE_TRANSLATIONS "Should the translations be generated?" ON) option(GENERATE_TRANSLATIONS "Should the translations be generated?" ON)
set(CURA_APP_NAME "cura" CACHE STRING "Short name of Cura, used for configuration folder")
set(CURA_APP_DISPLAY_NAME "Ultimaker Cura" CACHE STRING "Display name of Cura")
set(CURA_VERSION "master" CACHE STRING "Version name of Cura")
set(CURA_BUILDTYPE "" CACHE STRING "Build type of Cura, eg. 'PPA'")
set(CURA_CLOUD_API_ROOT "" CACHE STRING "Alternative Cura cloud API root")
set(CURA_CLOUD_API_VERSION "" CACHE STRING "Alternative Cura cloud API version")
set(CURA_CLOUD_ACCOUNT_API_ROOT "" CACHE STRING "Alternative Cura cloud account API version")
set(CURA_MARKETPLACE_ROOT "" CACHE STRING "Alternative Marketplace location")
set(CURA_DIGITAL_FACTORY_URL "" CACHE STRING "Alternative Digital Factory location")
configure_file(${CMAKE_SOURCE_DIR}/com.ultimaker.cura.desktop.in ${CMAKE_BINARY_DIR}/com.ultimaker.cura.desktop @ONLY)
configure_file(cura/CuraVersion.py.in CuraVersion.py @ONLY)
if(NOT DEFINED Python_VERSION)
set(Python_VERSION
3.10
CACHE STRING "Python Version" FORCE)
message(STATUS "Setting Python version to ${Python_VERSION}. Set Python_VERSION if you want to compile against an other version.")
endif()
if(APPLE)
set(Python_FIND_FRAMEWORK NEVER)
endif()
find_package(Python ${Python_VERSION} EXACT REQUIRED COMPONENTS Interpreter)
message(STATUS "Linking and building ${project_name} against Python ${Python_VERSION}")
if(NOT DEFINED Python_SITELIB_LOCAL)
set(Python_SITELIB_LOCAL
"${Python_SITELIB}"
CACHE PATH "Local alternative site-package location to install Cura" FORCE)
endif()
# Tests
include(CuraTests)
if(NOT ${URANIUM_DIR} STREQUAL "") if(NOT ${URANIUM_DIR} STREQUAL "")
set(CMAKE_MODULE_PATH "${CMAKE_MODULE_PATH};${URANIUM_DIR}/cmake") set(CMAKE_MODULE_PATH "${CMAKE_MODULE_PATH};${URANIUM_DIR}/cmake")
endif() endif()
@ -66,24 +29,4 @@ if(NOT ${URANIUM_SCRIPTS_DIR} STREQUAL "")
if(${GENERATE_TRANSLATIONS}) if(${GENERATE_TRANSLATIONS})
CREATE_TRANSLATION_TARGETS() CREATE_TRANSLATION_TARGETS()
endif() endif()
endif() endif()
install(DIRECTORY resources DESTINATION ${CMAKE_INSTALL_DATADIR}/cura)
include(CuraPluginInstall)
install(FILES cura_app.py DESTINATION ${CMAKE_INSTALL_BINDIR}
PERMISSIONS OWNER_READ OWNER_WRITE OWNER_EXECUTE GROUP_READ GROUP_EXECUTE WORLD_READ WORLD_EXECUTE)
install(DIRECTORY cura DESTINATION "${Python_SITELIB_LOCAL}")
install(FILES ${CMAKE_BINARY_DIR}/CuraVersion.py DESTINATION "${Python_SITELIB_LOCAL}/cura/")
if(NOT APPLE AND NOT WIN32)
install(FILES ${CMAKE_BINARY_DIR}/com.ultimaker.cura.desktop
DESTINATION ${CMAKE_INSTALL_DATADIR}/applications)
install(FILES ${CMAKE_SOURCE_DIR}/resources/images/cura-icon.png
DESTINATION ${CMAKE_INSTALL_DATADIR}/icons/hicolor/128x128/apps/)
install(FILES com.ultimaker.cura.appdata.xml
DESTINATION ${CMAKE_INSTALL_DATADIR}/metainfo)
install(FILES cura.sharedmimeinfo
DESTINATION ${CMAKE_INSTALL_DATADIR}/mime/packages/
RENAME cura.xml )
endif()

View File

@ -11,3 +11,4 @@ CuraCloudAPIVersion = "{{ cura_cloud_api_version }}"
CuraCloudAccountAPIRoot = "{{ cura_cloud_account_api_root }}" CuraCloudAccountAPIRoot = "{{ cura_cloud_account_api_root }}"
CuraMarketplaceRoot = "{{ cura_marketplace_root }}" CuraMarketplaceRoot = "{{ cura_marketplace_root }}"
CuraDigitalFactoryURL = "{{ cura_digital_factory_url }}" CuraDigitalFactoryURL = "{{ cura_digital_factory_url }}"
CuraLatestURL = "{{ cura_latest_url }}"

View File

@ -1,55 +0,0 @@
mode: ContinuousDelivery
next-version: 5.1
branches:
main:
regex: ^main$
mode: ContinuousDelivery
tag: alpha
increment: None
prevent-increment-of-merged-branch-version: true
track-merge-target: false
source-branches: [ ]
tracks-release-branches: false
is-release-branch: false
is-mainline: true
pre-release-weight: 55000
develop:
regex: ^CURA-.*$
mode: ContinuousDelivery
tag: alpha
increment: None
prevent-increment-of-merged-branch-version: false
track-merge-target: true
source-branches: [ 'main' ]
tracks-release-branches: true
is-release-branch: false
is-mainline: false
pre-release-weight: 0
release:
regex: ^[\d]+\.[\d]+$
mode: ContinuousDelivery
tag: beta
increment: None
prevent-increment-of-merged-branch-version: true
track-merge-target: false
source-branches: [ 'main' ]
tracks-release-branches: false
is-release-branch: true
is-mainline: false
pre-release-weight: 30000
pull-request-main:
regex: ^(pull|pull\-requests|pr)[/-]
mode: ContinuousDelivery
tag: alpha+
increment: Inherit
prevent-increment-of-merged-branch-version: true
tag-number-pattern: '[/-](?<number>\d+)[-/]'
track-merge-target: true
source-branches: [ 'main' ]
tracks-release-branches: false
is-release-branch: false
is-mainline: false
pre-release-weight: 30000
ignore:
sha: [ ]
merge-message-formats: { }

74
Jenkinsfile vendored
View File

@ -1,74 +0,0 @@
parallel_nodes(['linux && cura', 'windows && cura'])
{
timeout(time: 2, unit: "HOURS")
{
// Prepare building
stage('Prepare')
{
// Ensure we start with a clean build directory.
step([$class: 'WsCleanup'])
// Checkout whatever sources are linked to this pipeline.
checkout scm
}
// If any error occurs during building, we want to catch it and continue with the "finale" stage.
catchError
{
// Building and testing should happen in a subdirectory.
dir('build')
{
// Perform the "build". Since Uranium is Python code, this basically only ensures CMake is setup.
stage('Build')
{
def branch = env.BRANCH_NAME
if(!fileExists("${env.CURA_ENVIRONMENT_PATH}/${branch}"))
{
branch = "master"
}
// Ensure CMake is setup. Note that since this is Python code we do not really "build" it.
def uranium_dir = get_workspace_dir("Ultimaker/Uranium/${branch}")
cmake("..", "-DCMAKE_PREFIX_PATH=\"${env.CURA_ENVIRONMENT_PATH}/${branch}\" -DCMAKE_BUILD_TYPE=Release -DURANIUM_DIR=\"${uranium_dir}\"")
}
// Try and run the unit tests. If this stage fails, we consider the build to be "unstable".
stage('Unit Test')
{
if (isUnix())
{
// For Linux
try {
sh 'make CTEST_OUTPUT_ON_FAILURE=TRUE test'
} catch(e)
{
currentBuild.result = "UNSTABLE"
}
}
else
{
// For Windows
try
{
// This also does code style checks.
bat 'ctest -V'
} catch(e)
{
currentBuild.result = "UNSTABLE"
}
}
}
}
}
// Perform any post-build actions like notification and publishing of unit tests.
stage('Finalize')
{
// Publish the test results to Jenkins.
junit allowEmptyResults: true, testResults: 'build/junit*.xml'
notify_build_result(env.CURA_EMAIL_RECIPIENTS, '#cura-dev', ['master', '2.'])
}
}
}

132
README.md
View File

@ -1,64 +1,96 @@
# Cura
<p align="center"> <br>
<a href="https://github.com/Ultimaker/Cura/actions/workflows/unit-test.yml" alt="Unit Tests">
<img src="https://github.com/Ultimaker/Cura/actions/workflows/unit-test.yml/badge.svg" /></a>
<a href="https://github.com/Ultimaker/Cura/actions/workflows/conan-package.yml" alt="Unit Tests">
<img src="https://github.com/Ultimaker/Cura/actions/workflows/conan-package.yml/badge.svg" /></a>
<a href="https://github.com/Ultimaker/Cura/issues" alt="Open Issues">
<img src="https://img.shields.io/github/issues/ultimaker/cura" /></a>
<a href="https://github.com/Ultimaker/Cura/issues?q=is%3Aissue+is%3Aclosed" alt="Closed Issues">
<img src="https://img.shields.io/github/issues-closed/ultimaker/cura?color=g" /></a>
<a href="https://github.com/Ultimaker/Cura/pulls" alt="Pull Requests">
<img src="https://img.shields.io/github/issues-pr/ultimaker/cura" /></a>
<a href="https://github.com/Ultimaker/Cura/graphs/contributors" alt="Contributors">
<img src="https://img.shields.io/github/contributors/ultimaker/cura" /></a>
<a href="https://github.com/Ultimaker/Cura" alt="Repo Size">
<img src="https://img.shields.io/github/repo-size/ultimaker/cura?style=flat" /></a>
<a href="https://github.com/Ultimaker/Cura/blob/master/LICENSE" alt="License">
<img src="https://img.shields.io/github/license/ultimaker/cura?style=flat" /></a>
</p>
Ultimaker Cura is a state-of-the-art slicer application to prepare your 3D models for printing with a 3D printer. With hundreds of settings <div align = center>
and hundreds of community-managed print profiles, Ultimaker Cura is sure to lead your next project to a success.
![Screenshot](cura-logo.PNG) [![Badge Issues]][Issues]
[![Badge PullRequests]][PullRequests]
[![Badge Closed]][Closed]
## Logging Issues [![Badge Size]][#]
[![Badge License]][License]
[![Badge Contributors]][Contributors]
For crashes and similar issues, please attach the following information: [![Badge Test]][Test]
[![Badge Conan]][Conan]
* (On Windows) The log as produced by dxdiag (start -> run -> dxdiag -> save output) <br>
* The Cura GUI log file, located at <br>
* `%APPDATA%\cura\<Cura version>\cura.log` (Windows), or usually `C:\Users\<your username>\AppData\Roaming\cura\<Cura version>\cura.log`
* `$HOME/Library/Application Support/cura/<Cura version>/cura.log` (OSX)
* `$HOME/.local/share/cura/<Cura version>/cura.log` (Ubuntu/Linux)
If the Cura user interface still starts, you can also reach this directory from the application menu in Help -> Show settings folder. ![Logo]
An alternative is to install the [ExtensiveSupportLogging plugin](https://marketplace.ultimaker.com/app/cura/plugins/UltimakerPackages/ExtensiveSupportLogging)
this creates a zip folder of the relevant log files. If you're experiencing performance issues, we might ask you to connect the CPU profiler
in this plugin and attach the collected data to your support ticket.
## Running from Source # Ultimaker Cura
Please check our [Wiki page](https://github.com/Ultimaker/Cura/wiki/Running-Cura-from-Source) for details about running Cura from source.
## Plugins *State-of-the-art slicer app to prepare* <br>
Please check our [Wiki page](https://github.com/Ultimaker/Cura/wiki/Plugin-Directory) for details about creating and using plugins. *your 3D models for your 3D printer.*
## Supported printers *With hundreds of settings & community-managed print profiles,* <br>
Please check our [Wiki page](https://github.com/Ultimaker/Cura/wiki/Adding-new-machine-profiles-to-Cura) for guidelines about adding support *Ultimaker Cura is sure to lead your next project to a success.*
for new machines.
## Configuring Cura <br>
Please check out [Wiki page](https://github.com/Ultimaker/Cura/wiki/Cura-Settings) about configuration options for developers. <br>
## Translating Cura [![Button Building]][Building]
Please check out [Wiki page](https://github.com/Ultimaker/Cura/wiki/Translating-Cura) about how to translate Cura into other languages. [![Button Plugins]][Plugins]
[![Button Machines]][Machines]
[![Button Report]][Report]
[![Button Settings]][Settings]
[![Button Localize]][Localize]
<br>
<br>
<picture>
<source media="(prefers-color-scheme: light)" srcset="./cura-logo.PNG">
<source media="(prefers-color-scheme: dark)" srcset="./cura-logo-dark.PNG">
<img alt="Shows cura open on the preview screen with a large benchy model in the center." src="./cura-logo.PNG">
</picture>
</div>
<br>
<!----------------------------------------------------------------------------->
[Contributors]: https://github.com/Ultimaker/Cura/graphs/contributors
[PullRequests]: https://github.com/Ultimaker/Cura/pulls
[Machines]: https://github.com/Ultimaker/Cura/wiki/Adding-new-machine-profiles-to-Cura
[Building]: https://github.com/Ultimaker/Cura/wiki/Running-Cura-from-Source
[Localize]: https://github.com/Ultimaker/Cura/wiki/Translating-Cura
[Settings]: https://github.com/Ultimaker/Cura/wiki/Cura-Settings
[Plugins]: https://github.com/Ultimaker/Cura/wiki/Plugin-Directory
[Closed]: https://github.com/Ultimaker/Cura/issues?q=is%3Aissue+is%3Aclosed
[Issues]: https://github.com/Ultimaker/Cura/issues
[Conan]: https://github.com/Ultimaker/Cura/actions/workflows/conan-package.yml
[Test]: https://github.com/Ultimaker/Cura/actions/workflows/unit-test.yml
[License]: LICENSE
[Report]: docs/Report.md
[Logo]: resources/images/cura-icon.png
[#]: #
<!---------------------------------[ Badges ]---------------------------------->
[Badge Contributors]: https://img.shields.io/github/contributors/ultimaker/cura?style=for-the-badge&logoColor=white&labelColor=db5e8a&color=ab4a6c&logo=GitHub
[Badge PullRequests]: https://img.shields.io/github/issues-pr/ultimaker/cura?style=for-the-badge&logoColor=white&labelColor=bb9f3e&color=937d31&logo=GitExtensions
[Badge License]: https://img.shields.io/badge/License-LGPL3-336887.svg?style=for-the-badge&labelColor=458cb5&logoColor=white&logo=GNU
[Badge Closed]: https://img.shields.io/github/issues-closed/ultimaker/cura?style=for-the-badge&logoColor=white&labelColor=629944&color=446a30&logo=AddThis
[Badge Issues]: https://img.shields.io/github/issues/ultimaker/cura?style=for-the-badge&logoColor=white&labelColor=c34360&color=933349&logo=AdBlock
[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
<!---------------------------------[ Buttons ]--------------------------------->
[Button Localize]: https://img.shields.io/badge/Help_Localize-e2467d?style=for-the-badge&logoColor=white&logo=GoogleTranslate
[Button Machines]: https://img.shields.io/badge/Adding_Machines-yellow?style=for-the-badge&logoColor=white&logo=CloudFoundry
[Button Settings]: https://img.shields.io/badge/Configuration-00979D?style=for-the-badge&logoColor=white&logo=CodeReview
[Button Building]: https://img.shields.io/badge/Building_Cura-blue?style=for-the-badge&logoColor=white&logo=GitBook
[Button Plugins]: https://img.shields.io/badge/Plugin_Usage-569A31?style=for-the-badge&logoColor=white&logo=ROS
[Button Report]: https://img.shields.io/badge/Report_Issues-C9284D?style=for-the-badge&logoColor=white&logo=Cliqz
## License
![License](https://img.shields.io/github/license/ultimaker/cura?style=flat)
Cura is released under terms of the LGPLv3 or higher. A copy of this license should be included with the software. Terms of the license can be found in the LICENSE file. Or at
http://www.gnu.org/licenses/lgpl.html
> But in general it boils down to:
> **You need to share the source of any Cura modifications**

View File

@ -1,92 +0,0 @@
# Copyright (c) 2022 Ultimaker B.V.
# CuraPluginInstall.cmake is released under the terms of the LGPLv3 or higher.
#
# This module detects all plugins that need to be installed and adds them using the CMake install() command.
# It detects all plugin folder in the path "plugins/*" where there's a "plugin.json" in it.
#
# Plugins can be configured to NOT BE INSTALLED via the variable "CURA_NO_INSTALL_PLUGINS" as a list of string in the
# form of "a;b;c" or "a,b,c". By default all plugins will be installed.
#
option(PRINT_PLUGIN_LIST "Should the list of plugins that are installed be printed?" ON)
# Options or configuration variables
set(CURA_NO_INSTALL_PLUGINS "" CACHE STRING "A list of plugins that should not be installed, separated with ';' or ','.")
file(GLOB_RECURSE _plugin_json_list ${CMAKE_SOURCE_DIR}/plugins/*/plugin.json)
list(LENGTH _plugin_json_list _plugin_json_list_len)
# Sort the lists alphabetically so we can handle cases like this:
# - plugins/my_plugin/plugin.json
# - plugins/my_plugin/my_module/plugin.json
# In this case, only "plugins/my_plugin" should be added via install().
set(_no_install_plugin_list ${CURA_NO_INSTALL_PLUGINS})
# Sanitize the string so the comparison will be case-insensitive.
string(STRIP "${_no_install_plugin_list}" _no_install_plugin_list)
string(TOLOWER "${_no_install_plugin_list}" _no_install_plugin_list)
# WORKAROUND counterpart of what's in cura-build.
string(REPLACE "," ";" _no_install_plugin_list "${_no_install_plugin_list}")
list(LENGTH _no_install_plugin_list _no_install_plugin_list_len)
if(_no_install_plugin_list_len GREATER 0)
list(SORT _no_install_plugin_list)
endif()
if(_plugin_json_list_len GREATER 0)
list(SORT _plugin_json_list)
endif()
# Check all plugin directories and add them via install() if needed.
set(_install_plugin_list "")
foreach(_plugin_json_path ${_plugin_json_list})
get_filename_component(_plugin_dir ${_plugin_json_path} DIRECTORY)
file(RELATIVE_PATH _rel_plugin_dir ${CMAKE_CURRENT_SOURCE_DIR} ${_plugin_dir})
get_filename_component(_plugin_dir_name ${_plugin_dir} NAME)
# Make plugin name comparison case-insensitive
string(TOLOWER "${_plugin_dir_name}" _plugin_dir_name_lowercase)
# Check if this plugin needs to be skipped for installation
set(_add_plugin ON) # Indicates if this plugin should be added to the build or not.
set(_is_no_install_plugin OFF) # If this plugin will not be added, this indicates if it's because the plugin is
# specified in the NO_INSTALL_PLUGINS list.
if(_no_install_plugin_list)
if("${_plugin_dir_name_lowercase}" IN_LIST _no_install_plugin_list)
set(_add_plugin OFF)
set(_is_no_install_plugin ON)
endif()
endif()
# Make sure this is not a subdirectory in a plugin that's already in the install list
if(_add_plugin)
foreach(_known_install_plugin_dir ${_install_plugin_list})
if(_plugin_dir MATCHES "${_known_install_plugin_dir}.+")
set(_add_plugin OFF)
break()
endif()
endforeach()
endif()
if(_add_plugin)
if(${PRINT_PLUGIN_LIST})
message(STATUS "[+] PLUGIN TO INSTALL: ${_rel_plugin_dir}")
endif()
get_filename_component(_rel_plugin_parent_dir ${_rel_plugin_dir} DIRECTORY)
install(DIRECTORY ${_rel_plugin_dir}
DESTINATION lib${LIB_SUFFIX}/cura/${_rel_plugin_parent_dir}
PATTERN "__pycache__" EXCLUDE
PATTERN "*.qmlc" EXCLUDE
)
list(APPEND _install_plugin_list ${_plugin_dir})
elseif(_is_no_install_plugin)
if(${PRINT_PLUGIN_LIST})
message(STATUS "[-] PLUGIN TO REMOVE : ${_rel_plugin_dir}")
endif()
execute_process(COMMAND ${Python_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/cmake/mod_bundled_packages_json.py
-d ${CMAKE_CURRENT_SOURCE_DIR}/resources/bundled_packages
${_plugin_dir_name}
RESULT_VARIABLE _mod_json_result)
endif()
endforeach()

View File

@ -1,77 +0,0 @@
# Copyright (c) 2022 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
include(CTest)
include(CMakeParseArguments)
add_custom_target(test-verbose COMMAND ${CMAKE_CTEST_COMMAND} --verbose)
function(cura_add_test)
set(_single_args NAME DIRECTORY PYTHONPATH)
cmake_parse_arguments("" "" "${_single_args}" "" ${ARGN})
if(NOT _NAME)
message(FATAL_ERROR "cura_add_test requires a test name argument")
endif()
if(NOT _DIRECTORY)
message(FATAL_ERROR "cura_add_test requires a directory to test")
endif()
if(NOT _PYTHONPATH)
set(_PYTHONPATH ${_DIRECTORY})
endif()
if(WIN32)
string(REPLACE "|" "\\;" _PYTHONPATH ${_PYTHONPATH})
set(_PYTHONPATH "${_PYTHONPATH}\\;$ENV{PYTHONPATH}")
else()
string(REPLACE "|" ":" _PYTHONPATH ${_PYTHONPATH})
set(_PYTHONPATH "${_PYTHONPATH}:$ENV{PYTHONPATH}")
endif()
get_test_property(${_NAME} ENVIRONMENT test_exists) #Find out if the test exists by getting a property from it that always exists (such as ENVIRONMENT because we set that ourselves).
if (NOT ${test_exists})
add_test(
NAME ${_NAME}
COMMAND ${Python_EXECUTABLE} -m pytest --junitxml=${CMAKE_BINARY_DIR}/junit-${_NAME}.xml ${_DIRECTORY}
)
set_tests_properties(${_NAME} PROPERTIES ENVIRONMENT LANG=C)
set_tests_properties(${_NAME} PROPERTIES ENVIRONMENT "PYTHONPATH=${_PYTHONPATH}")
else()
message(WARNING "Duplicate test ${_NAME}!")
endif()
endfunction()
#Add code style test.
add_test(
NAME "code-style"
COMMAND ${Python_EXECUTABLE} run_mypy.py
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
)
#Add test for import statements which are not compatible with all builds
add_test(
NAME "invalid-imports"
COMMAND ${Python_EXECUTABLE} scripts/check_invalid_imports.py
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
)
cura_add_test(NAME pytest-main DIRECTORY ${CMAKE_SOURCE_DIR}/tests PYTHONPATH "${CMAKE_SOURCE_DIR}|${URANIUM_DIR}")
file(GLOB_RECURSE _plugins plugins/*/__init__.py)
foreach(_plugin ${_plugins})
get_filename_component(_plugin_directory ${_plugin} DIRECTORY)
if(EXISTS ${_plugin_directory}/tests)
get_filename_component(_plugin_name ${_plugin_directory} NAME)
cura_add_test(NAME pytest-${_plugin_name} DIRECTORY ${_plugin_directory} PYTHONPATH "${_plugin_directory}|${CMAKE_SOURCE_DIR}|${URANIUM_DIR}")
endif()
endforeach()
#Add test for whether the shortcut alt-keys are unique in every translation.
add_test(
NAME "shortcut-keys"
COMMAND ${Python_EXECUTABLE} scripts/check_shortcut_keys.py
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
)

View File

@ -1,73 +0,0 @@
#!/usr/bin/env python3
#
# This script removes the given package entries in the bundled_packages JSON files. This is used by the PluginInstall
# CMake module.
#
import argparse
import collections
import json
import os
import sys
def find_json_files(work_dir: str) -> list:
"""Finds all JSON files in the given directory recursively and returns a list of those files in absolute paths.
:param work_dir: The directory to look for JSON files recursively.
:return: A list of JSON files in absolute paths that are found in the given directory.
"""
json_file_list = []
for root, dir_names, file_names in os.walk(work_dir):
for file_name in file_names:
abs_path = os.path.abspath(os.path.join(root, file_name))
json_file_list.append(abs_path)
return json_file_list
def remove_entries_from_json_file(file_path: str, entries: list) -> None:
"""Removes the given entries from the given JSON file. The file will modified in-place.
:param file_path: The JSON file to modify.
:param entries: A list of strings as entries to remove.
:return: None
"""
try:
with open(file_path, "r", encoding = "utf-8") as f:
package_dict = json.load(f, object_hook = collections.OrderedDict)
except Exception as e:
msg = "Failed to load '{file_path}' as a JSON file. This file will be ignored Exception: {e}"\
.format(file_path = file_path, e = e)
sys.stderr.write(msg + os.linesep)
return
for entry in entries:
if entry in package_dict:
del package_dict[entry]
print("[INFO] Remove entry [{entry}] from [{file_path}]".format(file_path = file_path, entry = entry))
try:
with open(file_path, "w", encoding = "utf-8", newline = "\n") as f:
json.dump(package_dict, f, indent = 4)
except Exception as e:
msg = "Failed to write '{file_path}' as a JSON file. Exception: {e}".format(file_path = file_path, e = e)
raise IOError(msg)
def main() -> None:
parser = argparse.ArgumentParser("mod_bundled_packages_json")
parser.add_argument("-d", "--dir", dest = "work_dir",
help = "The directory to look for bundled packages JSON files, recursively.")
parser.add_argument("entries", metavar = "ENTRIES", type = str, nargs = "+")
args = parser.parse_args()
json_file_list = find_json_files(args.work_dir)
for json_file_path in json_file_list:
remove_entries_from_json_file(json_file_path, args.entries)
if __name__ == "__main__":
main()

View File

@ -1,34 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Copyright 2016 Richard Hughes <richard@hughsie.com> -->
<component type="desktop">
<id>com.ultimaker.cura.desktop</id>
<metadata_license>CC0-1.0</metadata_license>
<project_license>LGPL-3.0 and CC-BY-SA-4.0</project_license>
<name>Cura</name>
<summary>The world's most advanced 3d printer software</summary>
<description>
<p>
Cura creates a seamless integration between hardware, software and
materials for the best 3D printing experience around.
Cura supports the 3MF, OBJ and STL file formats and is available on
Windows, Mac and Linux.
</p>
<ul>
<li>Novices can start printing right away</li>
<li>Experts are able to customize 300 settings to achieve the best results</li>
<li>Optimized profiles for Ultimaker materials</li>
<li>Supported by a global network of Ultimaker certified service partners</li>
<li>Print multiple objects at once with different settings for each object</li>
<li>Cura supports STL, 3MF and OBJ file formats</li>
<li>Open source and completely free</li>
</ul>
</description>
<screenshots>
<screenshot type="default">
<image>https://raw.githubusercontent.com/Ultimaker/Cura/main/cura-logo.PNG</image>
</screenshot>
</screenshots>
<url type="homepage">https://ultimaker.com/software/ultimaker-cura?utm_source=cura&amp;utm_medium=software&amp;utm_campaign=cura-update-linux</url>
<translation type="gettext">Cura</translation>
<content_rating type="oars-1.1" />
</component>

View File

@ -1,19 +0,0 @@
[Desktop Entry]
Name=Ultimaker Cura
Name[de]=Ultimaker Cura
Name[nl]=Ultimaker Cura
GenericName=3D Printing Software
GenericName[de]=3D-Druck-Software
GenericName[nl]=3D-printsoftware
Comment=Cura converts 3D models into paths for a 3D printer. It prepares your print for maximum accuracy, minimum printing time and good reliability with many extra features that make your print come out great.
Comment[de]=Cura wandelt 3D-Modelle in Pfade für einen 3D-Drucker um. Es bereitet Ihren Druck für maximale Genauigkeit, minimale Druckzeit und guter Zuverlässigkeit mit vielen zusätzlichen Funktionen vor, damit Ihr Druck großartig wird.
Comment[nl]=Cura converteert 3D-modellen naar paden voor een 3D printer. Het bereidt je print voor om zeer precies, snel en betrouwbaar te kunnen printen, met veel extra functionaliteit om je print er goed uit te laten komen.
Exec=@CMAKE_INSTALL_FULL_BINDIR@/cura %F
TryExec=@CMAKE_INSTALL_FULL_BINDIR@/cura
Icon=cura-icon
Terminal=false
Type=Application
MimeType=model/stl;application/vnd.ms-3mfdocument;application/prs.wavefront-obj;image/bmp;image/gif;image/jpeg;image/png;text/x-gcode;application/x-amf;application/x-ply;application/x-ctm;model/vnd.collada+xml;model/gltf-binary;model/gltf+json;model/vnd.collada+xml+zip;
Categories=Graphics;
Keywords=3D;Printing;Slicer;
StartupWMClass=cura.real

View File

@ -1,5 +1,5 @@
--- ---
# Usage: defaults to None # Usage: defaults to the first entry in this conandata.yml file
# If you're on a release branch create an entry for that **version** e.q.: `5.1.0` update the requirements (use pinned versions, not latest) # If you're on a release branch create an entry for that **version** e.q.: `5.1.0` update the requirements (use pinned versions, not latest)
# also create a beta entry for that **version** e.q.: `5.1.0-beta`, update the requirements (use the <dep_name>/(latest)@ultimaker/stable) # also create a beta entry for that **version** e.q.: `5.1.0-beta`, update the requirements (use the <dep_name>/(latest)@ultimaker/stable)
# #
@ -10,111 +10,6 @@
# requirements (use the <dep_name>/(latest)@ultimaker/testing) # requirements (use the <dep_name>/(latest)@ultimaker/testing)
# #
# Subject to change in the future! # Subject to change in the future!
"None":
requirements:
- "pyarcus/(latest)@ultimaker/testing"
- "curaengine/(latest)@ultimaker/testing"
- "pysavitar/(latest)@ultimaker/testing"
- "pynest2d/(latest)@ultimaker/testing"
- "uranium/(latest)@ultimaker/testing"
- "fdm_materials/(latest)@ultimaker/testing"
- "cura_binary_data/(latest)@ultimaker/testing"
- "cpython/3.10.4"
internal_requirements:
- "fdm_materials_private/(latest)@ultimaker/testing"
- "cura_private_data/(latest)@ultimaker/testing"
runinfo:
entrypoint: "cura_app.py"
pyinstaller:
datas:
cura_plugins:
package: "cura"
src: "plugins"
dst: "share/cura/plugins"
cura_resources:
package: "cura"
src: "resources"
dst: "share/cura/resources"
cura_private_data:
package: "cura_private_data"
src: "resources"
dst: "share/cura/resources"
internal: true
uranium_plugins:
package: "uranium"
src: "plugins"
dst: "share/uranium/plugins"
uranium_resources:
package: "uranium"
src: "resources"
dst: "share/uranium/resources"
uranium_um_qt_qml_um:
package: "uranium"
src: "site-packages/UM/Qt/qml/UM"
dst: "PyQt6/Qt6/qml/UM"
cura_binary_data:
package: "cura_binary_data"
src: "resources/cura/resources"
dst: "share/cura/resources"
uranium_binary_data:
package: "cura_binary_data"
src: "resources/uranium/resources"
dst: "share/uranium/resources"
windows_binary_data:
package: "cura_binary_data"
src: "windows"
dst: "share/windows"
fdm_materials:
package: "fdm_materials"
src: "materials"
dst: "share/cura/resources/materials"
fdm_materials_private:
package: "fdm_materials_private"
src: "resources/materials"
dst: "share/cura/resources/materials"
internal: true
tcl:
package: "tcl"
src: "lib/tcl8.6"
dst: "tcl"
tk:
package: "tk"
src: "lib/tk8.6"
dst: "tk"
binaries:
curaengine:
package: "curaengine"
src: "bin"
dst: "."
binary: "CuraEngine"
hiddenimports:
- "pySavitar"
- "pyArcus"
- "pynest2d"
- "PyQt6"
- "PyQt6.QtNetwork"
- "PyQt6.sip"
- "logging.handlers"
- "zeroconf"
- "fcntl"
- "stl"
- "serial"
collect_all:
- "cura"
- "UM"
- "serial"
- "Charon"
- "sqlite3"
- "trimesh"
- "win32ctypes"
- "PyQt6"
- "PyQt6.QtNetwork"
- "PyQt6.sip"
- "stl"
icon:
Windows: "./icons/Cura.ico"
Macos: "./icons/cura.icns"
Linux: "./icons/cura-128.png"
"5.2.0-alpha": "5.2.0-alpha":
requirements: requirements:
- "pyarcus/(latest)@ultimaker/testing" - "pyarcus/(latest)@ultimaker/testing"

View File

@ -3,13 +3,13 @@ from pathlib import Path
from jinja2 import Template from jinja2 import Template
from conans import tools
from conan import ConanFile from conan import ConanFile
from conan.tools import files from conan.tools.files import copy, rmdir, save
from conan.tools.env import VirtualRunEnv, Environment from conan.tools.env import VirtualRunEnv, Environment
from conan.tools.scm import Version
from conan.errors import ConanInvalidConfiguration from conan.errors import ConanInvalidConfiguration
required_conan_version = ">=1.48.0" required_conan_version = ">=1.50.0"
class CuraConan(ConanFile): class CuraConan(ConanFile):
@ -26,7 +26,7 @@ class CuraConan(ConanFile):
# FIXME: Remove specific branch once merged to main # FIXME: Remove specific branch once merged to main
# Extending the conanfile with the UMBaseConanfile https://github.com/Ultimaker/conan-ultimaker-index/tree/CURA-9177_Fix_CI_CD/recipes/umbase # Extending the conanfile with the UMBaseConanfile https://github.com/Ultimaker/conan-ultimaker-index/tree/CURA-9177_Fix_CI_CD/recipes/umbase
python_requires = "umbase/0.1.5@ultimaker/testing" python_requires = "umbase/[>=0.1.7]@ultimaker/stable"
python_requires_extend = "umbase.UMBaseConanfile" python_requires_extend = "umbase.UMBaseConanfile"
options = { options = {
@ -100,6 +100,10 @@ class CuraConan(ConanFile):
def _digital_factory_url(self): def _digital_factory_url(self):
return "https://digitalfactory-staging.ultimaker.com" if self._staging else "https://digitalfactory.ultimaker.com" return "https://digitalfactory-staging.ultimaker.com" if self._staging else "https://digitalfactory.ultimaker.com"
@property
def _cura_latest_url(self):
return "https://software.ultimaker.com/latest.json"
@property @property
def requirements_txts(self): def requirements_txts(self):
if self.options.devtools: if self.options.devtools:
@ -131,7 +135,7 @@ class CuraConan(ConanFile):
def _site_packages(self): def _site_packages(self):
if self.settings.os == "Windows": if self.settings.os == "Windows":
return self._base_dir.joinpath("Lib", "site-packages") return self._base_dir.joinpath("Lib", "site-packages")
py_version = tools.Version(self.deps_cpp_info["cpython"].version) py_version = Version(self.deps_cpp_info["cpython"].version)
return self._base_dir.joinpath("lib", f"python{py_version.major}.{py_version.minor}", "site-packages") return self._base_dir.joinpath("lib", f"python{py_version.major}.{py_version.minor}", "site-packages")
@property @property
@ -145,10 +149,13 @@ class CuraConan(ConanFile):
with open(Path(__file__).parent.joinpath("CuraVersion.py.jinja"), "r") as f: with open(Path(__file__).parent.joinpath("CuraVersion.py.jinja"), "r") as f:
cura_version_py = Template(f.read()) cura_version_py = Template(f.read())
cura_version = self.version cura_version = self.conf_info.get("user.cura:version", default = self.version, check_type = str)
if self.options.internal: if self.options.internal:
version = tools.Version(self.version) version = Version(cura_version)
cura_version = f"{version.major}.{version.minor}.{version.patch}-{version.prerelease.replace('+', '+internal_')}" if version.pre:
cura_version = f"{version.major}.{version.minor}.{version.patch}-{version.pre}+internal_{version.build}"
else:
cura_version = f"{version.major}.{version.minor}.{version.patch}+internal_{version.build}"
with open(Path(location, "CuraVersion.py"), "w") as f: with open(Path(location, "CuraVersion.py"), "w") as f:
f.write(cura_version_py.render( f.write(cura_version_py.render(
@ -161,7 +168,8 @@ class CuraConan(ConanFile):
cura_cloud_api_version = self.options.cloud_api_version, cura_cloud_api_version = self.options.cloud_api_version,
cura_cloud_account_api_root = self._cloud_account_api_root, cura_cloud_account_api_root = self._cloud_account_api_root,
cura_marketplace_root = self._marketplace_root, cura_marketplace_root = self._marketplace_root,
cura_digital_factory_url = self._digital_factory_url)) cura_digital_factory_url = self._digital_factory_url,
cura_latest_url = self._cura_latest_url))
def _generate_pyinstaller_spec(self, location, entrypoint_location, icon_path, entitlements_file): def _generate_pyinstaller_spec(self, location, entrypoint_location, icon_path, entitlements_file):
pyinstaller_metadata = self._um_data()["pyinstaller"] pyinstaller_metadata = self._um_data()["pyinstaller"]
@ -215,7 +223,8 @@ class CuraConan(ConanFile):
with open(Path(__file__).parent.joinpath("Ultimaker-Cura.spec.jinja"), "r") as f: with open(Path(__file__).parent.joinpath("Ultimaker-Cura.spec.jinja"), "r") as f:
pyinstaller = Template(f.read()) pyinstaller = Template(f.read())
cura_version = tools.Version(self.version) if self.version else tools.Version("0.0.0") version = self.conf_info.get("user.cura:version", default = self.version, check_type = str)
cura_version = Version(version)
with open(Path(location, "Ultimaker-Cura.spec"), "w") as f: with open(Path(location, "Ultimaker-Cura.spec"), "w") as f:
f.write(pyinstaller.render( f.write(pyinstaller.render(
@ -234,10 +243,14 @@ class CuraConan(ConanFile):
strip = False, # This should be possible on Linux and MacOS but, it can also cause issues on some distributions. Safest is to disable it for now strip = False, # This should be possible on Linux and MacOS but, it can also cause issues on some distributions. Safest is to disable it for now
target_arch = "'x86_64'" if self.settings.os == "Macos" else "None", # FIXME: Make this dependent on the settings.arch_target target_arch = "'x86_64'" if self.settings.os == "Macos" else "None", # FIXME: Make this dependent on the settings.arch_target
macos = self.settings.os == "Macos", macos = self.settings.os == "Macos",
version = f"'{self.version}'", version = f"'{version}'",
short_version = f"'{cura_version.major}.{cura_version.minor}.{cura_version.patch}'", short_version = f"'{cura_version.major}.{cura_version.minor}.{cura_version.patch}'",
)) ))
def set_version(self):
if self.version is None:
self.version = self._umdefault_version()
def configure(self): def configure(self):
self.options["pyarcus"].shared = True self.options["pyarcus"].shared = True
self.options["pysavitar"].shared = True self.options["pysavitar"].shared = True
@ -245,7 +258,8 @@ class CuraConan(ConanFile):
self.options["cpython"].shared = True self.options["cpython"].shared = True
def validate(self): def validate(self):
if self.version and tools.Version(self.version) <= tools.Version("4"): version = self.conf_info.get("user.cura:version", default = self.version, check_type = str)
if version and Version(version) <= Version("4"):
raise ConanInvalidConfiguration("Only versions 5+ are support") raise ConanInvalidConfiguration("Only versions 5+ are support")
def requirements(self): def requirements(self):
@ -288,7 +302,7 @@ class CuraConan(ConanFile):
self.copy("CuraEngine.exe", root_package = "curaengine", src = "@bindirs", dst = "", keep_path = False) 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) self.copy("CuraEngine", root_package = "curaengine", src = "@bindirs", dst = "", keep_path = False)
files.rmdir(self, "resources/materials") 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("*.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) self.copy("*.sig", root_package = "fdm_materials", src = "@resdirs", dst = "resources/materials", keep_path = False)
@ -372,7 +386,8 @@ class CuraConan(ConanFile):
self.copy("*.txt", src = self.cpp_info.resdirs[-1], dst = self._base_dir.joinpath("pip_requirements")) self.copy("*.txt", src = self.cpp_info.resdirs[-1], dst = self._base_dir.joinpath("pip_requirements"))
# Generate the GitHub Action version info Environment # Generate the GitHub Action version info Environment
cura_version = tools.Version(self.version) version = self.conf_info.get("user.cura:version", default = self.version, check_type = str)
cura_version = Version(version)
env_prefix = "Env:" if self.settings.os == "Windows" else "" env_prefix = "Env:" if self.settings.os == "Windows" else ""
activate_github_actions_version_env = Template(r"""echo "CURA_VERSION_MAJOR={{ cura_version_major }}" >> ${{ env_prefix }}GITHUB_ENV activate_github_actions_version_env = Template(r"""echo "CURA_VERSION_MAJOR={{ cura_version_major }}" >> ${{ env_prefix }}GITHUB_ENV
echo "CURA_VERSION_MINOR={{ cura_version_minor }}" >> ${{ env_prefix }}GITHUB_ENV echo "CURA_VERSION_MINOR={{ cura_version_minor }}" >> ${{ env_prefix }}GITHUB_ENV
@ -387,7 +402,7 @@ echo "CURA_VERSION_FULL={{ cura_version_full }}" >> ${{ env_prefix }}GITHUB_ENV
env_prefix = env_prefix) env_prefix = env_prefix)
ext = ".sh" if self.settings.os != "Windows" else ".ps1" ext = ".sh" if self.settings.os != "Windows" else ".ps1"
files.save(self, self._script_dir.joinpath(f"activate_github_actions_version_env{ext}"), activate_github_actions_version_env) save(self, self._script_dir.joinpath(f"activate_github_actions_version_env{ext}"), activate_github_actions_version_env)
self._generate_cura_version(Path(self._site_packages, "cura")) self._generate_cura_version(Path(self._site_packages, "cura"))

View File

@ -16,4 +16,6 @@ Making pull requests
-------------------- --------------------
If you want to propose a change to Cura's source code, please create a pull request in the appropriate repository (being [Cura](https://github.com/Ultimaker/Cura), [Uranium](https://github.com/Ultimaker/Uranium), [CuraEngine](https://github.com/Ultimaker/CuraEngine), [fdm_materials](https://github.com/Ultimaker/fdm_materials), [libArcus](https://github.com/Ultimaker/libArcus), [cura-build](https://github.com/Ultimaker/cura-build), [cura-build-environment](https://github.com/Ultimaker/cura-build-environment), [libSavitar](https://github.com/Ultimaker/libSavitar), [libCharon](https://github.com/Ultimaker/libCharon) or [cura-binary-data](https://github.com/Ultimaker/cura-binary-data)) and if your change requires changes on multiple of these repositories, please link them together so that we know to merge them together. If you want to propose a change to Cura's source code, please create a pull request in the appropriate repository (being [Cura](https://github.com/Ultimaker/Cura), [Uranium](https://github.com/Ultimaker/Uranium), [CuraEngine](https://github.com/Ultimaker/CuraEngine), [fdm_materials](https://github.com/Ultimaker/fdm_materials), [libArcus](https://github.com/Ultimaker/libArcus), [cura-build](https://github.com/Ultimaker/cura-build), [cura-build-environment](https://github.com/Ultimaker/cura-build-environment), [libSavitar](https://github.com/Ultimaker/libSavitar), [libCharon](https://github.com/Ultimaker/libCharon) or [cura-binary-data](https://github.com/Ultimaker/cura-binary-data)) and if your change requires changes on multiple of these repositories, please link them together so that we know to merge them together.
Some of these repositories will have automated tests running when you create a pull request, indicated by green check marks or red crosses in the Github web page. If you see a red cross, that means that a test has failed. If the test doesn't fail on the Master branch but does fail on your branch, that indicates that you've probably made a mistake and you need to do that. Click on the cross for more details, or run the test locally by running `cmake . && ctest --verbose`. The style guide for code contributions to Cura and other Ultimaker projects can be found [here](https://github.com/Ultimaker/Meta/blob/master/general/generic_code_conventions.md).
Some of these repositories will have automated tests running when you create a pull request, indicated by green check marks or red crosses in the Github web page. If you see a red cross, that means that a test has failed. If the test doesn't fail on the Master branch but does fail on your branch, that indicates that you've probably made a mistake and you need to do that. Click on the cross for more details, or run the test locally by running `cmake . && ctest --verbose`.

BIN
cura-logo-dark.PNG Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 520 KiB

After

Width:  |  Height:  |  Size: 1.0 MiB

View File

@ -9,12 +9,20 @@ DEFAULT_CURA_DISPLAY_NAME = "Ultimaker Cura"
DEFAULT_CURA_VERSION = "dev" DEFAULT_CURA_VERSION = "dev"
DEFAULT_CURA_BUILD_TYPE = "" DEFAULT_CURA_BUILD_TYPE = ""
DEFAULT_CURA_DEBUG_MODE = False DEFAULT_CURA_DEBUG_MODE = False
DEFAULT_CURA_LATEST_URL = "https://software.ultimaker.com/latest.json"
# Each release has a fixed SDK version coupled with it. It doesn't make sense to make it configurable because, for # Each release has a fixed SDK version coupled with it. It doesn't make sense to make it configurable because, for
# example Cura 3.2 with SDK version 6.1 will not work. So the SDK version is hard-coded here and left out of the # example Cura 3.2 with SDK version 6.1 will not work. So the SDK version is hard-coded here and left out of the
# CuraVersion.py.in template. # CuraVersion.py.in template.
CuraSDKVersion = "8.1.0" CuraSDKVersion = "8.1.0"
try:
from cura.CuraVersion import CuraLatestURL
if CuraLatestURL == "":
CuraLatestURL = DEFAULT_CURA_LATEST_URL
except ImportError:
CuraLatestURL = DEFAULT_CURA_LATEST_URL
try: try:
from cura.CuraVersion import CuraAppName # type: ignore from cura.CuraVersion import CuraAppName # type: ignore
if CuraAppName == "": if CuraAppName == "":

View File

@ -21,6 +21,7 @@ class ArrangeObjectsJob(Job):
self._min_offset = min_offset self._min_offset = min_offset
def run(self): def run(self):
found_solution_for_all = False
status_message = Message(i18n_catalog.i18nc("@info:status", "Finding new location for objects"), status_message = Message(i18n_catalog.i18nc("@info:status", "Finding new location for objects"),
lifetime = 0, lifetime = 0,
dismissable = False, dismissable = False,
@ -28,18 +29,19 @@ class ArrangeObjectsJob(Job):
title = i18n_catalog.i18nc("@info:title", "Finding Location")) title = i18n_catalog.i18nc("@info:title", "Finding Location"))
status_message.show() status_message.show()
found_solution_for_all = None
try: try:
found_solution_for_all = arrange(self._nodes, Application.getInstance().getBuildVolume(), self._fixed_nodes) found_solution_for_all = arrange(self._nodes, Application.getInstance().getBuildVolume(), self._fixed_nodes)
except: # If the thread crashes, the message should still close except: # If the thread crashes, the message should still close
Logger.logException("e", "Unable to arrange the objects on the buildplate. The arrange algorithm has crashed.") Logger.logException("e", "Unable to arrange the objects on the buildplate. The arrange algorithm has crashed.")
status_message.hide() status_message.hide()
if found_solution_for_all is not None and not found_solution_for_all:
if not found_solution_for_all:
no_full_solution_message = Message( no_full_solution_message = Message(
i18n_catalog.i18nc("@info:status", i18n_catalog.i18nc("@info:status",
"Unable to find a location within the build volume for all objects"), "Unable to find a location within the build volume for all objects"),
title = i18n_catalog.i18nc("@info:title", "Can't Find Location"), title = i18n_catalog.i18nc("@info:title", "Can't Find Location"),
message_type = Message.MessageType.ERROR) message_type = Message.MessageType.ERROR)
no_full_solution_message.show() no_full_solution_message.show()
self.finished.emit(self) self.finished.emit(self)

View File

@ -115,6 +115,8 @@ from . import CuraActions
from . import PlatformPhysics from . import PlatformPhysics
from . import PrintJobPreviewImageProvider from . import PrintJobPreviewImageProvider
from .AutoSave import AutoSave from .AutoSave import AutoSave
from .Machines.Models.CompatibleMachineModel import CompatibleMachineModel
from .Machines.Models.MachineListModel import MachineListModel
from .Machines.Models.ActiveIntentQualitiesModel import ActiveIntentQualitiesModel from .Machines.Models.ActiveIntentQualitiesModel import ActiveIntentQualitiesModel
from .Machines.Models.IntentSelectionModel import IntentSelectionModel from .Machines.Models.IntentSelectionModel import IntentSelectionModel
from .SingleInstance import SingleInstance from .SingleInstance import SingleInstance
@ -152,6 +154,7 @@ class CuraApplication(QtApplication):
super().__init__(name = ApplicationMetadata.CuraAppName, super().__init__(name = ApplicationMetadata.CuraAppName,
app_display_name = ApplicationMetadata.CuraAppDisplayName, app_display_name = ApplicationMetadata.CuraAppDisplayName,
version = ApplicationMetadata.CuraVersion if not ApplicationMetadata.IsAlternateVersion else ApplicationMetadata.CuraBuildType, version = ApplicationMetadata.CuraVersion if not ApplicationMetadata.IsAlternateVersion else ApplicationMetadata.CuraBuildType,
latest_url = ApplicationMetadata.CuraLatestURL,
api_version = ApplicationMetadata.CuraSDKVersion, api_version = ApplicationMetadata.CuraSDKVersion,
build_type = ApplicationMetadata.CuraBuildType, build_type = ApplicationMetadata.CuraBuildType,
is_debug_mode = ApplicationMetadata.CuraDebugMode, is_debug_mode = ApplicationMetadata.CuraDebugMode,
@ -1188,6 +1191,8 @@ class CuraApplication(QtApplication):
qmlRegisterType(InstanceContainer, "Cura", 1, 0, "InstanceContainer") qmlRegisterType(InstanceContainer, "Cura", 1, 0, "InstanceContainer")
qmlRegisterType(ExtrudersModel, "Cura", 1, 0, "ExtrudersModel") qmlRegisterType(ExtrudersModel, "Cura", 1, 0, "ExtrudersModel")
qmlRegisterType(GlobalStacksModel, "Cura", 1, 0, "GlobalStacksModel") qmlRegisterType(GlobalStacksModel, "Cura", 1, 0, "GlobalStacksModel")
qmlRegisterType(MachineListModel, "Cura", 1, 0, "MachineListModel")
qmlRegisterType(CompatibleMachineModel, "Cura", 1, 0, "CompatibleMachineModel")
self.processEvents() self.processEvents()
qmlRegisterType(FavoriteMaterialsModel, "Cura", 1, 0, "FavoriteMaterialsModel") qmlRegisterType(FavoriteMaterialsModel, "Cura", 1, 0, "FavoriteMaterialsModel")
@ -1442,7 +1447,7 @@ class CuraApplication(QtApplication):
bounding_box = node.getBoundingBox() bounding_box = node.getBoundingBox()
if bounding_box is None or bounding_box.width < self._volume.getBoundingBox().width or bounding_box.depth < self._volume.getBoundingBox().depth: if bounding_box is None or bounding_box.width < self._volume.getBoundingBox().width or bounding_box.depth < self._volume.getBoundingBox().depth:
# Arrange only the unlocked nodes and keep the locked ones in place # Arrange only the unlocked nodes and keep the locked ones in place
if UM.Util.parseBool(node.getSetting(SceneNodeSettings.LockPosition)): if node.getSetting(SceneNodeSettings.LockPosition):
locked_nodes.append(node) locked_nodes.append(node)
else: else:
nodes_to_arrange.append(node) nodes_to_arrange.append(node)

View File

@ -24,9 +24,12 @@ class LayerPolygon:
PrimeTowerType = 11 PrimeTowerType = 11
__number_of_types = 12 __number_of_types = 12
__jump_map = numpy.logical_or(numpy.logical_or(numpy.arange(__number_of_types) == NoneType, numpy.arange(__number_of_types) == MoveCombingType), numpy.arange(__number_of_types) == MoveRetractionType) __jump_map = numpy.logical_or(numpy.logical_or(numpy.arange(__number_of_types) == NoneType,
numpy.arange(__number_of_types) == MoveCombingType),
numpy.arange(__number_of_types) == MoveRetractionType)
def __init__(self, extruder: int, line_types: numpy.ndarray, data: numpy.ndarray, line_widths: numpy.ndarray, line_thicknesses: numpy.ndarray, line_feedrates: numpy.ndarray) -> None: def __init__(self, extruder: int, line_types: numpy.ndarray, data: numpy.ndarray,
line_widths: numpy.ndarray, line_thicknesses: numpy.ndarray, line_feedrates: numpy.ndarray) -> None:
"""LayerPolygon, used in ProcessSlicedLayersJob """LayerPolygon, used in ProcessSlicedLayersJob
:param extruder: The position of the extruder :param extruder: The position of the extruder
@ -39,10 +42,12 @@ class LayerPolygon:
self._extruder = extruder self._extruder = extruder
self._types = line_types self._types = line_types
for i in range(len(self._types)): unknown_types = numpy.where(self._types >= self.__number_of_types, self._types, None)
if self._types[i] >= self.__number_of_types: # Got faulty line data from the engine. if unknown_types.any():
Logger.log("w", "Found an unknown line type: %s", i) # Got faulty line data from the engine.
self._types[i] = self.NoneType for idx in unknown_types:
Logger.warning(f"Found an unknown line type at: {idx}")
self._types[idx] = self.NoneType
self._data = data self._data = data
self._line_widths = line_widths self._line_widths = line_widths
self._line_thicknesses = line_thicknesses self._line_thicknesses = line_thicknesses
@ -58,14 +63,16 @@ class LayerPolygon:
self._mesh_line_count = len(self._types) - self._jump_count self._mesh_line_count = len(self._types) - self._jump_count
self._vertex_count = self._mesh_line_count + numpy.sum(self._types[1:] == self._types[:-1]) self._vertex_count = self._mesh_line_count + numpy.sum(self._types[1:] == self._types[:-1])
# Buffering the colors shouldn't be necessary as it is not # Buffering the colors shouldn't be necessary as it is not
# re-used and can save a lot of memory usage. # re-used and can save a lot of memory usage.
self._color_map = LayerPolygon.getColorMap() self._color_map = LayerPolygon.getColorMap()
self._colors = self._color_map[self._types] # type: numpy.ndarray self._colors = self._color_map[self._types] # type: numpy.ndarray
# When type is used as index returns true if type == LayerPolygon.InfillType or type == LayerPolygon.SkinType or type == LayerPolygon.SupportInfillType # When type is used as index returns true if type == LayerPolygon.InfillType
# or type == LayerPolygon.SkinType
# or type == LayerPolygon.SupportInfillType
# Should be generated in better way, not hardcoded. # Should be generated in better way, not hardcoded.
self._is_infill_or_skin_type_map = numpy.array([0, 0, 0, 1, 0, 0, 1, 1, 0, 0, 1, 0], dtype = bool) self._is_infill_or_skin_type_map = numpy.array([0, 0, 0, 1, 0, 0, 1, 1, 0, 0, 1, 0], dtype=bool)
self._build_cache_line_mesh_mask = None # type: Optional[numpy.ndarray] self._build_cache_line_mesh_mask = None # type: Optional[numpy.ndarray]
self._build_cache_needed_points = None # type: Optional[numpy.ndarray] self._build_cache_needed_points = None # type: Optional[numpy.ndarray]
@ -80,12 +87,14 @@ class LayerPolygon:
# Only if the type of line segment changes do we need to add an extra vertex to change colors # Only if the type of line segment changes do we need to add an extra vertex to change colors
self._build_cache_needed_points[1:, 0][:, numpy.newaxis] = self._types[1:] != self._types[:-1] self._build_cache_needed_points[1:, 0][:, numpy.newaxis] = self._types[1:] != self._types[:-1]
# Mark points as unneeded if they are of types we don't want in the line mesh according to the calculated mask # Mark points as unneeded if they are of types we don't want in the line mesh according to the calculated mask
numpy.logical_and(self._build_cache_needed_points, self._build_cache_line_mesh_mask, self._build_cache_needed_points ) numpy.logical_and(self._build_cache_needed_points, self._build_cache_line_mesh_mask, self._build_cache_needed_points)
self._vertex_begin = 0 self._vertex_begin = 0
self._vertex_end = cast(int, numpy.sum(self._build_cache_needed_points)) self._vertex_end = cast(int, numpy.sum(self._build_cache_needed_points))
def build(self, vertex_offset: int, index_offset: int, vertices: numpy.ndarray, colors: numpy.ndarray, line_dimensions: numpy.ndarray, feedrates: numpy.ndarray, extruders: numpy.ndarray, line_types: numpy.ndarray, indices: numpy.ndarray) -> None: def build(self, vertex_offset: int, index_offset: int, vertices: numpy.ndarray,
colors: numpy.ndarray, line_dimensions: numpy.ndarray, feedrates: numpy.ndarray,
extruders: numpy.ndarray, line_types: numpy.ndarray, indices: numpy.ndarray) -> None:
"""Set all the arrays provided by the function caller, representing the LayerPolygon """Set all the arrays provided by the function caller, representing the LayerPolygon
The arrays are either by vertex or by indices. The arrays are either by vertex or by indices.
@ -111,19 +120,20 @@ class LayerPolygon:
line_mesh_mask = self._build_cache_line_mesh_mask line_mesh_mask = self._build_cache_line_mesh_mask
needed_points_list = self._build_cache_needed_points needed_points_list = self._build_cache_needed_points
# Index to the points we need to represent the line mesh. This is constructed by generating simple # Index to the points we need to represent the line mesh.
# start and end points for each line. For line segment n these are points n and n+1. Row n reads [n n+1] # This is constructed by generating simple start and end points for each line.
# Then then the indices for the points we don't need are thrown away based on the pre-calculated list. # For line segment n, these are points n and n+1. Row n reads [n n+1]
index_list = ( numpy.arange(len(self._types)).reshape((-1, 1)) + numpy.array([[0, 1]]) ).reshape((-1, 1))[needed_points_list.reshape((-1, 1))] # Then the indices for the points we don't need are thrown away based on the pre-calculated list.
index_list = (numpy.arange(len(self._types)).reshape((-1, 1)) + numpy.array([[0, 1]])).reshape((-1, 1))[needed_points_list.reshape((-1, 1))]
# The relative values of begin and end indices have already been set in buildCache, so we only need to offset them to the parents offset. # The relative values of begin and end indices have already been set in buildCache, so we only need to offset them to the parents offset.
self._vertex_begin += vertex_offset self._vertex_begin += vertex_offset
self._vertex_end += vertex_offset self._vertex_end += vertex_offset
# Points are picked based on the index list to get the vertices needed. # Points are picked based on the index list to get the vertices needed.
vertices[self._vertex_begin:self._vertex_end, :] = self._data[index_list, :] vertices[self._vertex_begin:self._vertex_end, :] = self._data[index_list, :]
# Create an array with colors for each vertex and remove the color data for the points that has been thrown away. # Create an array with colors for each vertex and remove the color data for the points that has been thrown away.
colors[self._vertex_begin:self._vertex_end, :] = numpy.tile(self._colors, (1, 2)).reshape((-1, 4))[needed_points_list.ravel()] colors[self._vertex_begin:self._vertex_end, :] = numpy.tile(self._colors, (1, 2)).reshape((-1, 4))[needed_points_list.ravel()]
# Create an array with line widths and thicknesses for each vertex. # Create an array with line widths and thicknesses for each vertex.
@ -138,14 +148,15 @@ class LayerPolygon:
# Convert type per vertex to type per line # Convert type per vertex to type per line
line_types[self._vertex_begin:self._vertex_end] = numpy.tile(self._types, (1, 2)).reshape((-1, 1))[needed_points_list.ravel()][:, 0] line_types[self._vertex_begin:self._vertex_end] = numpy.tile(self._types, (1, 2)).reshape((-1, 1))[needed_points_list.ravel()][:, 0]
# The relative values of begin and end indices have already been set in buildCache, so we only need to offset them to the parents offset. # The relative values of begin and end indices have already been set in buildCache,
# so we only need to offset them to the parents offset.
self._index_begin += index_offset self._index_begin += index_offset
self._index_end += index_offset self._index_end += index_offset
indices[self._index_begin:self._index_end, :] = numpy.arange(self._index_end-self._index_begin, dtype = numpy.int32).reshape((-1, 1)) indices[self._index_begin:self._index_end, :] = numpy.arange(self._index_end-self._index_begin, dtype=numpy.int32).reshape((-1, 1))
# When the line type changes the index needs to be increased by 2. # When the line type changes the index needs to be increased by 2.
indices[self._index_begin:self._index_end, :] += numpy.cumsum(needed_points_list[line_mesh_mask.ravel(), 0], dtype = numpy.int32).reshape((-1, 1)) indices[self._index_begin:self._index_end, :] += numpy.cumsum(needed_points_list[line_mesh_mask.ravel(), 0], dtype = numpy.int32).reshape((-1, 1))
# Each line segment goes from it's starting point p to p+1, offset by the vertex index. # Each line segment goes from it's starting point p to p+1, offset by the vertex index.
# The -1 is to compensate for the necessarily True value of needed_points_list[0,0] which causes an unwanted +1 in cumsum above. # The -1 is to compensate for the necessarily True value of needed_points_list[0,0] which causes an unwanted +1 in cumsum above.
indices[self._index_begin:self._index_end, :] += numpy.array([self._vertex_begin - 1, self._vertex_begin]) indices[self._index_begin:self._index_end, :] += numpy.array([self._vertex_begin - 1, self._vertex_begin])
@ -214,13 +225,12 @@ class LayerPolygon:
""" """
normals = numpy.copy(self._data) normals = numpy.copy(self._data)
normals[:, 1] = 0.0 # We are only interested in 2D normals normals[:, 1] = 0.0 # We are only interested in 2D normals
# Calculate the edges between points. # Calculate the edges between points.
# The call to numpy.roll shifts the entire array by one so that # The call to numpy.roll shifts the entire array by one
# we end up subtracting each next point from the current, wrapping # so that we end up subtracting each next point from the current, wrapping around.
# around. This gives us the edges from the next point to the current # This gives us the edges from the next point to the current point.
# point.
normals = numpy.diff(normals, 1, 0) normals = numpy.diff(normals, 1, 0)
# Calculate the length of each edge using standard Pythagoras # Calculate the length of each edge using standard Pythagoras
@ -245,17 +255,17 @@ class LayerPolygon:
if cls.__color_map is None: if cls.__color_map is None:
theme = cast(Theme, QtApplication.getInstance().getTheme()) theme = cast(Theme, QtApplication.getInstance().getTheme())
cls.__color_map = numpy.array([ cls.__color_map = numpy.array([
theme.getColor("layerview_none").getRgbF(), # NoneType theme.getColor("layerview_none").getRgbF(), # NoneType
theme.getColor("layerview_inset_0").getRgbF(), # Inset0Type theme.getColor("layerview_inset_0").getRgbF(), # Inset0Type
theme.getColor("layerview_inset_x").getRgbF(), # InsetXType theme.getColor("layerview_inset_x").getRgbF(), # InsetXType
theme.getColor("layerview_skin").getRgbF(), # SkinType theme.getColor("layerview_skin").getRgbF(), # SkinType
theme.getColor("layerview_support").getRgbF(), # SupportType theme.getColor("layerview_support").getRgbF(), # SupportType
theme.getColor("layerview_skirt").getRgbF(), # SkirtType theme.getColor("layerview_skirt").getRgbF(), # SkirtType
theme.getColor("layerview_infill").getRgbF(), # InfillType theme.getColor("layerview_infill").getRgbF(), # InfillType
theme.getColor("layerview_support_infill").getRgbF(), # SupportInfillType theme.getColor("layerview_support_infill").getRgbF(), # SupportInfillType
theme.getColor("layerview_move_combing").getRgbF(), # MoveCombingType theme.getColor("layerview_move_combing").getRgbF(), # MoveCombingType
theme.getColor("layerview_move_retraction").getRgbF(), # MoveRetractionType theme.getColor("layerview_move_retraction").getRgbF(), # MoveRetractionType
theme.getColor("layerview_support_interface").getRgbF(), # SupportInterfaceType theme.getColor("layerview_support_interface").getRgbF(), # SupportInterfaceType
theme.getColor("layerview_prime_tower").getRgbF() # PrimeTowerType theme.getColor("layerview_prime_tower").getRgbF() # PrimeTowerType
]) ])

View File

@ -33,8 +33,11 @@ class MachineAction(QObject, PluginObject):
self._qml_url = "" self._qml_url = ""
self._view = None self._view = None
self._finished = False self._finished = False
self._open_as_dialog = True
self._visible = True
labelChanged = pyqtSignal() labelChanged = pyqtSignal()
visibilityChanged = pyqtSignal()
onFinished = pyqtSignal() onFinished = pyqtSignal()
def getKey(self) -> str: def getKey(self) -> str:
@ -79,6 +82,15 @@ class MachineAction(QObject, PluginObject):
pass pass
@pyqtSlot()
def execute(self) -> None:
self._execute()
def _execute(self) -> None:
"""Protected implementation of execute."""
pass
@pyqtSlot() @pyqtSlot()
def setFinished(self) -> None: def setFinished(self) -> None:
self._finished = True self._finished = True
@ -94,7 +106,7 @@ class MachineAction(QObject, PluginObject):
plugin_path = PluginRegistry.getInstance().getPluginPath(self.getPluginId()) plugin_path = PluginRegistry.getInstance().getPluginPath(self.getPluginId())
if plugin_path is None: if plugin_path is None:
Logger.log("e", "Cannot create QML view: cannot find plugin path for plugin [%s]", self.getPluginId()) Logger.error(f"Cannot create QML view: cannot find plugin path for plugin {self.getPluginId()}")
return None return None
path = os.path.join(plugin_path, self._qml_url) path = os.path.join(plugin_path, self._qml_url)
@ -106,7 +118,7 @@ class MachineAction(QObject, PluginObject):
def qmlPath(self) -> "QUrl": def qmlPath(self) -> "QUrl":
plugin_path = PluginRegistry.getInstance().getPluginPath(self.getPluginId()) plugin_path = PluginRegistry.getInstance().getPluginPath(self.getPluginId())
if plugin_path is None: if plugin_path is None:
Logger.log("e", "Cannot create QML view: cannot find plugin path for plugin [%s]", self.getPluginId()) Logger.error(f"Cannot create QML view: cannot find plugin path for plugin {self.getPluginId()}")
return QUrl("") return QUrl("")
path = os.path.join(plugin_path, self._qml_url) path = os.path.join(plugin_path, self._qml_url)
return QUrl.fromLocalFile(path) return QUrl.fromLocalFile(path)
@ -114,3 +126,30 @@ class MachineAction(QObject, PluginObject):
@pyqtSlot(result = QObject) @pyqtSlot(result = QObject)
def getDisplayItem(self) -> Optional["QObject"]: def getDisplayItem(self) -> Optional["QObject"]:
return self._createViewFromQML() return self._createViewFromQML()
@pyqtProperty(bool, constant=True)
def shouldOpenAsDialog(self) -> bool:
"""Whether this action will show a dialog.
If not, the action will directly run the function inside execute().
:return: Defaults to true to be in line with the old behaviour.
"""
return self._open_as_dialog
@pyqtSlot()
def setVisible(self, visible: bool) -> None:
if self._visible != visible:
self._visible = visible
self.visibilityChanged.emit()
@pyqtProperty(bool, notify = visibilityChanged)
def visible(self) -> bool:
"""Whether this action button will be visible.
Example: Show only when isLoggedIn
:return: Defaults to true to be in line with the old behaviour.
"""
return self._visible

View File

@ -43,13 +43,9 @@ class MachineErrorChecker(QObject):
self._application = cura.CuraApplication.CuraApplication.getInstance() self._application = cura.CuraApplication.CuraApplication.getInstance()
self._machine_manager = self._application.getMachineManager() self._machine_manager = self._application.getMachineManager()
self._start_time = 0. # measure checking time self._check_start_time = time.time()
# This timer delays the starting of error check so we can react less frequently if the user is frequently self._setCheckTimer()
# changing settings.
self._error_check_timer = QTimer(self)
self._error_check_timer.setInterval(100)
self._error_check_timer.setSingleShot(True)
self._keys_to_check = set() # type: Set[str] self._keys_to_check = set() # type: Set[str]
@ -66,6 +62,18 @@ class MachineErrorChecker(QObject):
self._onMachineChanged() self._onMachineChanged()
def _setCheckTimer(self) -> None:
"""A QTimer to regulate error check frequency
This timer delays the starting of error check
so we can react less frequently if the user is frequently
changing settings.
"""
self._error_check_timer = QTimer(self)
self._error_check_timer.setInterval(100)
self._error_check_timer.setSingleShot(True)
def _onMachineChanged(self) -> None: def _onMachineChanged(self) -> None:
if self._global_stack: if self._global_stack:
self._global_stack.propertyChanged.disconnect(self.startErrorCheckPropertyChanged) self._global_stack.propertyChanged.disconnect(self.startErrorCheckPropertyChanged)
@ -152,7 +160,7 @@ class MachineErrorChecker(QObject):
self._stacks_and_keys_to_check.append((stack, key)) self._stacks_and_keys_to_check.append((stack, key))
self._application.callLater(self._checkStack) self._application.callLater(self._checkStack)
self._start_time = time.time() self._check_start_time = time.time()
Logger.log("d", "New error check scheduled.") Logger.log("d", "New error check scheduled.")
def _checkStack(self) -> None: def _checkStack(self) -> None:
@ -204,12 +212,10 @@ class MachineErrorChecker(QObject):
self._has_errors = result self._has_errors = result
self.hasErrorUpdated.emit() self.hasErrorUpdated.emit()
self._machine_manager.stacksValidationChanged.emit() self._machine_manager.stacksValidationChanged.emit()
if keys_to_recheck is None: self._keys_to_check = keys_to_recheck if keys_to_recheck else set()
self._keys_to_check = set()
else:
self._keys_to_check = keys_to_recheck
self._need_to_check = False self._need_to_check = False
self._check_in_progress = False self._check_in_progress = False
self.needToWaitForResultChanged.emit() self.needToWaitForResultChanged.emit()
self.errorCheckFinished.emit() self.errorCheckFinished.emit()
Logger.log("i", "Error check finished, result = %s, time = %0.1fs", result, time.time() - self._start_time) execution_time = time.time() - self._check_start_time
Logger.info(f"Error check finished, result = {result}, time = {execution_time:.2f}s")

View File

@ -0,0 +1,80 @@
# Copyright (c) 2022 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from typing import Optional
from PyQt6.QtCore import Qt, QObject, pyqtSlot, pyqtProperty, pyqtSignal
from UM.Logger import Logger
from UM.Qt.ListModel import ListModel
from UM.i18n import i18nCatalog
class CompatibleMachineModel(ListModel):
NameRole = Qt.ItemDataRole.UserRole + 1
UniqueIdRole = Qt.ItemDataRole.UserRole + 2
ExtrudersRole = Qt.ItemDataRole.UserRole + 3
def __init__(self, parent: Optional[QObject] = None) -> None:
super().__init__(parent)
self._catalog = i18nCatalog("cura")
self.addRoleName(self.NameRole, "name")
self.addRoleName(self.UniqueIdRole, "unique_id")
self.addRoleName(self.ExtrudersRole, "extruders")
self._update()
from cura.CuraApplication import CuraApplication
machine_manager = CuraApplication.getInstance().getMachineManager()
machine_manager.globalContainerChanged.connect(self._update)
machine_manager.outputDevicesChanged.connect(self._update)
@pyqtSlot()
def forceUpdate(self):
self._update()
def _update(self) -> None:
self.clear()
from cura.CuraApplication import CuraApplication
machine_manager = CuraApplication.getInstance().getMachineManager()
# Loop over the output-devices, not the stacks; need all applicable configurations, not just the current loaded one.
for output_device in machine_manager.printerOutputDevices:
for printer in output_device.printers:
extruder_configs = dict()
# initialize & add current active material:
for extruder in printer.extruders:
materials = [{
"brand": extruder.activeMaterial.brand,
"name": extruder.activeMaterial.name,
"hexcolor": extruder.activeMaterial.color,
}]
extruder_configs[extruder.getPosition()] = {
"position": extruder.getPosition(),
"core": extruder.hotendID,
"materials": materials
}
# add currently inactive, but possible materials:
for configuration in printer.availableConfigurations:
for extruder in configuration.extruderConfigurations:
if not extruder.position in extruder_configs:
Logger.log("w", f"No active extruder for position {extruder.position}.")
continue
extruder_configs[extruder.position]["materials"].append({
"brand": extruder.material.brand,
"name": extruder.material.name,
"hexcolor": extruder.material.color
})
if any([len(extruder["materials"]) > 0 for extruder in extruder_configs.values()]):
self.appendItem({
"name": printer.name,
"unique_id": printer.name, # <- Can assume the cloud doesn't have duplicate names?
"extruders": list(extruder_configs.values())
})

View File

@ -44,6 +44,7 @@ class GlobalStacksModel(ListModel):
self._filter_connection_type = None # type: Optional[ConnectionType] self._filter_connection_type = None # type: Optional[ConnectionType]
self._filter_online_only = False self._filter_online_only = False
self._filter_capabilities: List[str] = [] # Required capabilities that all listed printers must have. self._filter_capabilities: List[str] = [] # Required capabilities that all listed printers must have.
self._filter_abstract_machines: Optional[bool] = None
# Listen to changes # Listen to changes
CuraContainerRegistry.getInstance().containerAdded.connect(self._onContainerChanged) CuraContainerRegistry.getInstance().containerAdded.connect(self._onContainerChanged)
@ -54,6 +55,7 @@ class GlobalStacksModel(ListModel):
filterConnectionTypeChanged = pyqtSignal() filterConnectionTypeChanged = pyqtSignal()
filterCapabilitiesChanged = pyqtSignal() filterCapabilitiesChanged = pyqtSignal()
filterOnlineOnlyChanged = pyqtSignal() filterOnlineOnlyChanged = pyqtSignal()
filterAbstractMachinesChanged = pyqtSignal()
def setFilterConnectionType(self, new_filter: Optional[ConnectionType]) -> None: def setFilterConnectionType(self, new_filter: Optional[ConnectionType]) -> None:
if self._filter_connection_type != new_filter: if self._filter_connection_type != new_filter:
@ -98,6 +100,22 @@ class GlobalStacksModel(ListModel):
""" """
return self._filter_capabilities return self._filter_capabilities
def setFilterAbstractMachines(self, new_filter: Optional[bool]) -> None:
if self._filter_abstract_machines != new_filter:
self._filter_abstract_machines = new_filter
self.filterAbstractMachinesChanged.emit()
@pyqtProperty(bool, fset = setFilterAbstractMachines, notify = filterAbstractMachinesChanged)
def filterAbstractMachines(self) -> Optional[bool]:
"""
Weather we include abstract printers, non-abstract printers or both
if this is set to None both abstract and non-abstract printers will be included in the list
set to True will only include abstract printers
set to False will only inclde non-abstract printers
"""
return self._filter_abstract_machines
def _onContainerChanged(self, container) -> None: def _onContainerChanged(self, container) -> None:
"""Handler for container added/removed events from registry""" """Handler for container added/removed events from registry"""
@ -130,6 +148,10 @@ class GlobalStacksModel(ListModel):
if self._filter_online_only and not is_online: if self._filter_online_only and not is_online:
continue continue
is_abstract_machine = parseBool(container_stack.getMetaDataEntry("is_abstract_machine", False))
if self._filter_abstract_machines is not None and self._filter_abstract_machines is not is_abstract_machine:
continue
capabilities = set(container_stack.getMetaDataEntry(META_CAPABILITIES, "").split(",")) capabilities = set(container_stack.getMetaDataEntry(META_CAPABILITIES, "").split(","))
if set(self._filter_capabilities) - capabilities: # Not all required capabilities are met. if set(self._filter_capabilities) - capabilities: # Not all required capabilities are met.
continue continue

View File

@ -0,0 +1,142 @@
# Copyright (c) 2022 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
# The MachineListModel is used to display the connected printers in the interface. Both the abstract machines and all
# online cloud connected printers are represented within this ListModel. Additional information such as the number of
# connected printers for each printer type is gathered.
from typing import Optional
from PyQt6.QtCore import Qt, QTimer, QObject, pyqtSlot, pyqtProperty, pyqtSignal
from UM.Qt.ListModel import ListModel
from UM.Settings.ContainerStack import ContainerStack
from UM.Settings.Interfaces import ContainerInterface
from UM.i18n import i18nCatalog
from UM.Util import parseBool
from cura.PrinterOutput.PrinterOutputDevice import ConnectionType
from cura.Settings.CuraContainerRegistry import CuraContainerRegistry
from cura.Settings.GlobalStack import GlobalStack
class MachineListModel(ListModel):
NameRole = Qt.ItemDataRole.UserRole + 1
IdRole = Qt.ItemDataRole.UserRole + 2
HasRemoteConnectionRole = Qt.ItemDataRole.UserRole + 3
MetaDataRole = Qt.ItemDataRole.UserRole + 4
IsOnlineRole = Qt.ItemDataRole.UserRole + 5
MachineCountRole = Qt.ItemDataRole.UserRole + 6
IsAbstractMachineRole = Qt.ItemDataRole.UserRole + 7
ComponentTypeRole = Qt.ItemDataRole.UserRole + 8
def __init__(self, parent: Optional[QObject] = None) -> None:
super().__init__(parent)
self._show_cloud_printers = False
self._catalog = i18nCatalog("cura")
self.addRoleName(self.NameRole, "name")
self.addRoleName(self.IdRole, "id")
self.addRoleName(self.HasRemoteConnectionRole, "hasRemoteConnection")
self.addRoleName(self.MetaDataRole, "metadata")
self.addRoleName(self.IsOnlineRole, "isOnline")
self.addRoleName(self.MachineCountRole, "machineCount")
self.addRoleName(self.IsAbstractMachineRole, "isAbstractMachine")
self.addRoleName(self.ComponentTypeRole, "componentType")
self._change_timer = QTimer()
self._change_timer.setInterval(200)
self._change_timer.setSingleShot(True)
self._change_timer.timeout.connect(self._update)
# Listen to changes
CuraContainerRegistry.getInstance().containerAdded.connect(self._onContainerChanged)
CuraContainerRegistry.getInstance().containerMetaDataChanged.connect(self._onContainerChanged)
CuraContainerRegistry.getInstance().containerRemoved.connect(self._onContainerChanged)
self._updateDelayed()
showCloudPrintersChanged = pyqtSignal(bool)
@pyqtProperty(bool, notify=showCloudPrintersChanged)
def showCloudPrinters(self) -> bool:
return self._show_cloud_printers
@pyqtSlot(bool)
def setShowCloudPrinters(self, show_cloud_printers: bool) -> None:
self._show_cloud_printers = show_cloud_printers
self._updateDelayed()
self.showCloudPrintersChanged.emit(show_cloud_printers)
def _onContainerChanged(self, container: ContainerInterface) -> None:
"""Handler for container added/removed events from registry"""
# We only need to update when the added / removed container GlobalStack
if isinstance(container, GlobalStack):
self._updateDelayed()
def _updateDelayed(self) -> None:
self._change_timer.start()
def _update(self) -> None:
self.clear()
from cura.CuraApplication import CuraApplication
machines_manager = CuraApplication.getInstance().getMachineManager()
other_machine_stacks = CuraContainerRegistry.getInstance().findContainerStacks(type="machine")
abstract_machine_stacks = CuraContainerRegistry.getInstance().findContainerStacks(is_abstract_machine = "True")
abstract_machine_stacks.sort(key = lambda machine: machine.getName(), reverse = True)
for abstract_machine in abstract_machine_stacks:
definition_id = abstract_machine.definition.getId()
online_machine_stacks = machines_manager.getMachinesWithDefinition(definition_id, online_only = True)
online_machine_stacks = list(filter(lambda machine: machine.hasNetworkedConnection(), online_machine_stacks))
other_machine_stacks.remove(abstract_machine)
if abstract_machine in online_machine_stacks:
online_machine_stacks.remove(abstract_machine)
# Create a list item for abstract machine
self.addItem(abstract_machine, True, len(online_machine_stacks))
# Create list of machines that are children of the abstract machine
for stack in online_machine_stacks:
if self._show_cloud_printers:
self.addItem(stack, True)
# Remove this machine from the other stack list
if stack in other_machine_stacks:
other_machine_stacks.remove(stack)
if len(abstract_machine_stacks) > 0:
if self._show_cloud_printers:
self.appendItem({"componentType": "HIDE_BUTTON",
"isOnline": True,
"isAbstractMachine": False,
"machineCount": 0
})
else:
self.appendItem({"componentType": "SHOW_BUTTON",
"isOnline": True,
"isAbstractMachine": False,
"machineCount": 0
})
for stack in other_machine_stacks:
self.addItem(stack, False)
def addItem(self, container_stack: ContainerStack, is_online: bool, machine_count: int = 0) -> None:
if parseBool(container_stack.getMetaDataEntry("hidden", False)):
return
self.appendItem({
"componentType": "MACHINE",
"name": container_stack.getName(),
"id": container_stack.getId(),
"metadata": container_stack.getMetaData().copy(),
"isOnline": is_online,
"isAbstractMachine": parseBool(container_stack.getMetaDataEntry("is_abstract_machine", False)),
"machineCount": machine_count,
})

View File

@ -50,8 +50,13 @@ class PlatformPhysics:
if not self._enabled: if not self._enabled:
return return
app_instance = Application.getInstance()
app_preferences = app_instance.getPreferences()
app_automatic_drop_down = app_preferences.getValue("physics/automatic_drop_down")
app_automatic_push_free = app_preferences.getValue("physics/automatic_push_free")
root = self._controller.getScene().getRoot() root = self._controller.getScene().getRoot()
build_volume = Application.getInstance().getBuildVolume() build_volume = app_instance.getBuildVolume()
build_volume.updateNodeBoundaryCheck() build_volume.updateNodeBoundaryCheck()
# Keep a list of nodes that are moving. We use this so that we don't move two intersecting objects in the # Keep a list of nodes that are moving. We use this so that we don't move two intersecting objects in the
@ -75,7 +80,7 @@ class PlatformPhysics:
# Move it downwards if bottom is above platform # Move it downwards if bottom is above platform
move_vector = Vector() move_vector = Vector()
if Application.getInstance().getPreferences().getValue("physics/automatic_drop_down") and not (node.getParent() and node.getParent().callDecoration("isGroup") or node.getParent() != root) and node.isEnabled(): #If an object is grouped, don't move it down if node.getSetting(SceneNodeSettings.AutoDropDown, app_automatic_drop_down) and not (node.getParent() and node.getParent().callDecoration("isGroup") or node.getParent() != root) and node.isEnabled(): #If an object is grouped, don't move it down
z_offset = node.callDecoration("getZOffset") if node.getDecorator(ZOffsetDecorator.ZOffsetDecorator) else 0 z_offset = node.callDecoration("getZOffset") if node.getDecorator(ZOffsetDecorator.ZOffsetDecorator) else 0
move_vector = move_vector.set(y = -bbox.bottom + z_offset) move_vector = move_vector.set(y = -bbox.bottom + z_offset)
@ -84,7 +89,7 @@ class PlatformPhysics:
node.addDecorator(ConvexHullDecorator()) node.addDecorator(ConvexHullDecorator())
# only push away objects if this node is a printing mesh # only push away objects if this node is a printing mesh
if not node.callDecoration("isNonPrintingMesh") and Application.getInstance().getPreferences().getValue("physics/automatic_push_free"): if not node.callDecoration("isNonPrintingMesh") and app_automatic_push_free:
# Do not move locked nodes # Do not move locked nodes
if node.getSetting(SceneNodeSettings.LockPosition): if node.getSetting(SceneNodeSettings.LockPosition):
continue continue

View File

@ -13,9 +13,9 @@ class ExtruderConfigurationModel(QObject):
def __init__(self, position: int = -1) -> None: def __init__(self, position: int = -1) -> None:
super().__init__() super().__init__()
self._position = position # type: int self._position: int = position
self._material = None # type: Optional[MaterialOutputModel] self._material: Optional[MaterialOutputModel] = None
self._hotend_id = None # type: Optional[str] self._hotend_id: Optional[str] = None
def setPosition(self, position: int) -> None: def setPosition(self, position: int) -> None:
self._position = position self._position = position

View File

@ -350,5 +350,6 @@ class PrinterOutputModel(QObject):
self.availableConfigurationsChanged.emit() self.availableConfigurationsChanged.emit()
def setAvailableConfigurations(self, new_configurations: List[PrinterConfigurationModel]) -> None: def setAvailableConfigurations(self, new_configurations: List[PrinterConfigurationModel]) -> None:
self._available_printer_configurations = new_configurations if self._available_printer_configurations != new_configurations:
self.availableConfigurationsChanged.emit() self._available_printer_configurations = new_configurations
self.availableConfigurationsChanged.emit()

View File

@ -1,20 +1,19 @@
# Copyright (c) 2019 Ultimaker B.V. # Copyright (c) 2019 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
from dataclasses import dataclass
@dataclass
class Peripheral: class Peripheral:
"""Data class that represents a peripheral for a printer. """Data class that represents a peripheral for a printer.
Output device plug-ins may specify that the printer has a certain set of Output device plug-ins may specify that the printer has a certain set of
peripherals. This set is then possibly shown in the interface of the monitor peripherals. This set is then possibly shown in the interface of the monitor
stage. stage.
Args:
type (string): A unique ID for the type of peripheral.
name (string): A human-readable name for the peripheral.
""" """
type: str
def __init__(self, peripheral_type: str, name: str) -> None: name: str
"""Constructs the peripheral.
:param peripheral_type: A unique ID for the type of peripheral.
:param name: A human-readable name for the peripheral.
"""
self.type = peripheral_type
self.name = name

View File

@ -50,13 +50,12 @@ class PrinterOutputDevice(QObject, OutputDevice):
The assumption is made the printer is a FDM printer. The assumption is made the printer is a FDM printer.
Note that a number of settings are marked as "final". This is because decorators Note that a number of settings are marked as "final". This is because decorators
are not inherited by children. To fix this we use the private counter part of those are not inherited by children. To fix this we use the private counterpart of those
functions to actually have the implementation. functions to actually have the implementation.
For all other uses it should be used in the same way as a "regular" OutputDevice. For all other uses it should be used in the same way as a "regular" OutputDevice.
""" """
printersChanged = pyqtSignal() printersChanged = pyqtSignal()
connectionStateChanged = pyqtSignal(str) connectionStateChanged = pyqtSignal(str)
acceptsCommandsChanged = pyqtSignal() acceptsCommandsChanged = pyqtSignal()
@ -183,8 +182,8 @@ class PrinterOutputDevice(QObject, OutputDevice):
@pyqtProperty(QObject, constant = True) @pyqtProperty(QObject, constant = True)
def monitorItem(self) -> QObject: def monitorItem(self) -> QObject:
# Note that we specifically only check if the monitor component is created. # Note that we specifically only check if the monitor component is created.
# It could be that it failed to actually create the qml item! If we check if the item was created, it will try to # It could be that it failed to actually create the qml item! If we check if the item was created, it will try
# create the item (and fail) every time. # to create the item (and fail) every time.
if not self._monitor_component: if not self._monitor_component:
self._createMonitorViewFromQML() self._createMonitorViewFromQML()
return self._monitor_item return self._monitor_item
@ -237,9 +236,9 @@ class PrinterOutputDevice(QObject, OutputDevice):
self.acceptsCommandsChanged.emit() self.acceptsCommandsChanged.emit()
# Returns the unique configurations of the printers within this output device
@pyqtProperty("QVariantList", notify = uniqueConfigurationsChanged) @pyqtProperty("QVariantList", notify = uniqueConfigurationsChanged)
def uniqueConfigurations(self) -> List["PrinterConfigurationModel"]: def uniqueConfigurations(self) -> List["PrinterConfigurationModel"]:
""" Returns the unique configurations of the printers within this output device """
return self._unique_configurations return self._unique_configurations
def _updateUniqueConfigurations(self) -> None: def _updateUniqueConfigurations(self) -> None:
@ -248,7 +247,9 @@ class PrinterOutputDevice(QObject, OutputDevice):
if printer.printerConfiguration is not None and printer.printerConfiguration.hasAnyMaterialLoaded(): if printer.printerConfiguration is not None and printer.printerConfiguration.hasAnyMaterialLoaded():
all_configurations.add(printer.printerConfiguration) all_configurations.add(printer.printerConfiguration)
all_configurations.update(printer.availableConfigurations) all_configurations.update(printer.availableConfigurations)
if None in all_configurations: # Shouldn't happen, but it does. I don't see how it could ever happen. Skip adding that configuration. List could end up empty! if None in all_configurations:
# Shouldn't happen, but it does. I don't see how it could ever happen. Skip adding that configuration.
# List could end up empty!
Logger.log("e", "Found a broken configuration in the synced list!") Logger.log("e", "Found a broken configuration in the synced list!")
all_configurations.remove(None) all_configurations.remove(None)
new_configurations = sorted(all_configurations, key = lambda config: config.printerType or "") new_configurations = sorted(all_configurations, key = lambda config: config.printerType or "")
@ -256,9 +257,9 @@ class PrinterOutputDevice(QObject, OutputDevice):
self._unique_configurations = new_configurations self._unique_configurations = new_configurations
self.uniqueConfigurationsChanged.emit() self.uniqueConfigurationsChanged.emit()
# Returns the unique configurations of the printers within this output device
@pyqtProperty("QStringList", notify = uniqueConfigurationsChanged) @pyqtProperty("QStringList", notify = uniqueConfigurationsChanged)
def uniquePrinterTypes(self) -> List[str]: def uniquePrinterTypes(self) -> List[str]:
""" Returns the unique configurations of the printers within this output device """
return list(sorted(set([configuration.printerType or "" for configuration in self._unique_configurations]))) return list(sorted(set([configuration.printerType or "" for configuration in self._unique_configurations])))
def _onPrintersChanged(self) -> None: def _onPrintersChanged(self) -> None:

View File

@ -390,7 +390,7 @@ class ConvexHullDecorator(SceneNodeDecorator):
if self._global_stack.getProperty("print_sequence", "value") == "one_at_a_time": if self._global_stack.getProperty("print_sequence", "value") == "one_at_a_time":
# Find the root node that's placed in the scene; the root of the mesh group. # Find the root node that's placed in the scene; the root of the mesh group.
ancestor = self.getNode() ancestor = self.getNode()
while ancestor.getParent() != self._root: while ancestor.getParent() != self._root and ancestor.getParent() is not None:
ancestor = ancestor.getParent() ancestor = ancestor.getParent()
center = ancestor.getBoundingBox().center center = ancestor.getBoundingBox().center
else: else:

View File

@ -139,7 +139,7 @@ class CuraSceneController(QObject):
def setActiveBuildPlate(self, nr): def setActiveBuildPlate(self, nr):
if nr == self._active_build_plate: if nr == self._active_build_plate:
return return
Logger.log("d", "Select build plate: %s" % nr) Logger.debug(f"Selected build plate: {nr}")
self._active_build_plate = nr self._active_build_plate = nr
Selection.clear() Selection.clear()

View File

@ -108,7 +108,7 @@ class CuraContainerRegistry(ContainerRegistry):
:param container_type: :type{string} Type of the container (machine, quality, ...) :param container_type: :type{string} Type of the container (machine, quality, ...)
:param container_name: :type{string} Name to check :param container_name: :type{string} Name to check
""" """
container_class = ContainerStack if container_type == "machine" else InstanceContainer container_class = ContainerStack if "machine" in container_type else InstanceContainer
return self.findContainersMetadata(container_type = container_class, id = container_name, type = container_type, ignore_case = True) or \ return self.findContainersMetadata(container_type = container_class, id = container_name, type = container_type, ignore_case = True) or \
self.findContainersMetadata(container_type = container_class, name = container_name, type = container_type) self.findContainersMetadata(container_type = container_class, name = container_name, type = container_type)

View File

@ -49,7 +49,7 @@ class CuraContainerStack(ContainerStack):
self._empty_material = cura_empty_instance_containers.empty_material_container #type: InstanceContainer self._empty_material = cura_empty_instance_containers.empty_material_container #type: InstanceContainer
self._empty_variant = cura_empty_instance_containers.empty_variant_container #type: InstanceContainer self._empty_variant = cura_empty_instance_containers.empty_variant_container #type: InstanceContainer
self._containers = [self._empty_instance_container for i in range(len(_ContainerIndexes.IndexTypeMap))] #type: List[ContainerInterface] self._containers: List[ContainerInterface] = [self._empty_instance_container for i in _ContainerIndexes.IndexTypeMap]
self._containers[_ContainerIndexes.QualityChanges] = self._empty_quality_changes self._containers[_ContainerIndexes.QualityChanges] = self._empty_quality_changes
self._containers[_ContainerIndexes.Quality] = self._empty_quality self._containers[_ContainerIndexes.Quality] = self._empty_quality
self._containers[_ContainerIndexes.Material] = self._empty_material self._containers[_ContainerIndexes.Material] = self._empty_material
@ -427,4 +427,4 @@ class _ContainerIndexes:
} }
# Reverse lookup: type -> index # Reverse lookup: type -> index
TypeIndexMap = dict([(v, k) for k, v in IndexTypeMap.items()]) TypeIndexMap = {v: k for k, v in IndexTypeMap.items()}

View File

@ -1,7 +1,9 @@
# Copyright (c) 2019 Ultimaker B.V. # Copyright (c) 2022 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
from typing import Optional import copy
from typing import Optional, cast
from UM.ConfigurationErrorMessage import ConfigurationErrorMessage from UM.ConfigurationErrorMessage import ConfigurationErrorMessage
from UM.Logger import Logger from UM.Logger import Logger
@ -27,7 +29,7 @@ class CuraStackBuilder:
:return: The new global stack or None if an error occurred. :return: The new global stack or None if an error occurred.
""" """
from cura.CuraApplication import CuraApplication from cura.CuraApplication import CuraApplication # inline import needed due to circular import
application = CuraApplication.getInstance() application = CuraApplication.getInstance()
registry = application.getContainerRegistry() registry = application.getContainerRegistry()
container_tree = ContainerTree.getInstance() container_tree = ContainerTree.getInstance()
@ -91,7 +93,7 @@ class CuraStackBuilder:
:param extruder_position: The position of the current extruder. :param extruder_position: The position of the current extruder.
""" """
from cura.CuraApplication import CuraApplication from cura.CuraApplication import CuraApplication # inline import needed due to circular import
application = CuraApplication.getInstance() application = CuraApplication.getInstance()
registry = application.getContainerRegistry() registry = application.getContainerRegistry()
@ -199,13 +201,21 @@ class CuraStackBuilder:
:return: A new Global stack instance with the specified parameters. :return: A new Global stack instance with the specified parameters.
""" """
from cura.CuraApplication import CuraApplication
application = CuraApplication.getInstance()
registry = application.getContainerRegistry()
stack = GlobalStack(new_stack_id) stack = GlobalStack(new_stack_id)
stack.setDefinition(definition) stack.setDefinition(definition)
cls.createUserContainer(new_stack_id, definition, stack, variant_container, material_container, quality_container)
return stack
@classmethod
def createUserContainer(cls, new_stack_id: str, definition: DefinitionContainerInterface,
stack: GlobalStack,
variant_container: "InstanceContainer",
material_container: "InstanceContainer",
quality_container: "InstanceContainer") -> None:
from cura.CuraApplication import CuraApplication
application = CuraApplication.getInstance()
registry = application.getContainerRegistry()
# Create user container # Create user container
user_container = cls.createUserChangesContainer(new_stack_id + "_user", definition.getId(), new_stack_id, user_container = cls.createUserChangesContainer(new_stack_id + "_user", definition.getId(), new_stack_id,
@ -221,8 +231,6 @@ class CuraStackBuilder:
registry.addContainer(user_container) registry.addContainer(user_container)
return stack
@classmethod @classmethod
def createUserChangesContainer(cls, container_name: str, definition_id: str, stack_id: str, def createUserChangesContainer(cls, container_name: str, definition_id: str, stack_id: str,
is_global_stack: bool) -> "InstanceContainer": is_global_stack: bool) -> "InstanceContainer":
@ -259,3 +267,36 @@ class CuraStackBuilder:
container_stack.definitionChanges = definition_changes_container container_stack.definitionChanges = definition_changes_container
return definition_changes_container return definition_changes_container
@classmethod
def createAbstractMachine(cls, definition_id: str) -> Optional[GlobalStack]:
"""Create a new instance of an abstract machine.
:param definition_id: The ID of the machine definition to use.
:return: The new Abstract Machine or None if an error occurred.
"""
abstract_machine_id = f"{definition_id}_abstract_machine"
from cura.CuraApplication import CuraApplication
application = CuraApplication.getInstance()
registry = application.getContainerRegistry()
abstract_machines = registry.findContainerStacks(id = abstract_machine_id)
if abstract_machines:
return cast(GlobalStack, abstract_machines[0])
definitions = registry.findDefinitionContainers(id=definition_id)
name = ""
if definitions:
name = definitions[0].getName()
stack = cls.createMachine(abstract_machine_id, definition_id, show_warning_message=False)
if not stack:
return None
stack.setName(name)
stack.setMetaDataEntry("is_abstract_machine", True)
stack.setMetaDataEntry("is_online", True)
return stack

View File

@ -1,4 +1,4 @@
# Copyright (c) 2019 Ultimaker B.V. # Copyright (c) 2022 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
from collections import defaultdict from collections import defaultdict
@ -8,10 +8,9 @@ import uuid
from PyQt6.QtCore import pyqtProperty, pyqtSlot, pyqtSignal from PyQt6.QtCore import pyqtProperty, pyqtSlot, pyqtSignal
from UM.Decorators import deprecated, override from UM.Decorators import override
from UM.MimeTypeDatabase import MimeType, MimeTypeDatabase from UM.MimeTypeDatabase import MimeType, MimeTypeDatabase
from UM.Settings.ContainerStack import ContainerStack from UM.Settings.ContainerStack import ContainerStack
from UM.Settings.SettingInstance import InstanceState
from UM.Settings.ContainerRegistry import ContainerRegistry from UM.Settings.ContainerRegistry import ContainerRegistry
from UM.Settings.Interfaces import PropertyEvaluationContext from UM.Settings.Interfaces import PropertyEvaluationContext
from UM.Logger import Logger from UM.Logger import Logger
@ -91,7 +90,6 @@ class GlobalStack(CuraContainerStack):
@pyqtProperty("QVariantList", notify=configuredConnectionTypesChanged) @pyqtProperty("QVariantList", notify=configuredConnectionTypesChanged)
def configuredConnectionTypes(self) -> List[int]: def configuredConnectionTypes(self) -> List[int]:
"""The configured connection types can be used to find out if the global """The configured connection types can be used to find out if the global
stack is configured to be connected with a printer, without having to stack is configured to be connected with a printer, without having to
know all the details as to how this is exactly done (and without know all the details as to how this is exactly done (and without
actually setting the stack to be active). actually setting the stack to be active).
@ -293,7 +291,6 @@ class GlobalStack(CuraContainerStack):
for extruder_train in extruder_trains: for extruder_train in extruder_trains:
extruder_position = extruder_train.getMetaDataEntry("position") extruder_position = extruder_train.getMetaDataEntry("position")
extruder_check_position.add(extruder_position) extruder_check_position.add(extruder_position)
for check_position in range(machine_extruder_count): for check_position in range(machine_extruder_count):
if str(check_position) not in extruder_check_position: if str(check_position) not in extruder_check_position:
return False return False
@ -344,13 +341,17 @@ class GlobalStack(CuraContainerStack):
def getName(self) -> str: def getName(self) -> str:
return self._metadata.get("group_name", self._metadata.get("name", "")) return self._metadata.get("group_name", self._metadata.get("name", ""))
def setName(self, name: "str") -> None: def setName(self, name: str) -> None:
super().setName(name) super().setName(name)
nameChanged = pyqtSignal() nameChanged = pyqtSignal()
name = pyqtProperty(str, fget=getName, fset=setName, notify=nameChanged) name = pyqtProperty(str, fget=getName, fset=setName, notify=nameChanged)
def hasNetworkedConnection(self) -> bool:
has_connection = False
for connection_type in [ConnectionType.NetworkConnection.value, ConnectionType.CloudConnection.value]:
has_connection |= connection_type in self.configuredConnectionTypes
return has_connection
## private: ## private:
global_stack_mime = MimeType( global_stack_mime = MimeType(

View File

@ -1,4 +1,4 @@
# Copyright (c) 2021 Ultimaker B.V. # Copyright (c) 2022 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
import time import time
@ -19,6 +19,7 @@ from UM.Logger import Logger
from UM.Message import Message from UM.Message import Message
from UM.Settings.SettingFunction import SettingFunction from UM.Settings.SettingFunction import SettingFunction
from UM.Settings.ContainerStack import ContainerStack
from UM.Signal import postponeSignals, CompressTechnique from UM.Signal import postponeSignals, CompressTechnique
import cura.CuraApplication # Imported like this to prevent circular references. import cura.CuraApplication # Imported like this to prevent circular references.
@ -98,7 +99,7 @@ class MachineManager(QObject):
self._application.getPreferences().addPreference("cura/active_machine", "") self._application.getPreferences().addPreference("cura/active_machine", "")
self._printer_output_devices = [] # type: List[PrinterOutputDevice] self._printer_output_devices: List[PrinterOutputDevice] = []
self._application.getOutputDeviceManager().outputDevicesChanged.connect(self._onOutputDevicesChanged) self._application.getOutputDeviceManager().outputDevicesChanged.connect(self._onOutputDevicesChanged)
# There might already be some output devices by the time the signal is connected # There might already be some output devices by the time the signal is connected
self._onOutputDevicesChanged() self._onOutputDevicesChanged()
@ -111,7 +112,7 @@ class MachineManager(QObject):
self._application.callLater(self.setInitialActiveMachine) self._application.callLater(self.setInitialActiveMachine)
containers = CuraContainerRegistry.getInstance().findInstanceContainers(id = self.activeMaterialId) # type: List[InstanceContainer] containers: List[InstanceContainer] = CuraContainerRegistry.getInstance().findInstanceContainers(id = self.activeMaterialId)
if containers: if containers:
containers[0].nameChanged.connect(self._onMaterialNameChanged) containers[0].nameChanged.connect(self._onMaterialNameChanged)
@ -186,6 +187,32 @@ class MachineManager(QObject):
self.outputDevicesChanged.emit() self.outputDevicesChanged.emit()
def getMachinesWithDefinition(self, definition_id: str, online_only=False) -> List[ContainerStack]:
""" Fetches all container stacks that match definition_id.
:param definition_id: The id of the machine definition.
:return: A list of Containers that match definition_id
"""
from cura.CuraApplication import CuraApplication # In function to avoid circular import
application = CuraApplication.getInstance()
registry = application.getContainerRegistry()
machines = registry.findContainerStacks(type="machine")
# Filter machines that match definition
machines = filter(lambda machine: machine.definition.id == definition_id, machines)
# Filter only LAN and Cloud printers
machines = filter(lambda machine: ConnectionType.CloudConnection in machine.configuredConnectionTypes or
ConnectionType.NetworkConnection in machine.configuredConnectionTypes,
machines)
if online_only:
# LAN printers can have is_online = False but should still be included,
# their online status is only checked when they are the active printer.
machines = filter(lambda machine: parseBool(machine.getMetaDataEntry("is_online", False) or
ConnectionType.NetworkConnection in machine.configuredConnectionTypes),
machines)
return list(machines)
@pyqtProperty(QObject, notify = currentConfigurationChanged) @pyqtProperty(QObject, notify = currentConfigurationChanged)
def currentConfiguration(self) -> PrinterConfigurationModel: def currentConfiguration(self) -> PrinterConfigurationModel:
return self._current_printer_configuration return self._current_printer_configuration
@ -332,6 +359,7 @@ class MachineManager(QObject):
extruder_manager = ExtruderManager.getInstance() extruder_manager = ExtruderManager.getInstance()
extruder_manager.fixSingleExtrusionMachineExtruderDefinition(global_stack) extruder_manager.fixSingleExtrusionMachineExtruderDefinition(global_stack)
if not global_stack.isValid(): if not global_stack.isValid():
Logger.warning("Global stack isn't valid, adding it to faulty container list")
# Mark global stack as invalid # Mark global stack as invalid
ConfigurationErrorMessage.getInstance().addFaultyContainers(global_stack.getId()) ConfigurationErrorMessage.getInstance().addFaultyContainers(global_stack.getId())
return # We're done here return # We're done here
@ -503,6 +531,10 @@ class MachineManager(QObject):
def printerConnected(self) -> bool: def printerConnected(self) -> bool:
return bool(self._printer_output_devices) return bool(self._printer_output_devices)
@pyqtProperty(bool, notify = globalContainerChanged)
def activeMachineIsAbstractCloudPrinter(self) -> bool:
return len(self._printer_output_devices) == 1 and self._printer_output_devices[0].__class__.__name__ == "AbstractCloudOutputDevice"
@pyqtProperty(bool, notify = printerConnectedStatusChanged) @pyqtProperty(bool, notify = printerConnectedStatusChanged)
def activeMachineIsGroup(self) -> bool: def activeMachineIsGroup(self) -> bool:
if self.activeMachine is None: if self.activeMachine is None:
@ -872,7 +904,7 @@ class MachineManager(QObject):
if self._global_container_stack is None \ if self._global_container_stack is None \
or self._global_container_stack.getProperty(setting_key, "value") == new_value \ or self._global_container_stack.getProperty(setting_key, "value") == new_value \
or self.numberExtrudersEnabled < 2: or self._global_container_stack.definitionChanges.getProperty("extruders_enabled_count", "value") < 2:
return return
user_changes_container = self._global_container_stack.userChanges user_changes_container = self._global_container_stack.userChanges
@ -977,7 +1009,7 @@ class MachineManager(QObject):
for position, extruder in enumerate(self._global_container_stack.extruderList): for position, extruder in enumerate(self._global_container_stack.extruderList):
if extruder.isEnabled and int(position) < machine_extruder_count: if extruder.isEnabled and int(position) < machine_extruder_count:
extruder_count += 1 extruder_count += 1
if self.numberExtrudersEnabled != extruder_count: if self._global_container_stack.definitionChanges.getProperty("extruders_enabled_count", "value") != extruder_count:
definition_changes_container.setProperty("extruders_enabled_count", "value", extruder_count) definition_changes_container.setProperty("extruders_enabled_count", "value", extruder_count)
self.numberExtrudersEnabledChanged.emit() self.numberExtrudersEnabledChanged.emit()

View File

@ -10,10 +10,13 @@ if TYPE_CHECKING:
# #
# This class manages a all registered upon-exit checks that need to be perform when the application tries to exit. # This class manages all registered upon-exit checks
# For example, to show a confirmation dialog when there is USB printing in progress, etc. All callbacks will be called # that need to be performed when the application tries to exit.
# in the order of when they got registered. If all callbacks "passes", that is, for example, if the user clicks "yes" # For example, show a confirmation dialog when there is USB printing in progress.
# on the exit confirmation dialog or nothing that's blocking the exit, then the application will quit after that. # All callbacks will be called in the order of when they were registered.
# If all callbacks "pass", for example:
# if the user clicks "yes" on the exit confirmation dialog
# and nothing else is blocking the exit, then the application will quit.
# #
class OnExitCallbackManager: class OnExitCallbackManager:
@ -35,10 +38,12 @@ class OnExitCallbackManager:
def getIsAllChecksPassed(self) -> bool: def getIsAllChecksPassed(self) -> bool:
return self._is_all_checks_passed return self._is_all_checks_passed
# Trigger the next callback if available. If not, it means that all callbacks have "passed", which means we should # Trigger the next callback if there is one.
# not block the application to quit, and it will call the application to actually quit. # If not, all callbacks have "passed",
# which means we should not prevent the application from quitting,
# and we call the application to actually quit.
def triggerNextCallback(self) -> None: def triggerNextCallback(self) -> None:
# Get the next callback and schedule that if # Get the next callback and schedule it
this_callback = None this_callback = None
if self._current_callback_idx < len(self._on_exit_callback_list): if self._current_callback_idx < len(self._on_exit_callback_list):
this_callback = self._on_exit_callback_list[self._current_callback_idx] this_callback = self._on_exit_callback_list[self._current_callback_idx]
@ -55,10 +60,11 @@ class OnExitCallbackManager:
# Tell the application to exit # Tell the application to exit
self._application.callLater(self._application.closeApplication) self._application.callLater(self._application.closeApplication)
# This is the callback function which an on-exit callback should call when it finishes, it should provide the # Callback function which an on-exit callback calls when it finishes.
# "should_proceed" flag indicating whether this check has "passed", or in other words, whether quitting the # It provides a "should_proceed" flag indicating whether the check has "passed",
# application should be blocked. If the last on-exit callback doesn't block the quitting, it will call the next # or whether quitting the application should be blocked.
# registered on-exit callback if available. # If the last on-exit callback doesn't block quitting, it will call the next
# registered on-exit callback if one is available.
def onCurrentCallbackFinished(self, should_proceed: bool = True) -> None: def onCurrentCallbackFinished(self, should_proceed: bool = True) -> None:
if not should_proceed: if not should_proceed:
Logger.log("d", "on-app-exit callback finished and we should not proceed.") Logger.log("d", "on-app-exit callback finished and we should not proceed.")

View File

@ -23,9 +23,9 @@ catalog = i18nCatalog("cura")
class PrintInformation(QObject): class PrintInformation(QObject):
"""A class for processing and the print times per build plate as well as managing the job name """A class for processing the print times per build plate and managing the job name
This class also mangles the current machine name and the filename of the first loaded mesh into a job name. This class also combines the current machine name and the filename of the first loaded mesh into a job name.
This job name is requested by the JobSpecs qml file. This job name is requested by the JobSpecs qml file.
""" """
@ -186,7 +186,7 @@ class PrintInformation(QObject):
if time != time: # Check for NaN. Engine can sometimes give us weird values. if time != time: # Check for NaN. Engine can sometimes give us weird values.
duration.setDuration(0) duration.setDuration(0)
Logger.log("w", "Received NaN for print duration message") Logger.warning("Received NaN for print duration message")
continue continue
total_estimated_time += time total_estimated_time += time
@ -368,7 +368,7 @@ class PrintInformation(QObject):
mime_type = MimeTypeDatabase.getMimeTypeForFile(name) mime_type = MimeTypeDatabase.getMimeTypeForFile(name)
data = mime_type.stripExtension(name) data = mime_type.stripExtension(name)
except MimeTypeNotFoundError: except MimeTypeNotFoundError:
Logger.log("w", "Unsupported Mime Type Database file extension %s", name) Logger.warning(f"Unsupported Mime Type Database file extension {name}")
if data is not None and check_name is not None: if data is not None and check_name is not None:
self._base_name = data self._base_name = data
@ -392,7 +392,7 @@ class PrintInformation(QObject):
return self._base_name return self._base_name
def _defineAbbreviatedMachineName(self) -> None: def _defineAbbreviatedMachineName(self) -> None:
"""Created an acronym-like abbreviated machine name from the currently active machine name. """Creates an abbreviated machine name from the currently active machine name.
Called each time the global stack is switched. Called each time the global stack is switched.
""" """
@ -446,7 +446,7 @@ class PrintInformation(QObject):
self.setToZeroPrintInformation(self._active_build_plate) self.setToZeroPrintInformation(self._active_build_plate)
def _onOutputStart(self, output_device: OutputDevice) -> None: def _onOutputStart(self, output_device: OutputDevice) -> None:
"""If this is the sort of output 'device' (like local or online file storage, rather than a printer), """If this is a sort of output 'device' (like local or online file storage, rather than a printer),
the user could have altered the file-name, and thus the project name should be altered as well.""" the user could have altered the file-name, and thus the project name should be altered as well."""
if isinstance(output_device, ProjectOutputDevice): if isinstance(output_device, ProjectOutputDevice):
new_name = output_device.getLastOutputName() new_name = output_device.getLastOutputName()

View File

@ -62,15 +62,21 @@ class WhatsNewPagesModel(WelcomePagesModel):
def initialize(self) -> None: def initialize(self) -> None:
self._pages = [] self._pages = []
self._pages.append({"id": "whats_new", try:
"page_url": self._getBuiltinWelcomePagePath("WhatsNewContent.qml"), self._pages.append({"id": "whats_new",
"next_page_button_text": self._catalog.i18nc("@action:button", "Skip"), "page_url": self._getBuiltinWelcomePagePath("WhatsNewContent.qml"),
"next_page_id": "changelog" "next_page_button_text": self._catalog.i18nc("@action:button", "Skip"),
}) "next_page_id": "changelog"
self._pages.append({"id": "changelog", })
"page_url": self._getBuiltinWelcomePagePath("ChangelogContent.qml"), except FileNotFoundError:
"next_page_button_text": self._catalog.i18nc("@action:button", "Close"), Logger.warning("Unable to find what's new page")
}) try:
self._pages.append({"id": "changelog",
"page_url": self._getBuiltinWelcomePagePath("ChangelogContent.qml"),
"next_page_button_text": self._catalog.i18nc("@action:button", "Close"),
})
except FileNotFoundError:
Logger.warning("Unable to find changelog page")
self.setItems(self._pages) self.setItems(self._pages)
images, max_image = WhatsNewPagesModel._collectOrdinalFiles(Resources.Images, WhatsNewPagesModel.image_formats) images, max_image = WhatsNewPagesModel._collectOrdinalFiles(Resources.Images, WhatsNewPagesModel.image_formats)

81
docs/Report.md Normal file
View File

@ -0,0 +1,81 @@
# Reporting Issues
Please attach the following information in case <br>
you want to report crashing or similar issues.
<br>
## DxDiag
### ![Badge Windows]
The log as produced by **dxdiag**.
<kbd>start</kbd>  »  <kbd>run</kbd>  »  <kbd>dxdiag</kbd>  »  <kbd>save output</kbd>
<br>
<br>
## Cura GUI Log
If the Cura user interface still starts, you can also <br>
reach these directories from the application menu:
<kbd>Help</kbd>  »  <kbd>Show settings folder</kbd>
<br>
### ![Badge Windows]
```
%APPDATA%\cura\< >\cura.log
```
or
```
C:\Users\<your username>\AppData\Roaming\cura\< >\cura.log
```
<br>
### ![Badge Linux]
```
~/.local/share/cura/< >/cura.log
```
<br>
### ![Badge MacOS]
```
~/Library/Application Support/cura/< >/cura.log
```
<br>
<br>
## Alternative
An alternative is to install the **[ExtensiveSupportLogging]** <br>
plugin this creates a zip folder of the relevant log files.
If you're experiencing performance issues, we might ask <br>
you to connect the CPU profiler in this plugin and attach <br>
the collected data to your support ticket.
<br>
<!----------------------------------------------------------------------------->
[ExtensiveSupportLogging]: https://marketplace.ultimaker.com/app/cura/plugins/UltimakerPackages/ExtensiveSupportLogging
<!---------------------------------[ Badges ]---------------------------------->
[Badge Windows]: https://img.shields.io/badge/Windows-0078D6?style=for-the-badge&logoColor=white&logo=Windows
[Badge Linux]: https://img.shields.io/badge/Linux-00A95C?style=for-the-badge&logoColor=white&logo=Linux
[Badge MacOS]: https://img.shields.io/badge/MacOS-403C3D?style=for-the-badge&logoColor=white&logo=MacOS

View File

@ -2,32 +2,77 @@ Setting Properties
==== ====
Each setting in Cura has a number of properties. It's not just a key and a value. This page lists the properties that a setting can define. Each setting in Cura has a number of properties. It's not just a key and a value. This page lists the properties that a setting can define.
* `key` (string): The identifier by which the setting is referenced. This is not a human-readable name, but just a reference string, such as `layer_height_0`. Typically these are named with the most significant category first, in order to sort them better, such as `material_print_temperature`. This is not actually a real property but just an identifier; it can't be changed. * `key` (string): __The identifier by which the setting is referenced.__
* `value` (optional): The current value of the setting. This can be a function, an arbitrary Python expression that depends on the values of other settings. If it's not present, the `default_value` is used. * This is not a human-readable name, but just a reference string, such as `layer_height_0`.
* `default_value`: A default value for the setting if `value` is undefined. This property is required however. It can't be a Python expression, but it can be any JSON type. This is made separate so that CuraEngine can read it out as well for its debugging mode via the command line, without needing a complete Python interpreter. * This is not actually a real property but just an identifier; it can't be changed.
* `label` (string): The human-readable name for the setting. This label is translated. * Typically these are named with the most significant category first, in order to sort them better, such as `material_print_temperature`.
* `description` (string): A longer description of what the setting does when you change it. This description is translated as well. * `value` (optional): __The current value of the setting.__
* `type` (string): The type of value that this setting contains. Allowed types are: `bool`, `str`, `float`, `int`, `enum`, `category`, `[int]`, `vec3`, `polygon` and `polygons`. * This can be a function (an arbitrary Python expression) that depends on the values of other settings.
* `unit` (optional string): A unit that is displayed at the right-hand side of the text field where the user enters the setting value. * If it's not present, the `default_value` is used.
* `resolve` (optional string): A Python expression that resolves disagreements for global settings if multiple per-extruder profiles define different values for a setting. Typically this takes the values for the setting from all stacks and computes one final value for it that will be used for the global setting. For instance, the `resolve` function for the build plate temperature is `max(extruderValues('material_bed_temperature')`, meaning that it will use the hottest bed temperature of all materials of the extruders in use. * `default_value`: __A default value for the setting if `value` is undefined.__
* `limit_to_extruder` (optional): A Python expression that indicates which extruder a setting will be obtained from. This is used for settings that may be extruder-specific but the extruder is not necessarily the current extruder. For instance, support settings need to be evaluated for the support extruder. Infill settings need to be evaluated for the infill extruder if the infill extruder is changed. * This property is required.
* `enabled` (optional string or boolean): Whether the setting can currently be made visible for the user. This can be a simple true/false, or a Python expression that depends on other settings. Typically used for settings that don't apply when another setting is disabled, such as to hide the support settings if support is disabled. * It can't be a Python expression, but it can be any JSON type.
* `minimum_value` (optional): The lowest acceptable value for this setting. If it's any lower, Cura will not allow the user to slice. By convention this is used to prevent setting values that are technically or physically impossible, such as a layer height of 0mm. This property only applies to numerical settings. * This is made separate so that CuraEngine can read it out for its debugging mode via the command line, without needing a complete Python interpreter.
* `maximum_value` (optional): The highest acceptable value for this setting. If it's any higher, Cura will not allow the user to slice. By convention this is used to prevent setting values that are technically or physically impossible, such as a support overhang angle of more than 90 degrees. This property only applies to numerical settings. * `label` (string): __The human-readable name for the setting.__
* `minimum_value_warning` (optional): The threshold under which a warning is displayed to the user. By convention this is used to indicate that it will probably not print very nicely with such a low setting value. This property only applies to numerical settings. * This label is translated.
* `maximum_value_warning` (optional): The threshold above which a warning is displayed to the user. By convention this is used to indicate that it will probably not print very nicely with such a high setting value. This property only applies to numerical settings. * `description` (string): __A longer description of what the setting does when you change it.__
* `settable_globally` (optional boolean): Whether the setting can be changed globally. For some mesh-type settings such as `support_mesh` this doesn't make sense, so those can't be changed globally. They are not displayed in the main settings list then. * This description is translated.
* `settable_per_meshgroup` (optional boolean): Whether a setting can be changed per group of meshes. Currently unused in Cura. * `type` (string): __The type of value that this setting contains.__
* `settable_per_extruder` (optional boolean): Whether a setting can be changed per extruder. Some settings, like the build plate temperature, can't be adjusted separately for each extruder. An icon is shown in the interface to indicate this. If the user changes these settings they are stored in the global stack. * Allowed types are: `bool`, `str`, `float`, `int`, `enum`, `category`, `[int]`, `vec3`, `polygon` and `polygons`.
* `settable_per_mesh` (optional boolean): Whether a setting can be changed per mesh. The settings that can be changed per mesh are shown in the list of available settings in the per-object settings tool. * `unit` (optional string): __A unit that is displayed at the right-hand side of the text field where the user enters the setting value.__
* `children` (optional list): A list of child settings. These are displayed with an indentation. If all child settings are overridden by the user, the parent setting gets greyed out to indicate that the parent setting has no effect any more. This is not strictly always the case though, because that would depend on the inheritance functions in the `value`. * `resolve` (optional string): __A Python expression that resolves disagreements for global settings if multiple per-extruder profiles define different values for a setting.__
* `icon` (optional string): A path to an icon to be displayed. Only applies to setting categories. * Typically this takes the values for the setting from all stacks and computes one final value for it that will be used for the global setting. For instance, the `resolve` function for the build plate temperature is `max(extruderValues('material_bed_temperature')`, meaning that it will use the hottest bed temperature of all materials of the extruders in use.
* `allow_empty` (optional bool): Whether the setting is allowed to be empty. If it's not, this will be treated as a setting error and Cura will not allow the user to slice. Only applies to string-type settings. * `limit_to_extruder` (optional): __A Python expression that indicates which extruder a setting will be obtained from.__
* `warning_description` (optional string): A warning message to display when the setting has a warning value. This is currently unused by Cura. * This is used for settings that may be extruder-specific but the extruder is not necessarily the current extruder. For instance, support settings need to be evaluated for the support extruder. Infill settings need to be evaluated for the infill extruder if the infill extruder is changed.
* `error_description` (optional string): An error message to display when the setting has an error value. This is currently unused by Cura. * `enabled` (optional string or boolean): __Whether the setting can currently be made visible for the user.__
* `options` (dictionary): A list of values that the user can choose from. The keys of this dictionary are keys that CuraEngine identifies the option with. The values are human-readable strings and will be translated. Only applies to (and only required for) enum-type settings. * This can be a simple true/false, or a Python expression that depends on other settings.
* `comments` (optional string): Comments to other programmers about the setting. This is not used by Cura. * Typically used for settings that don't apply when another setting is disabled, such as to hide the support settings if support is disabled.
* `is_uuid` (optional boolean): Whether or not this setting indicates a UUID-4. If it is, the setting will indicate an error if it's not in the correct format. Only applies to string-type settings. * `minimum_value` (optional): __The lowest acceptable value for this setting.__
* `regex_blacklist_pattern` (optional string): A regular expression, where if the setting value matches with this regular expression, it gets an error state. Only applies to string-type settings. * If it's any lower, Cura will not allow the user to slice.
* `error_value` (optional): If the setting value is equal to this value, it will show a setting error. This is used to display errors for non-numerical settings such as checkboxes. * This property only applies to numerical settings.
* `warning_value` (optional): If the setting value is equal to this value, it will show a setting warning. This is used to display warnings for non-numerical settings such as checkboxes. * By convention this is used to prevent setting values that are technically or physically impossible, such as a layer height of 0mm.
* `maximum_value` (optional): __The highest acceptable value for this setting.__
* If it's any higher, Cura will not allow the user to slice.
* This property only applies to numerical settings.
* By convention this is used to prevent setting values that are technically or physically impossible, such as a support overhang angle of more than 90 degrees.
* `minimum_value_warning` (optional): __The threshold under which a warning is displayed to the user.__
* This property only applies to numerical settings.
* By convention this is used to indicate that it will probably not print very nicely with such a low setting value.
* `maximum_value_warning` (optional): __The threshold above which a warning is displayed to the user.__
* This property only applies to numerical settings.
* By convention this is used to indicate that it will probably not print very nicely with such a high setting value.
* `settable_globally` (optional boolean): __Whether the setting can be changed globally.__
* For some mesh-type settings such as `support_mesh` this doesn't make sense, so those can't be changed globally. They are not displayed in the main settings list then.
* `settable_per_meshgroup` (optional boolean): __Whether a setting can be changed per group of meshes.__
* *This is currently unused by Cura.*
* `settable_per_extruder` (optional boolean): __Whether a setting can be changed per extruder.__
* Some settings, like the build plate temperature, can't be adjusted separately for each extruder. An icon is shown in the interface to indicate this.
* If the user changes these settings they are stored in the global stack.
* `settable_per_mesh` (optional boolean): __Whether a setting can be changed per mesh.__
* The settings that can be changed per mesh are shown in the list of available settings in the per-object settings tool.
* `children` (optional list): __A list of child settings.__
* These are displayed with an indentation. If all child settings are overridden by the user, the parent setting gets greyed out to indicate that the parent setting has no effect any more. This is not strictly always the case though, because that would depend on the inheritance functions in the `value`.
* `icon` (optional string): __A path to an icon to be displayed.__
* Only applies to setting categories.
* `allow_empty` (optional bool): __Whether the setting is allowed to be empty.__
* If it's not, this will be treated as a setting error and Cura will not allow the user to slice.
* Only applies to string-type settings.
* `warning_description` (optional string): __A warning message to display when the setting has a warning value.__
* *This is currently unused by Cura.*
* `error_description` (optional string): __An error message to display when the setting has an error value.__
* *This is currently unused by Cura.*
* `options` (dictionary): __A list of values that the user can choose from.__
* The keys of this dictionary are keys that CuraEngine identifies the option with.
* The values are human-readable strings and will be translated.
* Only applies to (and only required for) enum-type settings.
* `comments` (optional string): __Comments to other programmers about the setting.__
* *This is currently unused by Cura.*
* `is_uuid` (optional boolean): __Whether or not this setting indicates a UUID-4.__
* If it is, the setting will indicate an error if it's not in the correct format.
* Only applies to string-type settings.
* `regex_blacklist_pattern` (optional string): __A regular expression, where if the setting value matches with this regular expression, it gets an error state.__
* Only applies to string-type settings.
* `error_value` (optional): __If the setting value is equal to this value, it will show a setting error.__
* This is used to display errors for non-numerical settings such as checkboxes.
* `warning_value` (optional): __If the setting value is equal to this value, it will show a setting warning.__
* This is used to display warnings for non-numerical settings such as checkboxes.

View File

@ -664,10 +664,22 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
cura_file_names = [name for name in archive.namelist() if name.startswith("Cura/")] cura_file_names = [name for name in archive.namelist() if name.startswith("Cura/")]
# Create a shadow copy of the preferences (we don't want all of the preferences, but we do want to re-use its # Create a shadow copy of the preferences (We don't want all of the preferences, but we do want to re-use its
# parsing code. # parsing code.
temp_preferences = Preferences() temp_preferences = Preferences()
serialized = archive.open("Cura/preferences.cfg").read().decode("utf-8") try:
serialized = archive.open("Cura/preferences.cfg").read().decode("utf-8")
except KeyError:
# If there is no preferences file, it's not a workspace, so notify user of failure.
Logger.log("w", "File %s is not a valid workspace.", file_name)
message = Message(i18n_catalog.i18nc("@info:error Don't translate the XML tags <filename> or <message>!",
"Project file <filename>{0}</filename> is corrupt: <message>{1}</message>.",
file_name, str(e)),
title=i18n_catalog.i18nc("@info:title", "Can't Open Project File"),
message_type=Message.MessageType.ERROR)
message.show()
self.setWorkspaceName("")
return [], {}
temp_preferences.deserialize(serialized) temp_preferences.deserialize(serialized)
# Copy a number of settings from the temp preferences to the global # Copy a number of settings from the temp preferences to the global

View File

@ -156,6 +156,7 @@ class ThreeMFWorkspaceWriter(WorkspaceWriter):
"connection_type", "connection_type",
"capabilities", "capabilities",
"octoprint_api_key", "octoprint_api_key",
"is_online",
} }
serialized_data = container.serialize(ignored_metadata_keys = ignore_keys) serialized_data = container.serialize(ignored_metadata_keys = ignore_keys)

View File

@ -1,9 +1,8 @@
# Copyright (c) 2019 Ultimaker B.V. # Copyright (c) 2022 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
import re # For escaping characters in the settings. import re # For escaping characters in the settings.
import json import json
import copy
from UM.Mesh.MeshWriter import MeshWriter from UM.Mesh.MeshWriter import MeshWriter
from UM.Logger import Logger from UM.Logger import Logger
@ -12,6 +11,8 @@ from UM.Settings.InstanceContainer import InstanceContainer
from cura.Machines.ContainerTree import ContainerTree from cura.Machines.ContainerTree import ContainerTree
from UM.i18n import i18nCatalog from UM.i18n import i18nCatalog
from cura.Settings.CuraStackBuilder import CuraStackBuilder
catalog = i18nCatalog("cura") catalog = i18nCatalog("cura")
@ -96,25 +97,6 @@ class GCodeWriter(MeshWriter):
self.setInformation(catalog.i18nc("@warning:status", "Please prepare G-code before exporting.")) self.setInformation(catalog.i18nc("@warning:status", "Please prepare G-code before exporting."))
return False return False
def _createFlattenedContainerInstance(self, instance_container1, instance_container2):
"""Create a new container with container 2 as base and container 1 written over it."""
flat_container = InstanceContainer(instance_container2.getName())
# The metadata includes id, name and definition
flat_container.setMetaData(copy.deepcopy(instance_container2.getMetaData()))
if instance_container1.getDefinition():
flat_container.setDefinition(instance_container1.getDefinition().getId())
for key in instance_container2.getAllKeys():
flat_container.setProperty(key, "value", instance_container2.getProperty(key, "value"))
for key in instance_container1.getAllKeys():
flat_container.setProperty(key, "value", instance_container1.getProperty(key, "value"))
return flat_container
def _serialiseSettings(self, stack): def _serialiseSettings(self, stack):
"""Serialises a container stack to prepare it for writing at the end of the g-code. """Serialises a container stack to prepare it for writing at the end of the g-code.
@ -145,22 +127,22 @@ class GCodeWriter(MeshWriter):
container_with_profile.setDefinition(machine_definition_id_for_quality) container_with_profile.setDefinition(machine_definition_id_for_quality)
container_with_profile.setMetaDataEntry("setting_version", stack.quality.getMetaDataEntry("setting_version")) container_with_profile.setMetaDataEntry("setting_version", stack.quality.getMetaDataEntry("setting_version"))
flat_global_container = self._createFlattenedContainerInstance(stack.userChanges, container_with_profile) merged_global_instance_container = InstanceContainer.createMergedInstanceContainer(stack.userChanges, container_with_profile)
# If the quality changes is not set, we need to set type manually # If the quality changes is not set, we need to set type manually
if flat_global_container.getMetaDataEntry("type", None) is None: if merged_global_instance_container.getMetaDataEntry("type", None) is None:
flat_global_container.setMetaDataEntry("type", "quality_changes") merged_global_instance_container.setMetaDataEntry("type", "quality_changes")
# Ensure that quality_type is set. (Can happen if we have empty quality changes). # Ensure that quality_type is set. (Can happen if we have empty quality changes).
if flat_global_container.getMetaDataEntry("quality_type", None) is None: if merged_global_instance_container.getMetaDataEntry("quality_type", None) is None:
flat_global_container.setMetaDataEntry("quality_type", stack.quality.getMetaDataEntry("quality_type", "normal")) merged_global_instance_container.setMetaDataEntry("quality_type", stack.quality.getMetaDataEntry("quality_type", "normal"))
# Get the machine definition ID for quality profiles # Get the machine definition ID for quality profiles
flat_global_container.setMetaDataEntry("definition", machine_definition_id_for_quality) merged_global_instance_container.setMetaDataEntry("definition", machine_definition_id_for_quality)
serialized = flat_global_container.serialize() serialized = merged_global_instance_container.serialize()
data = {"global_quality": serialized} data = {"global_quality": serialized}
all_setting_keys = flat_global_container.getAllKeys() all_setting_keys = merged_global_instance_container.getAllKeys()
for extruder in stack.extruderList: for extruder in stack.extruderList:
extruder_quality = extruder.qualityChanges extruder_quality = extruder.qualityChanges
if extruder_quality.getId() == "empty_quality_changes": if extruder_quality.getId() == "empty_quality_changes":
@ -174,7 +156,7 @@ class GCodeWriter(MeshWriter):
extruder_quality.setDefinition(machine_definition_id_for_quality) extruder_quality.setDefinition(machine_definition_id_for_quality)
extruder_quality.setMetaDataEntry("setting_version", stack.quality.getMetaDataEntry("setting_version")) extruder_quality.setMetaDataEntry("setting_version", stack.quality.getMetaDataEntry("setting_version"))
flat_extruder_quality = self._createFlattenedContainerInstance(extruder.userChanges, extruder_quality) flat_extruder_quality = InstanceContainer.createMergedInstanceContainer(extruder.userChanges, extruder_quality)
# If the quality changes is not set, we need to set type manually # If the quality changes is not set, we need to set type manually
if flat_extruder_quality.getMetaDataEntry("type", None) is None: if flat_extruder_quality.getMetaDataEntry("type", None) is None:
flat_extruder_quality.setMetaDataEntry("type", "quality_changes") flat_extruder_quality.setMetaDataEntry("type", "quality_changes")

View File

@ -1,4 +1,4 @@
// Copyright (c) 2018 Ultimaker B.V. // Copyright (c) 2022 Ultimaker B.V.
// Cura is released under the terms of the LGPLv3 or higher. // Cura is released under the terms of the LGPLv3 or higher.
import QtQuick 2.10 import QtQuick 2.10
@ -114,6 +114,7 @@ Rectangle
font: UM.Theme.getFont("medium") font: UM.Theme.getFont("medium")
width: contentWidth width: contentWidth
} }
Item Item
{ {
anchors anchors

View File

@ -1,6 +1,6 @@
# Copyright (c) 2021 Ultimaker B.V. # Copyright (c) 2022 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
import json
from typing import cast, List, Dict from typing import cast, List, Dict
from Charon.VirtualFile import VirtualFile # To open UFP files. from Charon.VirtualFile import VirtualFile # To open UFP files.
@ -10,6 +10,7 @@ from io import StringIO # For converting g-code to bytes.
from PyQt6.QtCore import QBuffer from PyQt6.QtCore import QBuffer
from UM.Application import Application
from UM.Logger import Logger from UM.Logger import Logger
from UM.Mesh.MeshWriter import MeshWriter # The writer we need to implement. from UM.Mesh.MeshWriter import MeshWriter # The writer we need to implement.
from UM.MimeTypeDatabase import MimeTypeDatabase, MimeType from UM.MimeTypeDatabase import MimeTypeDatabase, MimeType
@ -17,12 +18,16 @@ from UM.PluginRegistry import PluginRegistry # To get the g-code writer.
from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
from UM.Scene.SceneNode import SceneNode from UM.Scene.SceneNode import SceneNode
from UM.Settings.InstanceContainer import InstanceContainer
from cura.CuraApplication import CuraApplication from cura.CuraApplication import CuraApplication
from cura.Settings.CuraStackBuilder import CuraStackBuilder
from cura.Settings.GlobalStack import GlobalStack
from cura.Utils.Threading import call_on_qt_thread from cura.Utils.Threading import call_on_qt_thread
from UM.i18n import i18nCatalog from UM.i18n import i18nCatalog
METADATA_OBJECTS_PATH = "metadata/objects" METADATA_OBJECTS_PATH = "metadata/objects"
SLICE_METADATA_PATH = "Cura/slicemetadata.json"
catalog = i18nCatalog("cura") catalog = i18nCatalog("cura")
@ -67,7 +72,21 @@ class UFPWriter(MeshWriter):
try: try:
gcode = archive.getStream("/3D/model.gcode") gcode = archive.getStream("/3D/model.gcode")
gcode.write(gcode_textio.getvalue().encode("UTF-8")) gcode.write(gcode_textio.getvalue().encode("UTF-8"))
archive.addRelation(virtual_path = "/3D/model.gcode", relation_type = "http://schemas.ultimaker.org/package/2018/relationships/gcode") archive.addRelation(virtual_path = "/3D/model.gcode",
relation_type = "http://schemas.ultimaker.org/package/2018/relationships/gcode")
except EnvironmentError as e:
error_msg = catalog.i18nc("@info:error", "Can't write to UFP file:") + " " + str(e)
self.setInformation(error_msg)
Logger.error(error_msg)
return False
# Write settings
try:
archive.addContentType(extension="json", mime_type="application/json")
setting_textio = StringIO()
json.dump(self._getSliceMetadata(), setting_textio, separators=(", ", ": "), indent=4)
steam = archive.getStream(SLICE_METADATA_PATH)
steam.write(setting_textio.getvalue().encode("UTF-8"))
except EnvironmentError as e: except EnvironmentError as e:
error_msg = catalog.i18nc("@info:error", "Can't write to UFP file:") + " " + str(e) error_msg = catalog.i18nc("@info:error", "Can't write to UFP file:") + " " + str(e)
self.setInformation(error_msg) self.setInformation(error_msg)
@ -190,3 +209,47 @@ class UFPWriter(MeshWriter):
return [{"name": item.getName()} return [{"name": item.getName()}
for item in DepthFirstIterator(node) for item in DepthFirstIterator(node)
if item.getMeshData() is not None and not item.callDecoration("isNonPrintingMesh")] if item.getMeshData() is not None and not item.callDecoration("isNonPrintingMesh")]
def _getSliceMetadata(self) -> Dict[str, Dict[str, Dict[str, str]]]:
"""Get all changed settings and all settings. For each extruder and the global stack"""
print_information = CuraApplication.getInstance().getPrintInformation()
settings = {
"material": {
"length": print_information.materialLengths,
"weight": print_information.materialWeights,
"cost": print_information.materialCosts,
},
"global": {
"changes": {},
"all_settings": {},
}
}
global_stack = cast(GlobalStack, Application.getInstance().getGlobalContainerStack())
# Add global user or quality changes
global_flattened_changes = InstanceContainer.createMergedInstanceContainer(global_stack.userChanges, global_stack.qualityChanges)
for setting in global_flattened_changes.getAllKeys():
settings["global"]["changes"][setting] = global_flattened_changes.getProperty(setting, "value")
# Get global all settings values without user or quality changes
for setting in global_stack.getAllKeys():
settings["global"]["all_settings"][setting] = global_stack.getProperty(setting, "value")
for i, extruder in enumerate(global_stack.extruderList):
# Add extruder fields to settings dictionary
settings[f"extruder_{i}"] = {
"changes": {},
"all_settings": {},
}
# Add extruder user or quality changes
extruder_flattened_changes = InstanceContainer.createMergedInstanceContainer(extruder.userChanges, extruder.qualityChanges)
for setting in extruder_flattened_changes.getAllKeys():
settings[f"extruder_{i}"]["changes"][setting] = extruder_flattened_changes.getProperty(setting, "value")
# Get extruder all settings values without user or quality changes
for setting in extruder.getAllKeys():
settings[f"extruder_{i}"]["all_settings"][setting] = extruder.getProperty(setting, "value")
return settings

View File

@ -153,7 +153,7 @@ Item
MonitorPrinterPill MonitorPrinterPill
{ {
text: printJob.configuration.printerType text: printJob ? printJob.configuration.printerType : ""
} }
} }
} }
@ -172,8 +172,15 @@ Item
{ {
id: printerConfiguration id: printerConfiguration
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
buildplate: catalog.i18nc("@label", "Glass") buildplate: {
configurations: base.printJob.configuration.extruderConfigurations switch (printJob.assignedPrinter.buildplate) {
case "glass":
return catalog.i18nc("@label", "Glass");
default:
return null
}
}
configurations: base.printJob ? base.printJob.configuration.extruderConfigurations : null
height: Math.round(72 * screenScaleFactor) // TODO: Theme! height: Math.round(72 * screenScaleFactor) // TODO: Theme!
} }

View File

@ -175,7 +175,14 @@ Item
{ {
id: printerConfiguration id: printerConfiguration
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
buildplate: printer ? catalog.i18nc("@label", "Glass") : null // 'Glass' as a default buildplate: {
switch (printer.buildplate) {
case "glass":
return catalog.i18nc("@label", "Glass");
default:
return null
}
}
configurations: configurations:
{ {
var configs = [] var configs = []

View File

@ -1,8 +1,8 @@
// Copyright (c) 2019 Ultimaker B.V. // Copyright (c) 2022 Ultimaker B.V.
// Cura is released under the terms of the LGPLv3 or higher. // Cura is released under the terms of the LGPLv3 or higher.
import QtQuick 2.2 import QtQuick 2.15
import UM 1.3 as UM import UM 1.5 as UM
import Cura 1.0 as Cura import Cura 1.0 as Cura
// This is the root component for the monitor stage. // This is the root component for the monitor stage.
@ -37,6 +37,7 @@ Component
Item Item
{ {
id: printers id: printers
visible: !Cura.MachineManager.activeMachineIsAbstractCloudPrinter
anchors anchors
{ {
top: parent.top top: parent.top
@ -69,14 +70,66 @@ Component
top: printers.bottom top: printers.bottom
topMargin: 48 * screenScaleFactor // TODO: Theme! topMargin: 48 * screenScaleFactor // TODO: Theme!
} }
visible: OutputDevice.supportsPrintJobQueue && OutputDevice.canReadPrintJobs visible: OutputDevice.supportsPrintJobQueue && OutputDevice.canReadPrintJobs && !Cura.MachineManager.activeMachineIsAbstractCloudPrinter
} }
PrinterVideoStream PrinterVideoStream
{ {
anchors.fill: parent anchors.fill: parent
cameraUrl: OutputDevice.activeCameraUrl cameraUrl: OutputDevice.activeCameraUrl
visible: OutputDevice.activeCameraUrl != "" visible: OutputDevice.activeCameraUrl != "" && !Cura.MachineManager.activeMachineIsAbstractCloudPrinter
}
Rectangle
{
id: sendToFactoryCard
visible: Cura.MachineManager.activeMachineIsAbstractCloudPrinter
color: UM.Theme.getColor("monitor_stage_background")
height: childrenRect.height + UM.Theme.getSize("default_margin").height * 2
width: childrenRect.width + UM.Theme.getSize("wide_margin").width * 2
anchors
{
horizontalCenter: parent.horizontalCenter
top: parent.top
topMargin: UM.Theme.getSize("wide_margin").height * screenScaleFactor * 2
}
Column
{
anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter
spacing: UM.Theme.getSize("wide_margin").height
padding: UM.Theme.getSize("default_margin").width
topPadding: 0
Image
{
id: sendToFactoryImage
anchors.horizontalCenter: parent.horizontalCenter
source: UM.Theme.getImage("cura_connected_printers")
}
UM.Label
{
anchors.horizontalCenter: parent.horizontalCenter
text: catalog.i18nc("@info", "Monitor your printers from everywhere using Ultimaker Digital Factory")
font: UM.Theme.getFont("medium")
width: sendToFactoryImage.width
wrapMode: Text.WordWrap
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
}
Cura.PrimaryButton
{
id: sendToFactoryButton
anchors.horizontalCenter: parent.horizontalCenter
text: catalog.i18nc("@button", "View printers in Digital Factory")
onClicked: Qt.openUrlExternally("https://digitalfactory.ultimaker.com/app/print-jobs?utm_source=cura&utm_medium=software&utm_campaign=monitor-view-cloud-printer-type")
}
}
} }
} }
} }

View File

@ -0,0 +1,122 @@
from time import time
from typing import Callable, List, Optional
from PyQt6.QtCore import QObject, pyqtSlot
from PyQt6.QtNetwork import QNetworkReply
from UM import i18nCatalog
from UM.Logger import Logger
from UM.FileHandler.FileHandler import FileHandler
from UM.Resources import Resources
from UM.Scene.SceneNode import SceneNode
from cura.CuraApplication import CuraApplication
from cura.PrinterOutput.NetworkedPrinterOutputDevice import AuthState
from cura.PrinterOutput.PrinterOutputDevice import ConnectionType
from .CloudApiClient import CloudApiClient
from ..Models.Http.CloudClusterWithConfigResponse import CloudClusterWithConfigResponse
from ..UltimakerNetworkedPrinterOutputDevice import UltimakerNetworkedPrinterOutputDevice
I18N_CATALOG = i18nCatalog("cura")
class AbstractCloudOutputDevice(UltimakerNetworkedPrinterOutputDevice):
API_CHECK_INTERVAL = 10.0 # seconds
def __init__(self, api_client: CloudApiClient, printer_type: str, request_write_callback: Callable, refresh_callback: Callable, parent: QObject = None) -> None:
self._api = api_client
properties = {b"printer_type": printer_type.encode()}
super().__init__(
device_id=f"ABSTRACT_{printer_type}",
address="",
connection_type=ConnectionType.CloudConnection,
properties=properties,
parent=parent
)
self._on_print_dialog: Optional[QObject] = None
self._nodes: List[SceneNode] = None
self._request_write_callback = request_write_callback
self._refresh_callback = refresh_callback
self._setInterfaceElements()
def connect(self) -> None:
"""Connects this device."""
if self.isConnected():
return
Logger.log("i", "Attempting to connect AbstractCloudOutputDevice %s", self.key)
super().connect()
self._update()
def disconnect(self) -> None:
"""Disconnects the device"""
if not self.isConnected():
return
super().disconnect()
def _update(self) -> None:
"""Called when the network data should be updated."""
super()._update()
if time() - self._time_of_last_request < self.API_CHECK_INTERVAL:
return # avoid calling the cloud too often
self._time_of_last_request = time()
if self._api.account.isLoggedIn:
self.setAuthenticationState(AuthState.Authenticated)
self._last_request_time = time()
self._api.getClustersByMachineType(self.printerType, self._onCompleted, self._onError)
else:
self.setAuthenticationState(AuthState.NotAuthenticated)
def _setInterfaceElements(self) -> None:
"""Set all the interface elements and texts for this output device."""
self.setPriority(2) # Make sure we end up below the local networking and above 'save to file'.
self.setShortDescription(I18N_CATALOG.i18nc("@action:button", "Print via cloud"))
self.setDescription(I18N_CATALOG.i18nc("@properties:tooltip", "Print via cloud"))
self.setConnectionText(I18N_CATALOG.i18nc("@info:status", "Connected via cloud"))
def _onCompleted(self, clusters: List[CloudClusterWithConfigResponse]) -> None:
self._responseReceived()
all_configurations = []
for resp in clusters:
if resp.configuration is not None:
# Usually when the printer is offline, it doesn't have a configuration...
all_configurations.append(resp.configuration)
self._updatePrinters(all_configurations)
def _onError(self, reply: QNetworkReply, error: QNetworkReply.NetworkError) -> None:
Logger.log("w", f"Failed to get clusters by machine type: {str(error)}.")
@pyqtSlot(str)
def printerSelected(self, unique_id: str):
self._request_write_callback(unique_id, self._nodes)
if self._on_print_dialog:
self._on_print_dialog.close()
@pyqtSlot()
def refresh(self):
self._refresh_callback()
self._update()
def _openChoosePrinterDialog(self) -> None:
if self._on_print_dialog is None:
qml_path = Resources.getPath(CuraApplication.ResourceTypes.QmlFiles, "Dialogs", "ChoosePrinterDialog.qml")
self._on_print_dialog = CuraApplication.getInstance().createQmlComponent(qml_path, {})
if self._on_print_dialog is None: # Failed to load QML file.
return
self._on_print_dialog.setProperty("manager", self)
self._on_print_dialog.show()
def requestWrite(self, nodes: List[SceneNode], file_name: Optional[str] = None, limit_mimetypes: bool = False, file_handler: Optional[FileHandler] = None, **kwargs) -> None:
if not nodes or len(nodes) < 1:
Logger.log("w", "Nothing to print.")
return
self._nodes = nodes
self._openChoosePrinterDialog()

View File

@ -1,6 +1,7 @@
# Copyright (c) 2019 Ultimaker B.V. # Copyright (c) 2019 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
import json import json
import urllib.parse
from json import JSONDecodeError from json import JSONDecodeError
from time import time from time import time
from typing import Callable, List, Type, TypeVar, Union, Optional, Tuple, Dict, Any, cast from typing import Callable, List, Type, TypeVar, Union, Optional, Tuple, Dict, Any, cast
@ -17,6 +18,7 @@ from cura.UltimakerCloud import UltimakerCloudConstants
from cura.UltimakerCloud.UltimakerCloudScope import UltimakerCloudScope from cura.UltimakerCloud.UltimakerCloudScope import UltimakerCloudScope
from .ToolPathUploader import ToolPathUploader from .ToolPathUploader import ToolPathUploader
from ..Models.BaseModel import BaseModel from ..Models.BaseModel import BaseModel
from ..Models.Http.CloudClusterWithConfigResponse import CloudClusterWithConfigResponse
from ..Models.Http.CloudClusterResponse import CloudClusterResponse from ..Models.Http.CloudClusterResponse import CloudClusterResponse
from ..Models.Http.CloudClusterStatus import CloudClusterStatus from ..Models.Http.CloudClusterStatus import CloudClusterStatus
from ..Models.Http.CloudError import CloudError from ..Models.Http.CloudError import CloudError
@ -48,7 +50,6 @@ class CloudApiClient:
"""Initializes a new cloud API client. """Initializes a new cloud API client.
:param app: :param app:
:param account: The user's account object
:param on_error: The callback to be called whenever we receive errors from the server. :param on_error: The callback to be called whenever we receive errors from the server.
""" """
super().__init__() super().__init__()
@ -57,12 +58,11 @@ class CloudApiClient:
self._scope = JsonDecoratorScope(UltimakerCloudScope(app)) self._scope = JsonDecoratorScope(UltimakerCloudScope(app))
self._http = HttpRequestManager.getInstance() self._http = HttpRequestManager.getInstance()
self._on_error = on_error self._on_error = on_error
self._upload = None # type: Optional[ToolPathUploader] self._upload: Optional[ToolPathUploader] = None
@property @property
def account(self) -> Account: def account(self) -> Account:
"""Gets the account used for the API.""" """Gets the account used for the API."""
return self._account return self._account
def getClusters(self, on_finished: Callable[[List[CloudClusterResponse]], Any], failed: Callable) -> None: def getClusters(self, on_finished: Callable[[List[CloudClusterResponse]], Any], failed: Callable) -> None:
@ -71,13 +71,31 @@ class CloudApiClient:
:param on_finished: The function to be called after the result is parsed. :param on_finished: The function to be called after the result is parsed.
""" """
url = "{}/clusters?status=active".format(self.CLUSTER_API_ROOT) url = f"{self.CLUSTER_API_ROOT}/clusters?status=active"
self._http.get(url, self._http.get(url,
scope = self._scope, scope = self._scope,
callback = self._parseCallback(on_finished, CloudClusterResponse, failed), callback = self._parseCallback(on_finished, CloudClusterResponse, failed),
error_callback = failed, error_callback = failed,
timeout = self.DEFAULT_REQUEST_TIMEOUT) timeout = self.DEFAULT_REQUEST_TIMEOUT)
def getClustersByMachineType(self, machine_type, on_finished: Callable[[List[CloudClusterWithConfigResponse]], Any], failed: Callable) -> None:
# HACK: There is something weird going on with the API, as it reports printer types in formats like
# "ultimaker_s3", but wants "Ultimaker S3" when using the machine_variant filter query. So we need to do some
# conversion!
machine_type = machine_type.replace("_plus", "+")
machine_type = machine_type.replace("_", " ")
machine_type = machine_type.replace("ultimaker", "ultimaker ")
machine_type = machine_type.replace(" ", " ")
machine_type = machine_type.title()
machine_type = urllib.parse.quote_plus(machine_type)
url = f"{self.CLUSTER_API_ROOT}/clusters?machine_variant={machine_type}"
self._http.get(url,
scope=self._scope,
callback=self._parseCallback(on_finished, CloudClusterWithConfigResponse, failed),
error_callback=failed,
timeout=self.DEFAULT_REQUEST_TIMEOUT)
def getClusterStatus(self, cluster_id: str, on_finished: Callable[[CloudClusterStatus], Any]) -> None: def getClusterStatus(self, cluster_id: str, on_finished: Callable[[CloudClusterStatus], Any]) -> None:
"""Retrieves the status of the given cluster. """Retrieves the status of the given cluster.
@ -85,7 +103,7 @@ class CloudApiClient:
:param on_finished: The function to be called after the result is parsed. :param on_finished: The function to be called after the result is parsed.
""" """
url = "{}/clusters/{}/status".format(self.CLUSTER_API_ROOT, cluster_id) url = f"{self.CLUSTER_API_ROOT}/clusters/{cluster_id}/status"
self._http.get(url, self._http.get(url,
scope = self._scope, scope = self._scope,
callback = self._parseCallback(on_finished, CloudClusterStatus), callback = self._parseCallback(on_finished, CloudClusterStatus),
@ -100,7 +118,7 @@ class CloudApiClient:
:param on_finished: The function to be called after the result is parsed. :param on_finished: The function to be called after the result is parsed.
""" """
url = "{}/jobs/upload".format(self.CURA_API_ROOT) url = f"{self.CURA_API_ROOT}/jobs/upload"
data = json.dumps({"data": request.toDict()}).encode() data = json.dumps({"data": request.toDict()}).encode()
self._http.put(url, self._http.put(url,
@ -131,7 +149,7 @@ class CloudApiClient:
# specific to sending print jobs) such as lost connection, unparsable responses, etc. are not returned here, but # specific to sending print jobs) such as lost connection, unparsable responses, etc. are not returned here, but
# handled in a generic way by the CloudApiClient. # handled in a generic way by the CloudApiClient.
def requestPrint(self, cluster_id: str, job_id: str, on_finished: Callable[[CloudPrintResponse], Any], on_error) -> None: def requestPrint(self, cluster_id: str, job_id: str, on_finished: Callable[[CloudPrintResponse], Any], on_error) -> None:
url = "{}/clusters/{}/print/{}".format(self.CLUSTER_API_ROOT, cluster_id, job_id) url = f"{self.CLUSTER_API_ROOT}/clusters/{cluster_id}/print/{job_id}"
self._http.post(url, self._http.post(url,
scope = self._scope, scope = self._scope,
data = b"", data = b"",
@ -150,7 +168,7 @@ class CloudApiClient:
""" """
body = json.dumps({"data": data}).encode() if data else b"" body = json.dumps({"data": data}).encode() if data else b""
url = "{}/clusters/{}/print_jobs/{}/action/{}".format(self.CLUSTER_API_ROOT, cluster_id, cluster_job_id, action) url = f"{self.CLUSTER_API_ROOT}/clusters/{cluster_id}/print_jobs/{cluster_job_id}/action/{action}"
self._http.post(url, self._http.post(url,
scope = self._scope, scope = self._scope,
data = body, data = body,
@ -159,7 +177,7 @@ class CloudApiClient:
def _createEmptyRequest(self, path: str, content_type: Optional[str] = "application/json") -> QNetworkRequest: def _createEmptyRequest(self, path: str, content_type: Optional[str] = "application/json") -> QNetworkRequest:
"""We override _createEmptyRequest in order to add the user credentials. """We override _createEmptyRequest in order to add the user credentials.
:param url: The URL to request :param path: The URL to request
:param content_type: The type of the body contents. :param content_type: The type of the body contents.
""" """
@ -168,7 +186,7 @@ class CloudApiClient:
request.setHeader(QNetworkRequest.KnownHeaders.ContentTypeHeader, content_type) request.setHeader(QNetworkRequest.KnownHeaders.ContentTypeHeader, content_type)
access_token = self._account.accessToken access_token = self._account.accessToken
if access_token: if access_token:
request.setRawHeader(b"Authorization", "Bearer {}".format(access_token).encode()) request.setRawHeader(b"Authorization", f"Bearer {access_token}".encode())
return request return request
@staticmethod @staticmethod
@ -189,9 +207,9 @@ class CloudApiClient:
Logger.logException("e", "Could not parse the stardust response: %s", error.toDict()) Logger.logException("e", "Could not parse the stardust response: %s", error.toDict())
return status_code, {"errors": [error.toDict()]} return status_code, {"errors": [error.toDict()]}
def _parseModels(self, response: Dict[str, Any], on_finished: Union[Callable[[CloudApiClientModel], Any], def _parseResponse(self, response: Dict[str, Any], on_finished: Union[Callable[[CloudApiClientModel], Any],
Callable[[List[CloudApiClientModel]], Any]], model_class: Type[CloudApiClientModel]) -> None: Callable[[List[CloudApiClientModel]], Any]], model_class: Type[CloudApiClientModel]) -> None:
"""Parses the given models and calls the correct callback depending on the result. """Parses the given response and calls the correct callback depending on the result.
:param response: The response from the server, after being converted to a dict. :param response: The response from the server, after being converted to a dict.
:param on_finished: The callback in case the response is successful. :param on_finished: The callback in case the response is successful.
@ -200,7 +218,10 @@ class CloudApiClient:
if "data" in response: if "data" in response:
data = response["data"] data = response["data"]
if isinstance(data, list): if "status" in data and data["status"] == "wait_approval":
on_finished_empty = cast(Callable[[List], Any], on_finished)
on_finished_empty([])
elif isinstance(data, list):
results = [model_class(**c) for c in data] # type: List[CloudApiClientModel] results = [model_class(**c) for c in data] # type: List[CloudApiClientModel]
on_finished_list = cast(Callable[[List[CloudApiClientModel]], Any], on_finished) on_finished_list = cast(Callable[[List[CloudApiClientModel]], Any], on_finished)
on_finished_list(results) on_finished_list(results)
@ -242,7 +263,7 @@ class CloudApiClient:
if status_code >= 300 and on_error is not None: if status_code >= 300 and on_error is not None:
on_error() on_error()
else: else:
self._parseModels(response, on_finished, model) self._parseResponse(response, on_finished, model)
self._anti_gc_callbacks.append(parse) self._anti_gc_callbacks.append(parse)
return parse return parse

View File

@ -3,14 +3,13 @@
from time import time from time import time
import os import os
from typing import cast, List, Optional, TYPE_CHECKING from typing import cast, List, Optional
from PyQt6.QtCore import QObject, QUrl, pyqtProperty, pyqtSignal, pyqtSlot from PyQt6.QtCore import QObject, QUrl, pyqtProperty, pyqtSignal, pyqtSlot
from PyQt6.QtGui import QDesktopServices from PyQt6.QtGui import QDesktopServices
from PyQt6.QtNetwork import QNetworkReply, QNetworkRequest # Parse errors specific to print job uploading. from PyQt6.QtNetwork import QNetworkReply, QNetworkRequest # Parse errors specific to print job uploading.
from UM import i18nCatalog from UM import i18nCatalog
from UM.Backend.Backend import BackendState
from UM.FileHandler.FileHandler import FileHandler from UM.FileHandler.FileHandler import FileHandler
from UM.Logger import Logger from UM.Logger import Logger
from UM.Scene.SceneNode import SceneNode from UM.Scene.SceneNode import SceneNode
@ -18,9 +17,12 @@ from UM.Version import Version
from cura.CuraApplication import CuraApplication from cura.CuraApplication import CuraApplication
from cura.PrinterOutput.NetworkedPrinterOutputDevice import AuthState from cura.PrinterOutput.NetworkedPrinterOutputDevice import AuthState
from cura.PrinterOutput.PrinterOutputDevice import ConnectionType from cura.PrinterOutput.PrinterOutputDevice import ConnectionType
from cura.Scene.GCodeListDecorator import GCodeListDecorator
from cura.Scene.SliceableObjectDecorator import SliceableObjectDecorator
from .CloudApiClient import CloudApiClient from .CloudApiClient import CloudApiClient
from ..ExportFileJob import ExportFileJob from ..ExportFileJob import ExportFileJob
from ..Messages.PrintJobAwaitingApprovalMessage import PrintJobPendingApprovalMessage
from ..UltimakerNetworkedPrinterOutputDevice import UltimakerNetworkedPrinterOutputDevice from ..UltimakerNetworkedPrinterOutputDevice import UltimakerNetworkedPrinterOutputDevice
from ..Messages.PrintJobUploadBlockedMessage import PrintJobUploadBlockedMessage from ..Messages.PrintJobUploadBlockedMessage import PrintJobUploadBlockedMessage
from ..Messages.PrintJobUploadErrorMessage import PrintJobUploadErrorMessage from ..Messages.PrintJobUploadErrorMessage import PrintJobUploadErrorMessage
@ -41,7 +43,7 @@ I18N_CATALOG = i18nCatalog("cura")
class CloudOutputDevice(UltimakerNetworkedPrinterOutputDevice): class CloudOutputDevice(UltimakerNetworkedPrinterOutputDevice):
"""The cloud output device is a network output device that works remotely but has limited functionality. """The cloud output device is a network output device that works remotely but has limited functionality.
Currently it only supports viewing the printer and print job status and adding a new job to the queue. Currently, it only supports viewing the printer and print job status and adding a new job to the queue.
As such, those methods have been implemented here. As such, those methods have been implemented here.
Note that this device represents a single remote cluster, not a list of multiple clusters. Note that this device represents a single remote cluster, not a list of multiple clusters.
""" """
@ -58,7 +60,7 @@ class CloudOutputDevice(UltimakerNetworkedPrinterOutputDevice):
PRINT_JOB_ACTIONS_MIN_VERSION = Version("5.2.12") PRINT_JOB_ACTIONS_MIN_VERSION = Version("5.2.12")
# Notify can only use signals that are defined by the class that they are in, not inherited ones. # Notify can only use signals that are defined by the class that they are in, not inherited ones.
# Therefore we create a private signal used to trigger the printersChanged signal. # Therefore, we create a private signal used to trigger the printersChanged signal.
_cloudClusterPrintersChanged = pyqtSignal() _cloudClusterPrintersChanged = pyqtSignal()
def __init__(self, api_client: CloudApiClient, cluster: CloudClusterResponse, parent: QObject = None) -> None: def __init__(self, api_client: CloudApiClient, cluster: CloudClusterResponse, parent: QObject = None) -> None:
@ -109,6 +111,9 @@ class CloudOutputDevice(UltimakerNetworkedPrinterOutputDevice):
self._pre_upload_print_job = None # type: Optional[CloudPrintJobResponse] self._pre_upload_print_job = None # type: Optional[CloudPrintJobResponse]
self._uploaded_print_job = None # type: Optional[CloudPrintJobResponse] self._uploaded_print_job = None # type: Optional[CloudPrintJobResponse]
CuraApplication.getInstance().getBackend().backendDone.connect(self._resetPrintJob)
CuraApplication.getInstance().getController().getScene().sceneChanged.connect(self._onSceneChanged)
def connect(self) -> None: def connect(self) -> None:
"""Connects this device.""" """Connects this device."""
@ -116,8 +121,6 @@ class CloudOutputDevice(UltimakerNetworkedPrinterOutputDevice):
return return
Logger.log("i", "Attempting to connect to cluster %s", self.key) Logger.log("i", "Attempting to connect to cluster %s", self.key)
super().connect() super().connect()
CuraApplication.getInstance().getBackend().backendStateChange.connect(self._onBackendStateChange)
self._update() self._update()
def disconnect(self) -> None: def disconnect(self) -> None:
@ -127,11 +130,14 @@ class CloudOutputDevice(UltimakerNetworkedPrinterOutputDevice):
return return
super().disconnect() super().disconnect()
Logger.log("i", "Disconnected from cluster %s", self.key) Logger.log("i", "Disconnected from cluster %s", self.key)
CuraApplication.getInstance().getBackend().backendStateChange.disconnect(self._onBackendStateChange)
def _onBackendStateChange(self, _: BackendState) -> None: def _onSceneChanged(self, node: SceneNode):
"""Resets the print job that was uploaded to force a new upload, runs whenever the user re-slices.""" # This will reset the print job if a ufp file is loaded. This forces a new upload when printing via cloud from ufp.
if node.getDecorator(GCodeListDecorator) or node.getDecorator(SliceableObjectDecorator):
self._resetPrintJob()
def _resetPrintJob(self) -> None:
"""Resets the print job that was uploaded to force a new upload, runs whenever slice finishes."""
self._tool_path = None self._tool_path = None
self._pre_upload_print_job = None self._pre_upload_print_job = None
self._uploaded_print_job = None self._uploaded_print_job = None
@ -202,7 +208,8 @@ class CloudOutputDevice(UltimakerNetworkedPrinterOutputDevice):
# Note that self.writeFinished is called in _onPrintUploadCompleted as well. # Note that self.writeFinished is called in _onPrintUploadCompleted as well.
if self._uploaded_print_job: if self._uploaded_print_job:
Logger.log("i", "Current mesh is already attached to a print-job, immediately request reprint.") Logger.log("i", "Current mesh is already attached to a print-job, immediately request reprint.")
self._api.requestPrint(self.key, self._uploaded_print_job.job_id, self._onPrintUploadCompleted, self._onPrintUploadSpecificError) self._api.requestPrint(self.key, self._uploaded_print_job.job_id, self._onPrintUploadCompleted,
self._onPrintUploadSpecificError)
return return
# Export the scene to the correct file type. # Export the scene to the correct file type.
@ -230,6 +237,7 @@ class CloudOutputDevice(UltimakerNetworkedPrinterOutputDevice):
:param job_response: The response received from the cloud API. :param job_response: The response received from the cloud API.
""" """
if not self._tool_path: if not self._tool_path:
return self._onUploadError() return self._onUploadError()
self._pre_upload_print_job = job_response # store the last uploaded job to prevent re-upload of the same file self._pre_upload_print_job = job_response # store the last uploaded job to prevent re-upload of the same file
@ -244,12 +252,15 @@ class CloudOutputDevice(UltimakerNetworkedPrinterOutputDevice):
self._progress.update(100) self._progress.update(100)
print_job = cast(CloudPrintJobResponse, self._pre_upload_print_job) print_job = cast(CloudPrintJobResponse, self._pre_upload_print_job)
if not print_job: # It's possible that another print job is requested in the meanwhile, which then fails to upload with an error, which sets self._pre_uploaded_print_job to `None`. if not print_job:
# It's possible that another print job is requested in the meanwhile, which then fails to upload with an
# error, which sets self._pre_uploaded_print_job to `None`.
self._pre_upload_print_job = None self._pre_upload_print_job = None
self._uploaded_print_job = None self._uploaded_print_job = None
Logger.log("w", "Interference from another job uploaded at roughly the same time, not uploading print!") Logger.log("w", "Interference from another job uploaded at roughly the same time, not uploading print!")
return # Prevent a crash. return # Prevent a crash.
self._api.requestPrint(self.key, print_job.job_id, self._onPrintUploadCompleted, self._onPrintUploadSpecificError) self._api.requestPrint(self.key, print_job.job_id, self._onPrintUploadCompleted,
self._onPrintUploadSpecificError)
def _onPrintUploadCompleted(self, response: CloudPrintResponse) -> None: def _onPrintUploadCompleted(self, response: CloudPrintResponse) -> None:
"""Shows a message when the upload has succeeded """Shows a message when the upload has succeeded
@ -258,16 +269,21 @@ class CloudOutputDevice(UltimakerNetworkedPrinterOutputDevice):
""" """
self._uploaded_print_job = self._pre_upload_print_job self._uploaded_print_job = self._pre_upload_print_job
self._progress.hide() self._progress.hide()
message = PrintJobUploadSuccessMessage()
message.addAction("monitor print",
name=I18N_CATALOG.i18nc("@action:button", "Monitor print"),
icon="",
description=I18N_CATALOG.i18nc("@action:tooltip", "Track the print in Ultimaker Digital Factory"),
button_align=message.ActionButtonAlignment.ALIGN_RIGHT)
df_url = f"https://digitalfactory.ultimaker.com/app/jobs/{self._cluster.cluster_id}?utm_source=cura&utm_medium=software&utm_campaign=message-printjob-sent"
message.pyQtActionTriggered.connect(lambda message, action: (QDesktopServices.openUrl(QUrl(df_url)), message.hide()))
message.show() if response:
message = PrintJobUploadSuccessMessage()
message.addAction("monitor print",
name=I18N_CATALOG.i18nc("@action:button", "Monitor print"),
icon="",
description=I18N_CATALOG.i18nc("@action:tooltip", "Track the print in Ultimaker Digital Factory"),
button_align=message.ActionButtonAlignment.ALIGN_RIGHT)
df_url = f"https://digitalfactory.ultimaker.com/app/jobs/{self._cluster.cluster_id}?utm_source=cura&utm_medium=software&utm_campaign=message-printjob-sent"
message.pyQtActionTriggered.connect(lambda message, action: (QDesktopServices.openUrl(QUrl(df_url)), message.hide()))
message.show()
else:
PrintJobPendingApprovalMessage(self._cluster.cluster_id).show()
self.writeFinished.emit() self.writeFinished.emit()
def _onPrintUploadSpecificError(self, reply: "QNetworkReply", _: "QNetworkReply.NetworkError"): def _onPrintUploadSpecificError(self, reply: "QNetworkReply", _: "QNetworkReply.NetworkError"):
@ -278,7 +294,9 @@ class CloudOutputDevice(UltimakerNetworkedPrinterOutputDevice):
if error_code == 409: if error_code == 409:
PrintJobUploadQueueFullMessage().show() PrintJobUploadQueueFullMessage().show()
else: else:
PrintJobUploadErrorMessage(I18N_CATALOG.i18nc("@error:send", "Unknown error code when uploading print job: {0}", error_code)).show() PrintJobUploadErrorMessage(I18N_CATALOG.i18nc("@error:send",
"Unknown error code when uploading print job: {0}",
error_code)).show()
Logger.log("w", "Upload of print job failed specifically with error code {}".format(error_code)) Logger.log("w", "Upload of print job failed specifically with error code {}".format(error_code))
@ -336,11 +354,13 @@ class CloudOutputDevice(UltimakerNetworkedPrinterOutputDevice):
@pyqtSlot(name="openPrintJobControlPanel") @pyqtSlot(name="openPrintJobControlPanel")
def openPrintJobControlPanel(self) -> None: def openPrintJobControlPanel(self) -> None:
QDesktopServices.openUrl(QUrl(self.clusterCloudUrl + "?utm_source=cura&utm_medium=software&utm_campaign=monitor-manage-browser")) QDesktopServices.openUrl(QUrl(f"{self.clusterCloudUrl}?utm_source=cura&utm_medium=software&"
f"utm_campaign=monitor-manage-browser"))
@pyqtSlot(name="openPrinterControlPanel") @pyqtSlot(name="openPrinterControlPanel")
def openPrinterControlPanel(self) -> None: def openPrinterControlPanel(self) -> None:
QDesktopServices.openUrl(QUrl(self.clusterCloudUrl + "?utm_source=cura&utm_medium=software&utm_campaign=monitor-manage-printer")) QDesktopServices.openUrl(QUrl(f"{self.clusterCloudUrl}?utm_source=cura&utm_medium=software"
f"&utm_campaign=monitor-manage-printer"))
permissionsChanged = pyqtSignal() permissionsChanged = pyqtSignal()
@ -362,7 +382,7 @@ class CloudOutputDevice(UltimakerNetworkedPrinterOutputDevice):
@pyqtProperty(bool, notify = permissionsChanged) @pyqtProperty(bool, notify = permissionsChanged)
def canWriteOwnPrintJobs(self) -> bool: def canWriteOwnPrintJobs(self) -> bool:
""" """
Whether this user can change things about print jobs made by themself. Whether this user can change things about print jobs made by them.
""" """
return "digital-factory.print-job.write.own" in self._account.permissions return "digital-factory.print-job.write.own" in self._account.permissions
@ -390,4 +410,8 @@ class CloudOutputDevice(UltimakerNetworkedPrinterOutputDevice):
"""Gets the URL on which to monitor the cluster via the cloud.""" """Gets the URL on which to monitor the cluster via the cloud."""
root_url_prefix = "-staging" if self._account.is_staging else "" root_url_prefix = "-staging" if self._account.is_staging else ""
return "https://digitalfactory{}.ultimaker.com/app/jobs/{}".format(root_url_prefix, self.clusterData.cluster_id) return f"https://digitalfactory{root_url_prefix}.ultimaker.com/app/jobs/{self.clusterData.cluster_id}"
def __del__(self):
CuraApplication.getInstance().getBackend().backendDone.disconnect(self._resetPrintJob)
CuraApplication.getInstance().getController().getScene().sceneChanged.disconnect(self._onSceneChanged)

View File

@ -9,7 +9,6 @@ from PyQt6.QtWidgets import QMessageBox
from UM import i18nCatalog from UM import i18nCatalog
from UM.Logger import Logger # To log errors talking to the API. from UM.Logger import Logger # To log errors talking to the API.
from UM.Message import Message
from UM.Settings.Interfaces import ContainerInterface from UM.Settings.Interfaces import ContainerInterface
from UM.Signal import Signal from UM.Signal import Signal
from UM.Util import parseBool from UM.Util import parseBool
@ -20,16 +19,19 @@ from cura.Settings.CuraContainerRegistry import CuraContainerRegistry # To upda
from cura.Settings.CuraStackBuilder import CuraStackBuilder from cura.Settings.CuraStackBuilder import CuraStackBuilder
from cura.Settings.GlobalStack import GlobalStack from cura.Settings.GlobalStack import GlobalStack
from cura.UltimakerCloud.UltimakerCloudConstants import META_CAPABILITIES, META_UM_LINKED_TO_ACCOUNT from cura.UltimakerCloud.UltimakerCloudConstants import META_CAPABILITIES, META_UM_LINKED_TO_ACCOUNT
from .AbstractCloudOutputDevice import AbstractCloudOutputDevice
from .CloudApiClient import CloudApiClient from .CloudApiClient import CloudApiClient
from .CloudOutputDevice import CloudOutputDevice from .CloudOutputDevice import CloudOutputDevice
from ..Messages.RemovedPrintersMessage import RemovedPrintersMessage
from ..Models.Http.CloudClusterResponse import CloudClusterResponse from ..Models.Http.CloudClusterResponse import CloudClusterResponse
from ..Messages.NewPrinterDetectedMessage import NewPrinterDetectedMessage
class CloudOutputDeviceManager: class CloudOutputDeviceManager:
"""The cloud output device manager is responsible for using the Ultimaker Cloud APIs to manage remote clusters. """The cloud output device manager is responsible for using the Ultimaker Cloud APIs to manage remote clusters.
Keeping all cloud related logic in this class instead of the UM3OutputDevicePlugin results in more readable code. Keeping all cloud related logic in this class instead of the UM3OutputDevicePlugin results in more readable code.
API spec is available on https://api.ultimaker.com/docs/connect/spec/. API spec is available on https://docs.api.ultimaker.com/connect/index.html.
""" """
META_CLUSTER_ID = "um_cloud_cluster_id" META_CLUSTER_ID = "um_cloud_cluster_id"
@ -46,21 +48,22 @@ class CloudOutputDeviceManager:
def __init__(self) -> None: def __init__(self) -> None:
# Persistent dict containing the remote clusters for the authenticated user. # Persistent dict containing the remote clusters for the authenticated user.
self._remote_clusters = {} # type: Dict[str, CloudOutputDevice] self._remote_clusters: Dict[str, CloudOutputDevice] = {}
self._abstract_clusters: Dict[str, AbstractCloudOutputDevice] = {}
# Dictionary containing all the cloud printers loaded in Cura # Dictionary containing all the cloud printers loaded in Cura
self._um_cloud_printers = {} # type: Dict[str, GlobalStack] self._um_cloud_printers: Dict[str, GlobalStack] = {}
self._account = CuraApplication.getInstance().getCuraAPI().account # type: Account self._account: Account = CuraApplication.getInstance().getCuraAPI().account
self._api = CloudApiClient(CuraApplication.getInstance(), on_error = lambda error: Logger.log("e", str(error))) self._api = CloudApiClient(CuraApplication.getInstance(), on_error = lambda error: Logger.log("e", str(error)))
self._account.loginStateChanged.connect(self._onLoginStateChanged) self._account.loginStateChanged.connect(self._onLoginStateChanged)
self._removed_printers_message = None # type: Optional[Message] self._removed_printers_message: Optional[RemovedPrintersMessage] = None
# Ensure we don't start twice. # Ensure we don't start twice.
self._running = False self._running = False
self._syncing = False self._syncing = False
CuraApplication.getInstance().getContainerRegistry().containerRemoved.connect(self._printerRemoved) CuraApplication.getInstance().getContainerRegistry().containerRemoved.connect(self._printerRemoved)
def start(self): def start(self):
@ -113,8 +116,8 @@ class CloudOutputDeviceManager:
CuraApplication.getInstance().getContainerRegistry().findContainerStacks( CuraApplication.getInstance().getContainerRegistry().findContainerStacks(
type = "machine") if m.getMetaDataEntry(self.META_CLUSTER_ID, None)} type = "machine") if m.getMetaDataEntry(self.META_CLUSTER_ID, None)}
new_clusters = [] new_clusters = []
all_clusters = {c.cluster_id: c for c in clusters} # type: Dict[str, CloudClusterResponse] all_clusters: Dict[str, CloudClusterResponse] = {c.cluster_id: c for c in clusters}
online_clusters = {c.cluster_id: c for c in clusters if c.is_online} # type: Dict[str, CloudClusterResponse] online_clusters: Dict[str, CloudClusterResponse] = {c.cluster_id: c for c in clusters if c.is_online}
# Add the new printers in Cura. # Add the new printers in Cura.
for device_id, cluster_data in all_clusters.items(): for device_id, cluster_data in all_clusters.items():
@ -130,8 +133,11 @@ class CloudOutputDeviceManager:
self._um_cloud_printers[device_id].setMetaDataEntry(META_UM_LINKED_TO_ACCOUNT, True) self._um_cloud_printers[device_id].setMetaDataEntry(META_UM_LINKED_TO_ACCOUNT, True)
if not self._um_cloud_printers[device_id].getMetaDataEntry(META_CAPABILITIES, None): if not self._um_cloud_printers[device_id].getMetaDataEntry(META_CAPABILITIES, None):
self._um_cloud_printers[device_id].setMetaDataEntry(META_CAPABILITIES, ",".join(cluster_data.capabilities)) self._um_cloud_printers[device_id].setMetaDataEntry(META_CAPABILITIES, ",".join(cluster_data.capabilities))
self._onDevicesDiscovered(new_clusters)
# We want a machine stack per remote printer that we discovered. Create them now!
self._createMachineStacksForDiscoveredClusters(new_clusters)
# Update the online vs offline status for all found devices
self._updateOnlinePrinters(all_clusters) self._updateOnlinePrinters(all_clusters)
# Hide the current removed_printers_message, if there is any # Hide the current removed_printers_message, if there is any
@ -152,6 +158,7 @@ class CloudOutputDeviceManager:
if new_clusters or offline_device_keys or removed_device_keys: if new_clusters or offline_device_keys or removed_device_keys:
self.discoveredDevicesChanged.emit() self.discoveredDevicesChanged.emit()
if offline_device_keys: if offline_device_keys:
# If the removed device was active we should connect to the new active device # If the removed device was active we should connect to the new active device
self._connectToActiveMachine() self._connectToActiveMachine()
@ -165,54 +172,72 @@ class CloudOutputDeviceManager:
self._syncing = False self._syncing = False
self._account.setSyncState(self.SYNC_SERVICE_NAME, SyncState.ERROR) self._account.setSyncState(self.SYNC_SERVICE_NAME, SyncState.ERROR)
def _onDevicesDiscovered(self, clusters: List[CloudClusterResponse]) -> None: def _requestWrite(self, unique_id: str, nodes: List["SceneNode"]):
for remote in self._remote_clusters.values():
if unique_id == remote.name: # No other id-type would match. Assume cloud doesn't have duplicate names.
remote.requestWrite(nodes)
return
Logger.log("e", f"Failed writing to specific cloud printer: {unique_id} not in remote clusters.")
def _createMachineStacksForDiscoveredClusters(self, discovered_clusters: List[CloudClusterResponse]) -> None:
"""**Synchronously** create machines for discovered devices """**Synchronously** create machines for discovered devices
Any new machines are made available to the user. Any new machines are made available to the user.
May take a long time to complete. As this code needs access to the Application May take a long time to complete. This currently forcefully calls the "processEvents", which isn't
and blocks the GIL, creating a Job for this would not make sense. the nicest solution out there. We might need to consider moving this into a job later!
Shows a Message informing the user of progress.
""" """
new_devices = [] new_output_devices: List[CloudOutputDevice] = []
remote_clusters_added = False remote_clusters_added = False
host_guid_map = {machine.getMetaDataEntry(self.META_HOST_GUID): device_cluster_id
for device_cluster_id, machine in self._um_cloud_printers.items() # Create a map that maps the HOST_GUID to the DEVICE_CLUSTER_ID
if machine.getMetaDataEntry(self.META_HOST_GUID)} host_guid_map: Dict[str, str] = {machine.getMetaDataEntry(self.META_HOST_GUID): device_cluster_id
for device_cluster_id, machine in self._um_cloud_printers.items()
if machine.getMetaDataEntry(self.META_HOST_GUID)}
machine_manager = CuraApplication.getInstance().getMachineManager() machine_manager = CuraApplication.getInstance().getMachineManager()
for cluster_data in clusters: for cluster_data in discovered_clusters:
device = CloudOutputDevice(self._api, cluster_data) output_device = CloudOutputDevice(self._api, cluster_data)
if cluster_data.printer_type not in self._abstract_clusters:
self._abstract_clusters[cluster_data.printer_type] = AbstractCloudOutputDevice(self._api, cluster_data.printer_type, self._requestWrite, self.refreshConnections)
# Ensure that the abstract machine is added (either because it was never added, or it somehow got
# removed)
_abstract_machine = CuraStackBuilder.createAbstractMachine(cluster_data.printer_type)
# If the machine already existed before, it will be present in the host_guid_map # If the machine already existed before, it will be present in the host_guid_map
if cluster_data.host_guid in host_guid_map: if cluster_data.host_guid in host_guid_map:
machine = machine_manager.getMachine(device.printerType, {self.META_HOST_GUID: cluster_data.host_guid}) machine = machine_manager.getMachine(output_device.printerType, {self.META_HOST_GUID: cluster_data.host_guid})
if machine and machine.getMetaDataEntry(self.META_CLUSTER_ID) != device.key: if machine and machine.getMetaDataEntry(self.META_CLUSTER_ID) != output_device.key:
# If the retrieved device has a different cluster_id than the existing machine, bring the existing # If the retrieved device has a different cluster_id than the existing machine, bring the existing
# machine up-to-date. # machine up-to-date.
self._updateOutdatedMachine(outdated_machine = machine, new_cloud_output_device = device) self._updateOutdatedMachine(outdated_machine = machine, new_cloud_output_device = output_device)
# Create a machine if we don't already have it. Do not make it the active machine. # Create a machine if we don't already have it. Do not make it the active machine.
# We only need to add it if it wasn't already added by "local" network or by cloud. # We only need to add it if it wasn't already added by "local" network or by cloud.
if machine_manager.getMachine(device.printerType, {self.META_CLUSTER_ID: device.key}) is None \ if machine_manager.getMachine(output_device.printerType, {self.META_CLUSTER_ID: output_device.key}) is None \
and machine_manager.getMachine(device.printerType, {self.META_NETWORK_KEY: cluster_data.host_name + "*"}) is None: # The host name is part of the network key. and machine_manager.getMachine(output_device.printerType, {self.META_NETWORK_KEY: cluster_data.host_name + "*"}) is None: # The host name is part of the network key.
new_devices.append(device) new_output_devices.append(output_device)
elif device.getId() not in self._remote_clusters: elif output_device.getId() not in self._remote_clusters:
self._remote_clusters[device.getId()] = device self._remote_clusters[output_device.getId()] = output_device
remote_clusters_added = True remote_clusters_added = True
# If a printer that was removed from the account is re-added, change its metadata to mark it not removed # If a printer that was removed from the account is re-added, change its metadata to mark it not removed
# from the account # from the account
elif not parseBool(self._um_cloud_printers[device.key].getMetaDataEntry(META_UM_LINKED_TO_ACCOUNT, "true")): elif not parseBool(self._um_cloud_printers[output_device.key].getMetaDataEntry(META_UM_LINKED_TO_ACCOUNT, "true")):
self._um_cloud_printers[device.key].setMetaDataEntry(META_UM_LINKED_TO_ACCOUNT, True) self._um_cloud_printers[output_device.key].setMetaDataEntry(META_UM_LINKED_TO_ACCOUNT, True)
# As adding a lot of machines might take some time, ensure that the GUI (and progress message) is updated
CuraApplication.getInstance().processEvents()
# Inform the Cloud printers model about new devices. # Inform the Cloud printers model about new devices.
new_devices_list_of_dicts = [{ new_devices_list_of_dicts = [{
"key": d.getId(), "key": d.getId(),
"name": d.name, "name": d.name,
"machine_type": d.printerTypeName, "machine_type": d.printerTypeName,
"firmware_version": d.firmwareVersion} for d in new_devices] "firmware_version": d.firmwareVersion} for d in new_output_devices]
discovered_cloud_printers_model = CuraApplication.getInstance().getDiscoveredCloudPrintersModel() discovered_cloud_printers_model = CuraApplication.getInstance().getDiscoveredCloudPrintersModel()
discovered_cloud_printers_model.addDiscoveredCloudPrinters(new_devices_list_of_dicts) discovered_cloud_printers_model.addDiscoveredCloudPrinters(new_devices_list_of_dicts)
if not new_devices: if not new_output_devices:
if remote_clusters_added: if remote_clusters_added:
self._connectToActiveMachine() self._connectToActiveMachine()
return return
@ -220,55 +245,29 @@ class CloudOutputDeviceManager:
# Sort new_devices on online status first, alphabetical second. # Sort new_devices on online status first, alphabetical second.
# Since the first device might be activated in case there is no active printer yet, # Since the first device might be activated in case there is no active printer yet,
# it would be nice to prioritize online devices # it would be nice to prioritize online devices
online_cluster_names = {c.friendly_name.lower() for c in clusters if c.is_online and not c.friendly_name is None} online_cluster_names = {c.friendly_name.lower() for c in discovered_clusters if c.is_online and not c.friendly_name is None}
new_devices.sort(key = lambda x: ("a{}" if x.name.lower() in online_cluster_names else "b{}").format(x.name.lower())) new_output_devices.sort(key = lambda x: ("a{}" if x.name.lower() in online_cluster_names else "b{}").format(x.name.lower()))
message = Message( message = NewPrinterDetectedMessage(num_printers_found = len(new_output_devices))
title = self.i18n_catalog.i18ncp(
"info:status",
"New printer detected from your Ultimaker account",
"New printers detected from your Ultimaker account",
len(new_devices)
),
progress = 0,
lifetime = 0,
message_type = Message.MessageType.POSITIVE
)
message.show() message.show()
new_devices_added = [] new_devices_added = []
for idx, device in enumerate(new_devices): for idx, output_device in enumerate(new_output_devices):
message_text = self.i18n_catalog.i18nc("info:status Filled in with printer name and printer model.", "Adding printer {name} ({model}) from your account").format(name = device.name, model = device.printerTypeName) message.updateProgressText(output_device)
message.setText(message_text)
if len(new_devices) > 1: self._remote_clusters[output_device.getId()] = output_device
message.setProgress((idx / len(new_devices)) * 100)
CuraApplication.getInstance().processEvents()
self._remote_clusters[device.getId()] = device
# If there is no active machine, activate the first available cloud printer # If there is no active machine, activate the first available cloud printer
activate = not CuraApplication.getInstance().getMachineManager().activeMachine activate = not CuraApplication.getInstance().getMachineManager().activeMachine
if self._createMachineFromDiscoveredDevice(device.getId(), activate = activate): if self._createMachineFromDiscoveredDevice(output_device.getId(), activate = activate):
new_devices_added.append(device) new_devices_added.append(output_device)
message.setProgress(None) message.finalize(new_devices_added, new_output_devices)
max_disp_devices = 3 @staticmethod
if len(new_devices_added) > max_disp_devices: def _updateOnlinePrinters(printer_responses: Dict[str, CloudClusterResponse]) -> None:
num_hidden = len(new_devices_added) - max_disp_devices
device_name_list = ["<li>{} ({})</li>".format(device.name, device.printerTypeName) for device in new_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:") + "<ul>" + device_names + "</ul>"
message.setText(message_text)
else:
message.hide()
def _updateOnlinePrinters(self, printer_responses: Dict[str, CloudClusterResponse]) -> None:
""" """
Update the metadata of the printers to store whether they are online or not. Update the metadata of the printers to store whether they are online or not.
:param printer_responses: The responses received from the API about the printer statuses. :param printer_responses: The responses received from the API about the printer statuses.
@ -291,7 +290,8 @@ class CloudOutputDeviceManager:
old_cluster_id = outdated_machine.getMetaDataEntry(self.META_CLUSTER_ID) old_cluster_id = outdated_machine.getMetaDataEntry(self.META_CLUSTER_ID)
outdated_machine.setMetaDataEntry(self.META_CLUSTER_ID, new_cloud_output_device.key) outdated_machine.setMetaDataEntry(self.META_CLUSTER_ID, new_cloud_output_device.key)
outdated_machine.setMetaDataEntry(META_UM_LINKED_TO_ACCOUNT, True) outdated_machine.setMetaDataEntry(META_UM_LINKED_TO_ACCOUNT, True)
# Cleanup the remainings of the old CloudOutputDevice(old_cluster_id)
# Cleanup the remains of the old CloudOutputDevice(old_cluster_id)
self._um_cloud_printers[new_cloud_output_device.key] = self._um_cloud_printers.pop(old_cluster_id) self._um_cloud_printers[new_cloud_output_device.key] = self._um_cloud_printers.pop(old_cluster_id)
output_device_manager = CuraApplication.getInstance().getOutputDeviceManager() output_device_manager = CuraApplication.getInstance().getOutputDeviceManager()
if old_cluster_id in output_device_manager.getOutputDeviceIds(): if old_cluster_id in output_device_manager.getOutputDeviceIds():
@ -321,56 +321,19 @@ class CloudOutputDeviceManager:
for device_id in removed_device_ids: for device_id in removed_device_ids:
if not parseBool(self._um_cloud_printers[device_id].getMetaDataEntry(META_UM_LINKED_TO_ACCOUNT, "true")): if not parseBool(self._um_cloud_printers[device_id].getMetaDataEntry(META_UM_LINKED_TO_ACCOUNT, "true")):
ignored_device_ids.add(device_id) ignored_device_ids.add(device_id)
# Keep the reported_device_ids list in a class variable, so that the message button actions can access it and # Keep the reported_device_ids list in a class variable, so that the message button actions can access it and
# take the necessary steps to fulfill their purpose. # take the necessary steps to fulfill their purpose.
self.reported_device_ids = removed_device_ids - ignored_device_ids self.reported_device_ids = removed_device_ids - ignored_device_ids
if not self.reported_device_ids: if not self.reported_device_ids:
return return
# Generate message
self._removed_printers_message = Message(
title = self.i18n_catalog.i18ncp(
"info:status",
"A cloud connection is not available for a printer",
"A cloud connection is not available for some printers",
len(self.reported_device_ids)
),
message_type = Message.MessageType.WARNING
)
device_names = "".join(["<li>{} ({})</li>".format(self._um_cloud_printers[device].name, self._um_cloud_printers[device].definition.name) for device in self.reported_device_ids])
message_text = self.i18n_catalog.i18ncp(
"info:status",
"This printer is not linked to the Digital Factory:",
"These printers are not linked to the Digital Factory:",
len(self.reported_device_ids)
)
message_text += "<br/><ul>{}</ul><br/>".format(device_names)
digital_factory_string = self.i18n_catalog.i18nc("info:name", "Ultimaker Digital Factory")
message_text += self.i18n_catalog.i18nc(
"info:status",
"To establish a connection, please visit the {website_link}".format(website_link = "<a href='https://digitalfactory.ultimaker.com?utm_source=cura&utm_medium=software&utm_campaign=change-account-connect-printer'>{}</a>.".format(digital_factory_string))
)
self._removed_printers_message.setText(message_text)
self._removed_printers_message.addAction("keep_printer_configurations_action",
name = self.i18n_catalog.i18nc("@action:button", "Keep printer configurations"),
icon = "",
description = "Keep cloud printers in Ultimaker Cura when not connected to your account.",
button_align = Message.ActionButtonAlignment.ALIGN_RIGHT)
self._removed_printers_message.addAction("remove_printers_action",
name = self.i18n_catalog.i18nc("@action:button", "Remove printers"),
icon = "",
description = "Remove cloud printer(s) which aren't linked to your account.",
button_style = Message.ActionButtonStyle.SECONDARY,
button_align = Message.ActionButtonAlignment.ALIGN_LEFT)
self._removed_printers_message.actionTriggered.connect(self._onRemovedPrintersMessageActionTriggered)
output_device_manager = CuraApplication.getInstance().getOutputDeviceManager() output_device_manager = CuraApplication.getInstance().getOutputDeviceManager()
# Remove the output device from the printers # Remove the output device from the printers
for device_id in removed_device_ids: for device_id in removed_device_ids:
device = self._um_cloud_printers.get(device_id, None) # type: Optional[GlobalStack] global_stack: Optional[GlobalStack] = self._um_cloud_printers.get(device_id, None)
if not device: if not global_stack:
continue continue
if device_id in output_device_manager.getOutputDeviceIds(): if device_id in output_device_manager.getOutputDeviceIds():
output_device_manager.removeOutputDevice(device_id) output_device_manager.removeOutputDevice(device_id)
@ -378,12 +341,19 @@ class CloudOutputDeviceManager:
del self._remote_clusters[device_id] del self._remote_clusters[device_id]
# Update the printer's metadata to mark it as not linked to the account # Update the printer's metadata to mark it as not linked to the account
device.setMetaDataEntry(META_UM_LINKED_TO_ACCOUNT, False) global_stack.setMetaDataEntry(META_UM_LINKED_TO_ACCOUNT, False)
# Generate message to show
device_names = "".join(["<li>{} ({})</li>".format(self._um_cloud_printers[device].name,
self._um_cloud_printers[device].definition.name) for device in
self.reported_device_ids])
self._removed_printers_message = RemovedPrintersMessage(self.reported_device_ids, device_names)
self._removed_printers_message.actionTriggered.connect(self._onRemovedPrintersMessageActionTriggered)
self._removed_printers_message.show() self._removed_printers_message.show()
def _onDiscoveredDeviceRemoved(self, device_id: str) -> None: def _onDiscoveredDeviceRemoved(self, device_id: str) -> None:
device = self._remote_clusters.pop(device_id, None) # type: Optional[CloudOutputDevice] """ Remove the CloudOutputDevices for printers that are offline"""
device: Optional[CloudOutputDevice] = self._remote_clusters.pop(device_id, None)
if not device: if not device:
return return
device.close() device.close()
@ -392,15 +362,15 @@ class CloudOutputDeviceManager:
output_device_manager.removeOutputDevice(device.key) output_device_manager.removeOutputDevice(device.key)
def _createMachineFromDiscoveredDevice(self, key: str, activate: bool = True) -> bool: def _createMachineFromDiscoveredDevice(self, key: str, activate: bool = True) -> bool:
device = self._remote_clusters[key] device = self._remote_clusters.get(key)
if not device: if not device:
return False return False
# Create a new machine. # Create a new machine.
# We do not use use MachineManager.addMachine here because we need to set the cluster ID before activating it. # We do not use MachineManager.addMachine here because we need to set the cluster ID before activating it.
new_machine = CuraStackBuilder.createMachine(device.name, device.printerType, show_warning_message=False) new_machine = CuraStackBuilder.createMachine(device.name, device.printerType, show_warning_message=False)
if not new_machine: if not new_machine:
Logger.log("e", "Failed creating a new machine") Logger.error(f"Failed creating a new machine for {device.name}")
return False return False
self._setOutputDeviceMetadata(device, new_machine) self._setOutputDeviceMetadata(device, new_machine)
@ -412,15 +382,19 @@ class CloudOutputDeviceManager:
def _connectToActiveMachine(self) -> None: def _connectToActiveMachine(self) -> None:
"""Callback for when the active machine was changed by the user""" """Callback for when the active machine was changed by the user"""
active_machine = CuraApplication.getInstance().getGlobalContainerStack() active_machine = CuraApplication.getInstance().getGlobalContainerStack()
if not active_machine: if not active_machine:
return return
# Check if we should directly connect with a "normal" CloudOutputDevice or that we should connect to an
# 'abstract' one
output_device_manager = CuraApplication.getInstance().getOutputDeviceManager() output_device_manager = CuraApplication.getInstance().getOutputDeviceManager()
stored_cluster_id = active_machine.getMetaDataEntry(self.META_CLUSTER_ID) stored_cluster_id = active_machine.getMetaDataEntry(self.META_CLUSTER_ID)
local_network_key = active_machine.getMetaDataEntry(self.META_NETWORK_KEY) local_network_key = active_machine.getMetaDataEntry(self.META_NETWORK_KEY)
for device in list(self._remote_clusters.values()): # Make a copy of the remote devices list, to prevent modifying the list while iterating, if a device gets added asynchronously.
# Copy of the device list, to prevent modifying the list while iterating, if a device gets added asynchronously.
remote_cluster_copy: List[CloudOutputDevice] = list(self._remote_clusters.values())
for device in remote_cluster_copy:
if device.key == stored_cluster_id: if device.key == stored_cluster_id:
# Connect to it if the stored ID matches. # Connect to it if the stored ID matches.
self._connectToOutputDevice(device, active_machine) self._connectToOutputDevice(device, active_machine)
@ -431,6 +405,14 @@ class CloudOutputDeviceManager:
# Remove device if it is not meant for the active machine. # Remove device if it is not meant for the active machine.
output_device_manager.removeOutputDevice(device.key) output_device_manager.removeOutputDevice(device.key)
# Update state of all abstract output devices
remote_abstract_cluster_copy: List[CloudOutputDevice] = list(self._abstract_clusters.values())
for device in remote_abstract_cluster_copy:
if device.printerType == active_machine.definition.getId() and parseBool(active_machine.getMetaDataEntry("is_abstract_machine", False)):
self._connectToAbstractOutputDevice(device, active_machine)
elif device.key in output_device_manager.getOutputDeviceIds():
output_device_manager.removeOutputDevice(device.key)
def _setOutputDeviceMetadata(self, device: CloudOutputDevice, machine: GlobalStack): def _setOutputDeviceMetadata(self, device: CloudOutputDevice, machine: GlobalStack):
machine.setName(device.name) machine.setName(device.name)
machine.setMetaDataEntry(self.META_CLUSTER_ID, device.key) machine.setMetaDataEntry(self.META_CLUSTER_ID, device.key)
@ -438,13 +420,24 @@ class CloudOutputDeviceManager:
machine.setMetaDataEntry("group_name", device.name) machine.setMetaDataEntry("group_name", device.name)
machine.setMetaDataEntry("group_size", device.clusterSize) machine.setMetaDataEntry("group_size", device.clusterSize)
digital_factory_string = self.i18n_catalog.i18nc("info:name", "Ultimaker Digital Factory") digital_factory_string = self.i18n_catalog.i18nc("info:name", "Ultimaker Digital Factory")
digital_factory_link = "<a href='https://digitalfactory.ultimaker.com?utm_source=cura&utm_medium=software&utm_campaign=change-account-remove-printer'>{digital_factory_string}</a>".format(digital_factory_string = digital_factory_string) digital_factory_link = f"<a href='https://digitalfactory.ultimaker.com?utm_source=cura&utm_medium=software&" \
f"utm_campaign=change-account-remove-printer'>{digital_factory_string}</a>"
removal_warning_string = self.i18n_catalog.i18nc("@message {printer_name} is replaced with the name of the printer", "{printer_name} will be removed until the next account sync.").format(printer_name = device.name) \ removal_warning_string = self.i18n_catalog.i18nc("@message {printer_name} is replaced with the name of the printer", "{printer_name} will be removed until the next account sync.").format(printer_name = device.name) \
+ "<br>" + self.i18n_catalog.i18nc("@message {printer_name} is replaced with the name of the printer", "To remove {printer_name} permanently, visit {digital_factory_link}").format(printer_name = device.name, digital_factory_link = digital_factory_link) \ + "<br>" + self.i18n_catalog.i18nc("@message {printer_name} is replaced with the name of the printer", "To remove {printer_name} permanently, visit {digital_factory_link}").format(printer_name = device.name, digital_factory_link = digital_factory_link) \
+ "<br><br>" + self.i18n_catalog.i18nc("@message {printer_name} is replaced with the name of the printer", "Are you sure you want to remove {printer_name} temporarily?").format(printer_name = device.name) + "<br><br>" + self.i18n_catalog.i18nc("@message {printer_name} is replaced with the name of the printer", "Are you sure you want to remove {printer_name} temporarily?").format(printer_name = device.name)
machine.setMetaDataEntry("removal_warning", removal_warning_string) machine.setMetaDataEntry("removal_warning", removal_warning_string)
machine.addConfiguredConnectionType(device.connectionType.value) machine.addConfiguredConnectionType(device.connectionType.value)
def _connectToAbstractOutputDevice(self, device: AbstractCloudOutputDevice, machine: GlobalStack) -> None:
Logger.debug(f"Attempting to connect to abstract machine {machine.id}")
if not device.isConnected():
device.connect()
machine.addConfiguredConnectionType(device.connectionType.value)
output_device_manager = CuraApplication.getInstance().getOutputDeviceManager()
if device.key not in output_device_manager.getOutputDeviceIds():
output_device_manager.addOutputDevice(device)
def _connectToOutputDevice(self, device: CloudOutputDevice, machine: GlobalStack) -> None: def _connectToOutputDevice(self, device: CloudOutputDevice, machine: GlobalStack) -> None:
"""Connects to an output device and makes sure it is registered in the output device manager.""" """Connects to an output device and makes sure it is registered in the output device manager."""
@ -470,7 +463,7 @@ class CloudOutputDeviceManager:
if container_cluster_id in self._remote_clusters.keys(): if container_cluster_id in self._remote_clusters.keys():
del self._remote_clusters[container_cluster_id] del self._remote_clusters[container_cluster_id]
def _onRemovedPrintersMessageActionTriggered(self, removed_printers_message: Message, action: str) -> None: def _onRemovedPrintersMessageActionTriggered(self, removed_printers_message: RemovedPrintersMessage, action: str) -> None:
if action == "keep_printer_configurations_action": if action == "keep_printer_configurations_action":
removed_printers_message.hide() removed_printers_message.hide()
elif action == "remove_printers_action": elif action == "remove_printers_action":
@ -481,12 +474,16 @@ class CloudOutputDeviceManager:
question_title = self.i18n_catalog.i18nc("@title:window", "Remove printers?") question_title = self.i18n_catalog.i18nc("@title:window", "Remove printers?")
question_content = self.i18n_catalog.i18ncp( question_content = self.i18n_catalog.i18ncp(
"@label", "@label",
"You are about to remove {0} printer from Cura. This action cannot be undone.\nAre you sure you want to continue?", "You are about to remove {0} printer from Cura. This action cannot be undone.\n"
"You are about to remove {0} printers from Cura. This action cannot be undone.\nAre you sure you want to continue?", "Are you sure you want to continue?",
"You are about to remove {0} printers from Cura. This action cannot be undone.\n"
"Are you sure you want to continue?",
len(remove_printers_ids) len(remove_printers_ids)
) )
if remove_printers_ids == all_ids: if remove_printers_ids == all_ids:
question_content = self.i18n_catalog.i18nc("@label", "You are about to remove all printers from Cura. This action cannot be undone.\nAre you sure you want to continue?") question_content = self.i18n_catalog.i18nc("@label", "You are about to remove all printers from Cura. "
"This action cannot be undone.\n"
"Are you sure you want to continue?")
result = QMessageBox.question(None, question_title, question_content) result = QMessageBox.question(None, question_title, question_content)
if result == QMessageBox.StandardButton.No: if result == QMessageBox.StandardButton.No:
return return

View File

@ -0,0 +1,60 @@
# Copyright (c) 2022 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from UM import i18nCatalog
from UM.Message import Message
from cura.CuraApplication import CuraApplication
class NewPrinterDetectedMessage(Message):
i18n_catalog = i18nCatalog("cura")
def __init__(self, num_printers_found: int) -> None:
super().__init__(title = self.i18n_catalog.i18ncp("info:status",
"New printer detected from your Ultimaker account",
"New printers detected from your Ultimaker account",
num_printers_found),
progress = 0,
lifetime = 0,
message_type = Message.MessageType.POSITIVE)
self._printers_added = 0
self._num_printers_found = num_printers_found
def updateProgressText(self, output_device):
"""
While the progress of adding printers is running, update the text displayed.
:param output_device: The output device that is being added.
:return:
"""
message_text = self.i18n_catalog.i18nc("info:status Filled in with printer name and printer model.",
"Adding printer {name} ({model}) from your account").format(
name=output_device.name, model=output_device.printerTypeName)
self.setText(message_text)
if self._num_printers_found > 1:
self.setProgress((self._printers_added / self._num_printers_found) * 100)
self._printers_added += 1
CuraApplication.getInstance().processEvents()
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>"
self.setText(message_text)
else:
self.hide()

View File

@ -0,0 +1,38 @@
# Copyright (c) 2022 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from PyQt6.QtCore import QUrl
from PyQt6.QtGui import QDesktopServices
from UM import i18nCatalog
from UM.Message import Message
I18N_CATALOG = i18nCatalog("cura")
class PrintJobPendingApprovalMessage(Message):
"""Message shown when waiting for approval on an uploaded print job."""
def __init__(self, cluster_id: str) -> None:
super().__init__(
text = I18N_CATALOG.i18nc("@info:status", "You will receive a confirmation via email when the print job is approved"),
title=I18N_CATALOG.i18nc("@info:title", "The print job was successfully submitted"),
message_type=Message.MessageType.POSITIVE
)
self.addAction("manage_print_jobs", I18N_CATALOG.i18nc("@action", "Manage print jobs"), "", "")
self.addAction("learn_more", I18N_CATALOG.i18nc("@action", "Learn more"), "", "",
button_style = Message.ActionButtonStyle.LINK,
button_align = Message.ActionButtonAlignment.ALIGN_LEFT)
self.actionTriggered.connect(self._onActionTriggered)
self.cluster_id = cluster_id
def _onActionTriggered(self, message: Message, action: str) -> None:
""" Callback function for the "Manage print jobs" button on the pending approval notification. """
match action:
case "manage_print_jobs":
QDesktopServices.openUrl(QUrl(f"https://digitalfactory.ultimaker.com/app/jobs/{self._cluster.cluster_id}?utm_source=cura&utm_medium=software&utm_campaign=message-printjob-sent"))
case "learn_more":
QDesktopServices.openUrl(QUrl("https://support.ultimaker.com/hc/en-us/articles/5329940078620?utm_source=cura&utm_medium=software&utm_campaign=message-printjob-sent"))

View File

@ -0,0 +1,52 @@
# Copyright (c) 2022 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from UM import i18nCatalog
from UM.Message import Message
from cura.CuraApplication import CuraApplication
class RemovedPrintersMessage(Message):
i18n_catalog = i18nCatalog("cura")
def __init__(self, removed_devices, device_names) -> None:
self._removed_devices = removed_devices
message_text = self.i18n_catalog.i18ncp(
"info:status",
"This printer is not linked to the Digital Factory:",
"These printers are not linked to the Digital Factory:",
len(self._removed_devices)
)
message_text += "<br/><ul>{}</ul><br/>".format(device_names)
digital_factory_string = self.i18n_catalog.i18nc("info:name", "Ultimaker Digital Factory")
website_link = f"<a href='https://digitalfactory.ultimaker.com?utm_source=cura&" \
f"utm_medium=software&utm_campaign=change-account-connect-printer'>{digital_factory_string}</a>."
message_text += self.i18n_catalog.i18nc(
"info:status",
f"To establish a connection, please visit the {website_link}"
)
super().__init__(title=self.i18n_catalog.i18ncp("info:status",
"A cloud connection is not available for a printer",
"A cloud connection is not available for some printers",
len(self._removed_devices)),
message_type=Message.MessageType.WARNING,
text = message_text)
self.addAction("keep_printer_configurations_action",
name=self.i18n_catalog.i18nc("@action:button",
"Keep printer configurations"),
icon="",
description="Keep cloud printers in Ultimaker Cura when not connected to your account.",
button_align=Message.ActionButtonAlignment.ALIGN_RIGHT)
self.addAction("remove_printers_action",
name=self.i18n_catalog.i18nc("@action:button", "Remove printers"),
icon="",
description="Remove cloud printer(s) which aren't linked to your account.",
button_style=Message.ActionButtonStyle.SECONDARY,
button_align=Message.ActionButtonAlignment.ALIGN_LEFT)

View File

@ -8,7 +8,6 @@ from ..BaseModel import BaseModel
class CloudClusterResponse(BaseModel): class CloudClusterResponse(BaseModel):
"""Class representing a cloud connected cluster.""" """Class representing a cloud connected cluster."""
def __init__(self, cluster_id: str, host_guid: str, host_name: str, is_online: bool, status: str, def __init__(self, cluster_id: str, host_guid: str, host_name: str, is_online: bool, status: str,
host_internal_ip: Optional[str] = None, host_version: Optional[str] = None, host_internal_ip: Optional[str] = None, host_version: Optional[str] = None,
friendly_name: Optional[str] = None, printer_type: str = "ultimaker3", printer_count: int = 1, friendly_name: Optional[str] = None, printer_type: str = "ultimaker3", printer_count: int = 1,

View File

@ -0,0 +1,14 @@
# Copyright (c) 2019 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from typing import Optional, List
from .CloudClusterResponse import CloudClusterResponse
from .ClusterPrinterStatus import ClusterPrinterStatus
class CloudClusterWithConfigResponse(CloudClusterResponse):
"""Class representing a cloud connected cluster."""
def __init__(self, **kwargs) -> None:
self.configuration = self.parseModel(ClusterPrinterStatus, kwargs.get("host_printer"))
super().__init__(**kwargs)

View File

@ -20,7 +20,6 @@ from ..BaseModel import BaseModel
class ClusterPrinterStatus(BaseModel): class ClusterPrinterStatus(BaseModel):
"""Class representing a cluster printer""" """Class representing a cluster printer"""
def __init__(self, enabled: bool, firmware_version: str, friendly_name: str, ip_address: str, machine_variant: str, def __init__(self, enabled: bool, firmware_version: str, friendly_name: str, ip_address: str, machine_variant: str,
status: str, unique_name: str, uuid: str, status: str, unique_name: str, uuid: str,
configuration: List[Union[Dict[str, Any], ClusterPrintCoreConfiguration]], configuration: List[Union[Dict[str, Any], ClusterPrintCoreConfiguration]],
@ -28,9 +27,9 @@ class ClusterPrinterStatus(BaseModel):
firmware_update_status: Optional[str] = None, latest_available_firmware: Optional[str] = None, firmware_update_status: Optional[str] = None, latest_available_firmware: Optional[str] = None,
build_plate: Union[Dict[str, Any], ClusterBuildPlate] = None, build_plate: Union[Dict[str, Any], ClusterBuildPlate] = None,
material_station: Union[Dict[str, Any], ClusterPrinterMaterialStation] = None, **kwargs) -> None: material_station: Union[Dict[str, Any], ClusterPrinterMaterialStation] = None, **kwargs) -> None:
"""Creates a new cluster printer status """
Creates a new cluster printer status
:param enabled: A printer can be disabled if it should not receive new jobs. By default every printer is enabled. :param enabled: A printer can be disabled if it should not receive new jobs. By default, every printer is enabled.
:param firmware_version: Firmware version installed on the printer. Can differ for each printer in a cluster. :param firmware_version: Firmware version installed on the printer. Can differ for each printer in a cluster.
:param friendly_name: Human readable name of the printer. Can be used for identification purposes. :param friendly_name: Human readable name of the printer. Can be used for identification purposes.
:param ip_address: The IP address of the printer in the local network. :param ip_address: The IP address of the printer in the local network.

View File

@ -3,6 +3,8 @@
import os import os
from typing import Optional, Dict, List, Callable, Any from typing import Optional, Dict, List, Callable, Any
from time import time
from PyQt6.QtGui import QDesktopServices from PyQt6.QtGui import QDesktopServices
from PyQt6.QtCore import pyqtSlot, QUrl, pyqtSignal, pyqtProperty, QObject from PyQt6.QtCore import pyqtSlot, QUrl, pyqtSignal, pyqtProperty, QObject
from PyQt6.QtNetwork import QNetworkReply from PyQt6.QtNetwork import QNetworkReply
@ -32,6 +34,8 @@ class LocalClusterOutputDevice(UltimakerNetworkedPrinterOutputDevice):
activeCameraUrlChanged = pyqtSignal() activeCameraUrlChanged = pyqtSignal()
CHECK_CLUSTER_INTERVAL = 10.0 # seconds
def __init__(self, device_id: str, address: str, properties: Dict[bytes, bytes], parent=None) -> None: def __init__(self, device_id: str, address: str, properties: Dict[bytes, bytes], parent=None) -> None:
super().__init__( super().__init__(
@ -107,6 +111,8 @@ class LocalClusterOutputDevice(UltimakerNetworkedPrinterOutputDevice):
def _update(self) -> None: def _update(self) -> None:
super()._update() super()._update()
if time() - self._time_of_last_request < self.CHECK_CLUSTER_INTERVAL:
return # avoid calling the cluster too often
self._getApiClient().getPrinters(self._updatePrinters) self._getApiClient().getPrinters(self._updatePrinters)
self._getApiClient().getPrintJobs(self._updatePrintJobs) self._getApiClient().getPrintJobs(self._updatePrintJobs)
self._updatePrintJobPreviewImages() self._updatePrintJobPreviewImages()

View File

@ -221,7 +221,7 @@ class LocalClusterOutputDeviceManager:
return return
# Create a new machine and activate it. # Create a new machine and activate it.
# We do not use use MachineManager.addMachine here because we need to set the network key before activating it. # We do not use MachineManager.addMachine here because we need to set the network key before activating it.
# If we do not do this the auto-pairing with the cloud-equivalent device will not work. # If we do not do this the auto-pairing with the cloud-equivalent device will not work.
new_machine = CuraStackBuilder.createMachine(device.name, device.printerType) new_machine = CuraStackBuilder.createMachine(device.name, device.printerType)
if not new_machine: if not new_machine:
@ -232,6 +232,8 @@ class LocalClusterOutputDeviceManager:
self._connectToOutputDevice(device, new_machine) self._connectToOutputDevice(device, new_machine)
self._showCloudFlowMessage(device) self._showCloudFlowMessage(device)
_abstract_machine = CuraStackBuilder.createAbstractMachine(device.printerType)
def _storeManualAddress(self, address: str) -> None: def _storeManualAddress(self, address: str) -> None:
"""Add an address to the stored preferences.""" """Add an address to the stored preferences."""

View File

@ -398,7 +398,7 @@ class USBPrinterOutputDevice(PrinterOutputDevice):
line = line[:line.find(";")] line = line[:line.find(";")]
line = line.strip() line = line.strip()
# Don't send empty lines. But we do have to send something, so send M105 instead. # Don't send empty lines. But we do have to send something, so send M105 instead.
# Don't send the M0 or M1 to the machine, as M0 and M1 are handled as an LCD menu pause. # Don't send the M0 or M1 to the machine, as M0 and M1 are handled as an LCD menu pause.
if line == "" or line == "M0" or line == "M1": if line == "" or line == "M0" or line == "M1":
@ -429,7 +429,7 @@ class USBPrinterOutputDevice(PrinterOutputDevice):
print_job.updateTimeElapsed(elapsed_time) print_job.updateTimeElapsed(elapsed_time)
estimated_time = self._print_estimated_time estimated_time = self._print_estimated_time
if progress > .1: if progress > .1:
estimated_time = self._print_estimated_time * (1 - progress) + elapsed_time estimated_time = int(self._print_estimated_time * (1 - progress) + elapsed_time)
print_job.updateTimeTotal(estimated_time) print_job.updateTimeTotal(estimated_time)
self._gcode_position += 1 self._gcode_position += 1

View File

@ -64,7 +64,7 @@ class VersionUpgrade22to24(VersionUpgrade):
config.remove_option("general", "containers") config.remove_option("general", "containers")
for idx in range(len(container_list)): for idx, _ in enumerate(container_list):
config.set("containers", str(idx), container_list[idx]) config.set("containers", str(idx), container_list[idx])
output = io.StringIO() output = io.StringIO()

View File

@ -866,13 +866,13 @@ def readIndex(node, attr):
v = readIntArray(node, attr, []) v = readIntArray(node, attr, [])
chunks = [] chunks = []
chunk = [] chunk = []
for i in range(len(v)): for i in v:
if v[i] == -1: if i == -1:
if chunk: if chunk:
chunks.append(chunk) chunks.append(chunk)
chunk = [] chunk = []
else: else:
chunk.append(v[i]) chunk.append(i)
if chunk: if chunk:
chunks.append(chunk) chunks.append(chunk)
return chunks return chunks

View File

@ -228,6 +228,9 @@ SecretStorage==3.3.1 \
keyring==23.0.1 \ keyring==23.0.1 \
--hash=sha256:045703609dd3fccfcdb27da201684278823b72af515aedec1a8515719a038cb8 \ --hash=sha256:045703609dd3fccfcdb27da201684278823b72af515aedec1a8515719a038cb8 \
--hash=sha256:8f607d7d1cc502c43a932a275a56fe47db50271904513a379d39df1af277ac48 --hash=sha256:8f607d7d1cc502c43a932a275a56fe47db50271904513a379d39df1af277ac48
networkx==2.6.2 \
--hash=sha256:2306f1950ce772c5a59a57f5486d59bb9cab98497c45fc49cbc45ac0dec119bb \
--hash=sha256:5fcb7004be69e8fbdf07dcb502efa5c77cadcaad6982164134eeb9721f826c2e
pywin32==303; \ pywin32==303; \
sys_platform=="win32" \ sys_platform=="win32" \
--hash=sha256:51cb52c5ec6709f96c3f26e7795b0bf169ee0d8395b2c1d7eb2c029a5008ed51 --hash=sha256:51cb52c5ec6709f96c3f26e7795b0bf169ee0d8395b2c1d7eb2c029a5008ed51

View File

@ -63,9 +63,6 @@
"machine_heated_bed": { "machine_heated_bed": {
"default_value": true "default_value": true
}, },
"speed_infill": {
"value": "speed_print"
},
"speed_wall_x": { "speed_wall_x": {
"value": "speed_wall" "value": "speed_wall"
}, },

View File

@ -45,16 +45,12 @@
"acceleration_enabled": { "value": false }, "acceleration_enabled": { "value": false },
"jerk_enabled": { "value": false }, "jerk_enabled": { "value": false },
"speed_print": { "value": 50.0 } , "speed_print": { "value": 50.0 } ,
"speed_infill": { "value": "speed_print" },
"skirt_brim_speed": { "value": "speed_layer_0" }, "skirt_brim_speed": { "value": "speed_layer_0" },
"line_width": { "value": "machine_nozzle_size" },
"optimize_wall_printing_order": { "value": "True" }, "optimize_wall_printing_order": { "value": "True" },
"material_initial_print_temperature": { "value": "material_print_temperature" }, "material_initial_print_temperature": { "value": "material_print_temperature" },
"material_final_print_temperature": { "value": "material_print_temperature" }, "material_final_print_temperature": { "value": "material_print_temperature" },
"material_flow": { "value": 100 },
"z_seam_type": { "value": "'back'" }, "z_seam_type": { "value": "'back'" },
"z_seam_corner": { "value": "'z_seam_corner_weighted'" }, "z_seam_corner": { "value": "'z_seam_corner_weighted'" },
"infill_sparse_density": { "value": "20" },
"infill_pattern": { "value": "'lines'" }, "infill_pattern": { "value": "'lines'" },
"infill_before_walls": { "value": false }, "infill_before_walls": { "value": false },
"infill_overlap": { "value": 30.0 }, "infill_overlap": { "value": 30.0 },

View File

@ -28,8 +28,7 @@
"retraction_speed": { "default_value": 50}, "retraction_speed": { "default_value": 50},
"gantry_height": { "value": "30" }, "gantry_height": { "value": "30" },
"speed_print": { "default_value": 50 }, "speed_print": { "default_value": 50 },
"material_print_temperature": { "value": 195 }, "default_material_print_temperature": { "value": 195 },
"material_print_temperature_layer_0": { "value": "material_print_temperature" },
"material_initial_print_temperature": { "value": "material_print_temperature" }, "material_initial_print_temperature": { "value": "material_print_temperature" },
"material_final_print_temperature": { "value": 195 }, "material_final_print_temperature": { "value": 195 },
"machine_max_feedrate_x": { "value": 500 }, "machine_max_feedrate_x": { "value": 500 },
@ -41,11 +40,9 @@
"machine_max_acceleration_z": { "value": 100 }, "machine_max_acceleration_z": { "value": 100 },
"machine_max_acceleration_e": { "value": 500 }, "machine_max_acceleration_e": { "value": 500 },
"machine_acceleration": { "value": 500 }, "machine_acceleration": { "value": 500 },
"machine_max_jerk_xy": { "value": 8 },
"machine_max_jerk_z": { "value": 0.4 },
"machine_max_jerk_e": { "value": 5 },
"material_diameter": { "default_value": 1.75 }, "material_diameter": { "default_value": 1.75 },
"infill_overlap": { "default_value": 15 },
"acceleration_print": { "value": 500 }, "acceleration_print": { "value": 500 },
"acceleration_travel": { "value": 500 }, "acceleration_travel": { "value": 500 },
"acceleration_travel_layer_0": { "value": "acceleration_travel" }, "acceleration_travel_layer_0": { "value": "acceleration_travel" },

View File

@ -45,16 +45,12 @@
"acceleration_enabled": { "value": false }, "acceleration_enabled": { "value": false },
"jerk_enabled": { "value": false }, "jerk_enabled": { "value": false },
"speed_print": { "value": 50.0 } , "speed_print": { "value": 50.0 } ,
"speed_infill": { "value": "speed_print" },
"skirt_brim_speed": { "value": "speed_layer_0" }, "skirt_brim_speed": { "value": "speed_layer_0" },
"line_width": { "value": "machine_nozzle_size" },
"optimize_wall_printing_order": { "value": "True" }, "optimize_wall_printing_order": { "value": "True" },
"material_initial_print_temperature": { "value": "material_print_temperature" }, "material_initial_print_temperature": { "value": "material_print_temperature" },
"material_final_print_temperature": { "value": "material_print_temperature" }, "material_final_print_temperature": { "value": "material_print_temperature" },
"material_flow": { "value": 100 },
"z_seam_type": { "value": "'back'" }, "z_seam_type": { "value": "'back'" },
"z_seam_corner": { "value": "'z_seam_corner_weighted'" }, "z_seam_corner": { "value": "'z_seam_corner_weighted'" },
"infill_sparse_density": { "value": "20" },
"infill_pattern": { "value": "'lines'" }, "infill_pattern": { "value": "'lines'" },
"infill_before_walls": { "value": false }, "infill_before_walls": { "value": false },
"infill_overlap": { "value": 30.0 }, "infill_overlap": { "value": 30.0 },

View File

@ -7,6 +7,7 @@
"author": "Abax 3D Technologies", "author": "Abax 3D Technologies",
"manufacturer": "Abax 3D Technologies", "manufacturer": "Abax 3D Technologies",
"file_formats": "text/x-gcode", "file_formats": "text/x-gcode",
"has_machine_quality": "true",
"machine_extruder_trains": "machine_extruder_trains":
{ {
"0": "abax_pri3_extruder_0" "0": "abax_pri3_extruder_0"

View File

@ -7,6 +7,7 @@
"author": "Abax 3D Technologies", "author": "Abax 3D Technologies",
"manufacturer": "Abax 3D Technologies", "manufacturer": "Abax 3D Technologies",
"file_formats": "text/x-gcode", "file_formats": "text/x-gcode",
"has_machine_quality": "true",
"machine_extruder_trains": "machine_extruder_trains":
{ {
"0": "abax_pri5_extruder_0" "0": "abax_pri5_extruder_0"

View File

@ -10,7 +10,7 @@
"file_formats": "text/x-gcode", "file_formats": "text/x-gcode",
"platform": "alya_platform.3mf", "platform": "alya_platform.3mf",
"platform_offset": [-60, -45, 75 ], "platform_offset": [-60, -45, 75 ],
"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", "ultimaker_abs_black", "ultimaker_abs_blue", "ultimaker_abs_green", "ultimaker_abs_grey", "ultimaker_abs_orange", "ultimaker_abs_pearl-gold", "ultimaker_abs_red", "ultimaker_abs_silver-metallic", "ultimaker_abs_white", "ultimaker_abs_yellow", "ultimaker_bam", "ultimaker_cpe_black", "ultimaker_cpe_blue", "ultimaker_cpe_dark-grey", "ultimaker_cpe_green", "ultimaker_cpe_light-grey", "ultimaker_cpe_plus_black", "ultimaker_cpe_plus_transparent", "ultimaker_cpe_plus_white", "ultimaker_cpe_red", "ultimaker_cpe_transparent", "ultimaker_cpe_white", "ultimaker_cpe_yellow", "ultimaker_nylon_black", "ultimaker_nylon_transparent", "ultimaker_pc_black", "ultimaker_pc_transparent", "ultimaker_pc_white", "ultimaker_pla_black", "ultimaker_pla_blue", "ultimaker_pla_green", "ultimaker_pla_magenta", "ultimaker_pla_orange", "ultimaker_pla_pearl-white", "ultimaker_pla_red", "ultimaker_pla_silver-metallic", "ultimaker_pla_transparent", "ultimaker_pla_white", "ultimaker_pla_yellow", "ultimaker_pp_transparent", "ultimaker_pva", "ultimaker_tough_pla_black", "ultimaker_tough_pla_green", "ultimaker_tough_pla_red", "ultimaker_tough_pla_white", "ultimaker_tpu_black", "ultimaker_tpu_blue", "ultimaker_tpu_red", "ultimaker_tpu_white", "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" ], "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" ],
"preferred_material": "generic_pla", "preferred_material": "generic_pla",
"has_machine_quality": true, "has_machine_quality": true,
"has_materials": true, "has_materials": true,

View File

@ -10,7 +10,7 @@
"file_formats": "text/x-gcode", "file_formats": "text/x-gcode",
"platform": "alya_nx_platform.3mf", "platform": "alya_nx_platform.3mf",
"platform_offset": [-104, 0, 93 ], "platform_offset": [-104, 0, 93 ],
"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", "ultimaker_abs_black", "ultimaker_abs_blue", "ultimaker_abs_green", "ultimaker_abs_grey", "ultimaker_abs_orange", "ultimaker_abs_pearl-gold", "ultimaker_abs_red", "ultimaker_abs_silver-metallic", "ultimaker_abs_white", "ultimaker_abs_yellow", "ultimaker_bam", "ultimaker_cpe_black", "ultimaker_cpe_blue", "ultimaker_cpe_dark-grey", "ultimaker_cpe_green", "ultimaker_cpe_light-grey", "ultimaker_cpe_plus_black", "ultimaker_cpe_plus_transparent", "ultimaker_cpe_plus_white", "ultimaker_cpe_red", "ultimaker_cpe_transparent", "ultimaker_cpe_white", "ultimaker_cpe_yellow", "ultimaker_nylon_black", "ultimaker_nylon_transparent", "ultimaker_pc_black", "ultimaker_pc_transparent", "ultimaker_pc_white", "ultimaker_pla_black", "ultimaker_pla_blue", "ultimaker_pla_green", "ultimaker_pla_magenta", "ultimaker_pla_orange", "ultimaker_pla_pearl-white", "ultimaker_pla_red", "ultimaker_pla_silver-metallic", "ultimaker_pla_transparent", "ultimaker_pla_white", "ultimaker_pla_yellow", "ultimaker_pp_transparent", "ultimaker_pva", "ultimaker_tough_pla_black", "ultimaker_tough_pla_green", "ultimaker_tough_pla_red", "ultimaker_tough_pla_white", "ultimaker_tpu_black", "ultimaker_tpu_blue", "ultimaker_tpu_red", "ultimaker_tpu_white", "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" ], "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" ],
"preferred_material": "generic_pla", "preferred_material": "generic_pla",
"has_machine_quality": true, "has_machine_quality": true,
"has_materials": true, "has_materials": true,

View File

@ -51,14 +51,11 @@
"speed_print": { "value": 50.0 } , "speed_print": { "value": 50.0 } ,
"speed_infill": { "value": "speed_print * 2" }, "speed_infill": { "value": "speed_print * 2" },
"speed_wall": { "value": "speed_print / 2" },
"speed_wall_0": { "value": "speed_wall" },
"speed_wall_x": { "value": "speed_wall" }, "speed_wall_x": { "value": "speed_wall" },
"speed_topbottom": { "value": "speed_print / 2" },
"speed_roofing": { "value": "speed_topbottom" }, "speed_roofing": { "value": "speed_topbottom" },
"speed_travel": { "value": "150.0 if speed_print < 60 else 250.0 if speed_print > 100 else speed_print * 2.5" }, "speed_travel": { "value": "150.0 if speed_print < 60 else 250.0 if speed_print > 100 else speed_print * 2.5" },
"speed_layer_0": { "value": "speed_print / 2" }, "speed_layer_0": { "value": "speed_print / 2" },
"speed_print_layer_0": { "value": "speed_layer_0" },
"speed_travel_layer_0": { "value": "100 if speed_layer_0 < 20 else 150 if speed_layer_0 > 30 else speed_layer_0 * 5" }, "speed_travel_layer_0": { "value": "100 if speed_layer_0 < 20 else 150 if speed_layer_0 > 30 else speed_layer_0 * 5" },
"speed_prime_tower": { "value": "speed_print" }, "speed_prime_tower": { "value": "speed_print" },
"speed_support": { "value": "speed_print" }, "speed_support": { "value": "speed_print" },
@ -67,18 +64,14 @@
"skirt_brim_speed": { "value": "speed_layer_0" }, "skirt_brim_speed": { "value": "speed_layer_0" },
"line_width": { "value": "machine_nozzle_size" },
"optimize_wall_printing_order": { "value": true }, "optimize_wall_printing_order": { "value": true },
"material_initial_print_temperature": { "value": "material_print_temperature" }, "material_initial_print_temperature": { "value": "material_print_temperature" },
"material_final_print_temperature": { "value": "material_print_temperature" }, "material_final_print_temperature": { "value": "material_print_temperature" },
"material_flow": { "value": 100 },
"z_seam_type": { "value": "'back'" }, "z_seam_type": { "value": "'back'" },
"z_seam_corner": { "value": "'z_seam_corner_weighted'" }, "z_seam_corner": { "value": "'z_seam_corner_weighted'" },
"infill_sparse_density": { "value": "20" },
"infill_pattern": { "value": "'lines' if infill_sparse_density > 50 else 'cubic'" }, "infill_pattern": { "value": "'lines' if infill_sparse_density > 50 else 'cubic'" },
"infill_before_walls": { "value": true }, "infill_before_walls": { "value": true },
"infill_overlap": { "value": 30.0 }, "infill_overlap": { "value": 30.0 },
@ -88,34 +81,34 @@
"fill_outline_gaps": { "value": false }, "fill_outline_gaps": { "value": false },
"retraction_speed": { "retraction_speed":
{
"maximum_value_warning": "machine_max_feedrate_e if retraction_enable else float('inf')", "maximum_value_warning": "machine_max_feedrate_e if retraction_enable else float('inf')",
"maximum_value": 200 "maximum_value": 200
}, },
"retraction_retract_speed": { "retraction_retract_speed":
{
"maximum_value_warning": "machine_max_feedrate_e if retraction_enable else float('inf')", "maximum_value_warning": "machine_max_feedrate_e if retraction_enable else float('inf')",
"maximum_value": 200 "maximum_value": 200
}, },
"retraction_prime_speed": { "retraction_prime_speed":
{
"maximum_value_warning": "machine_max_feedrate_e if retraction_enable else float('inf')", "maximum_value_warning": "machine_max_feedrate_e if retraction_enable else float('inf')",
"maximum_value": 200 "maximum_value": 200
}, },
"retraction_hop_enabled": { "value": "False" },
"retraction_hop": { "value": 1 }, "retraction_hop": { "value": 1 },
"retraction_combing": { "value": "'off' if retraction_hop_enabled else 'noskin'" }, "retraction_combing": { "value": "'off' if retraction_hop_enabled else 'noskin'" },
"retraction_combing_max_distance": { "value": 30 }, "retraction_combing_max_distance": { "value": 30 },
"travel_avoid_other_parts": { "value": true }, "travel_avoid_other_parts": { "value": true },
"travel_avoid_supports": { "value": true }, "travel_avoid_supports": { "value": true },
"travel_retract_before_outer_wall": { "value": true }, "travel_retract_before_outer_wall": { "value": true },
"retraction_enable": { "value": true },
"retraction_count_max": { "value": 100 }, "retraction_count_max": { "value": 100 },
"retraction_extrusion_window": { "value": 10 }, "retraction_extrusion_window": { "value": 10 },
"retraction_min_travel": { "value": 1.5 }, "retraction_min_travel": { "value": 1.5 },
"cool_fan_full_at_height": { "value": "layer_height_0 + 2 * layer_height" }, "cool_fan_full_at_height": { "value": "layer_height_0 + 2 * layer_height" },
"cool_fan_enabled": { "value": true },
"cool_min_layer_time": { "value": 10 }, "cool_min_layer_time": { "value": 10 },
"adaptive_layer_height_variation": { "value": 0.04 }, "adaptive_layer_height_variation": { "value": 0.04 },
@ -133,16 +126,12 @@
"extruder_prime_pos_y":{"minimum_value": "0","maximum_value": "machine_depth"}, "extruder_prime_pos_y":{"minimum_value": "0","maximum_value": "machine_depth"},
"extruder_prime_pos_x":{"minimum_value": "0","maximum_value": "machine_width"}, "extruder_prime_pos_x":{"minimum_value": "0","maximum_value": "machine_width"},
"relative_extrusion":{"value": false,"enabled": false}, "relative_extrusion":{"value": false, "enabled": false},
"machine_use_extruder_offset_to_offset_coords": {"default_value": true}, "machine_use_extruder_offset_to_offset_coords": {"default_value": true},
"machine_gcode_flavor": {"default_value": "RepRap (Marlin/Sprinter)"}, "machine_gcode_flavor": {"default_value": "RepRap (Marlin/Sprinter)"},
"machine_center_is_zero": { "machine_center_is_zero": { "default_value": false },
"default_value": false "gantry_height": { "value": "0"}
},
"gantry_height": {
"value": "0"
}
} }
} }

View File

@ -37,7 +37,7 @@
"machine_max_jerk_xy": { "default_value": 11.0 }, "machine_max_jerk_xy": { "default_value": 11.0 },
"machine_max_jerk_z": { "default_value": 0.4 }, "machine_max_jerk_z": { "default_value": 0.4 },
"machine_max_jerk_e": { "default_value": 11.0 }, "machine_max_jerk_e": { "default_value": 11.0 },
"acceleration_enabled": { "value": true },
"jerk_enabled": { "value": "True" }, "jerk_enabled": { "value": "True" },
"jerk_layer_0": { "value": "jerk_topbottom" }, "jerk_layer_0": { "value": "jerk_topbottom" },
"jerk_prime_tower": { "value": "math.ceil(jerk_print * 15 / 25)" }, "jerk_prime_tower": { "value": "math.ceil(jerk_print * 15 / 25)" },
@ -51,7 +51,6 @@
"gantry_height": { "value": "25.0" }, "gantry_height": { "value": "25.0" },
"skin_overlap": { "value": "10" }, "skin_overlap": { "value": "10" },
"acceleration_enabled": { "value": "True" },
"acceleration_layer_0": { "value": "acceleration_topbottom" }, "acceleration_layer_0": { "value": "acceleration_topbottom" },
"acceleration_prime_tower": { "value": "math.ceil(acceleration_print * 2000 / 4000)" }, "acceleration_prime_tower": { "value": "math.ceil(acceleration_print * 2000 / 4000)" },
"acceleration_print": { "value": "900" }, "acceleration_print": { "value": "900" },

View File

@ -53,8 +53,6 @@
"material_bed_temperature": { "maximum_value_warning": 110 }, "material_bed_temperature": { "maximum_value_warning": 110 },
"material_bed_temperature_layer_0": { "maximum_value_warning": 110 }, "material_bed_temperature_layer_0": { "maximum_value_warning": 110 },
"material_flow": { "value": 100 },
"top_bottom_thickness": { "value": "layer_height_0 + layer_height * math.floor(1.2 / layer_height)" }, "top_bottom_thickness": { "value": "layer_height_0 + layer_height * math.floor(1.2 / layer_height)" },
"wall_thickness": { "value": "line_width * 3 if line_width < 0.6 else line_width * 2" }, "wall_thickness": { "value": "line_width * 3 if line_width < 0.6 else line_width * 2" },
@ -70,15 +68,11 @@
"jerk_enabled": { "value": true }, "jerk_enabled": { "value": true },
"speed_print": { "value": 50.0 } , "speed_print": { "value": 50.0 } ,
"speed_infill": { "value": "speed_print" },
"speed_wall": { "value": "speed_print / 2" },
"speed_wall_0": { "value": "speed_wall" },
"speed_wall_x": { "value": "speed_wall" }, "speed_wall_x": { "value": "speed_wall" },
"speed_topbottom": { "value": "speed_print / 2" },
"speed_roofing": { "value": "speed_topbottom" }, "speed_roofing": { "value": "speed_topbottom" },
"speed_travel": { "value": 100.0, "maximum_value_warning": 150.0, "maximum_value": 200.0 }, "speed_travel": { "value": 100.0, "maximum_value_warning": 150.0, "maximum_value": 200.0 },
"speed_layer_0": { "value": "speed_topbottom if speed_topbottom < 20 else 20" }, "speed_layer_0": { "value": "speed_topbottom if speed_topbottom < 20 else 20" },
"speed_print_layer_0": { "value": "speed_layer_0" },
"speed_travel_layer_0": { "value": "speed_travel" }, "speed_travel_layer_0": { "value": "speed_travel" },
"speed_prime_tower": { "value": "speed_topbottom" }, "speed_prime_tower": { "value": "speed_topbottom" },
"speed_support": { "value": "speed_wall_0" }, "speed_support": { "value": "speed_wall_0" },
@ -101,7 +95,7 @@
"retraction_hop": { "value": 0.075 }, "retraction_hop": { "value": 0.075 },
"retraction_hop_only_when_collides": { "value": true }, "retraction_hop_only_when_collides": { "value": true },
"retraction_amount": { "value": 6 }, "retraction_amount": { "value": 6 },
"retraction_enable": { "value": true },
"retraction_min_travel": { "value": 1.5 }, "retraction_min_travel": { "value": 1.5 },
"retraction_combing": { "value": "'off'" }, "retraction_combing": { "value": "'off'" },
"retraction_combing_max_distance": { "value": 30 }, "retraction_combing_max_distance": { "value": 30 },
@ -114,7 +108,6 @@
"cool_fan_speed": { "value": 100 }, "cool_fan_speed": { "value": 100 },
"cool_fan_speed_0": { "value": 30 }, "cool_fan_speed_0": { "value": 30 },
"cool_fan_enabled": { "value": true },
"cool_min_layer_time": { "value": 10 }, "cool_min_layer_time": { "value": 10 },
"adhesion_type": { "value": "'none' if support_enable else 'skirt'" }, "adhesion_type": { "value": "'none' if support_enable else 'skirt'" },

View File

@ -76,8 +76,8 @@
"acceleration_enabled": { "value": false }, "acceleration_enabled": { "value": false },
"machine_max_jerk_xy": { "value": 10 }, "machine_max_jerk_xy": { "value": 10 },
"machine_max_jerk_z": { "value": 0.4 }, "machine_max_jerk_z": { "value": 0.3 },
"machine_max_jerk_e": { "value": 5 }, "machine_max_jerk_e": { "value": 15 },
"jerk_print": { "value": 10 }, "jerk_print": { "value": 10 },
"jerk_travel": { "value": "jerk_print" }, "jerk_travel": { "value": "jerk_print" },
"jerk_travel_layer_0": { "value": "jerk_travel" }, "jerk_travel_layer_0": { "value": "jerk_travel" },
@ -99,7 +99,6 @@
"travel_avoid_supports": { "value": true }, "travel_avoid_supports": { "value": true },
"travel_retract_before_outer_wall": { "value": true }, "travel_retract_before_outer_wall": { "value": true },
"retraction_enable": { "value": true },
"retraction_speed": { "value": 30 }, "retraction_speed": { "value": 30 },
"retraction_amount": { "value": 7 }, "retraction_amount": { "value": 7 },
"retraction_count_max": { "value": 100 }, "retraction_count_max": { "value": 100 },

View File

@ -64,55 +64,6 @@
"tizyx_abs", "tizyx_abs",
"tizyx_pla", "tizyx_pla",
"tizyx_pla_bois", "tizyx_pla_bois",
"ultimaker_abs_black",
"ultimaker_abs_blue",
"ultimaker_abs_green",
"ultimaker_abs_grey",
"ultimaker_abs_orange",
"ultimaker_abs_pearl-gold",
"ultimaker_abs_red",
"ultimaker_abs_silver-metallic",
"ultimaker_abs_white",
"ultimaker_abs_yellow",
"ultimaker_bam",
"ultimaker_cpe_black",
"ultimaker_cpe_blue",
"ultimaker_cpe_dark-grey",
"ultimaker_cpe_green",
"ultimaker_cpe_light-grey",
"ultimaker_cpe_plus_black",
"ultimaker_cpe_plus_transparent",
"ultimaker_cpe_plus_white",
"ultimaker_cpe_red",
"ultimaker_cpe_transparent",
"ultimaker_cpe_white",
"ultimaker_cpe_yellow",
"ultimaker_nylon_black",
"ultimaker_nylon_transparent",
"ultimaker_pc_black",
"ultimaker_pc_transparent",
"ultimaker_pc_white",
"ultimaker_pla_black",
"ultimaker_pla_blue",
"ultimaker_pla_green",
"ultimaker_pla_magenta",
"ultimaker_pla_orange",
"ultimaker_pla_pearl-white",
"ultimaker_pla_red",
"ultimaker_pla_silver-metallic",
"ultimaker_pla_transparent",
"ultimaker_pla_white",
"ultimaker_pla_yellow",
"ultimaker_pp_transparent",
"ultimaker_pva",
"ultimaker_tough_pla_black",
"ultimaker_tough_pla_green",
"ultimaker_tough_pla_red",
"ultimaker_tough_pla_white",
"ultimaker_tpu_black",
"ultimaker_tpu_blue",
"ultimaker_tpu_red",
"ultimaker_tpu_white",
"verbatim_bvoh_175", "verbatim_bvoh_175",
"zyyx_pro_flex", "zyyx_pro_flex",
"zyyx_pro_pla" "zyyx_pro_pla"
@ -155,35 +106,25 @@
"jerk_enabled": { "value": false }, "jerk_enabled": { "value": false },
"speed_print": { "value": 60.0 } , "speed_print": { "value": 60.0 } ,
"speed_infill": { "value": "speed_print" },
"speed_wall": { "value": "speed_print / 2" },
"speed_wall_0": { "value": "speed_wall" },
"speed_wall_x": { "value": "speed_wall" }, "speed_wall_x": { "value": "speed_wall" },
"speed_topbottom": { "value": "speed_print / 2" },
"speed_roofing": { "value": "speed_topbottom" },
"speed_travel": { "value": "150.0 if speed_print < 60 else 250.0 if speed_print > 100 else speed_print * 2.5" }, "speed_travel": { "value": "150.0 if speed_print < 60 else 250.0 if speed_print > 100 else speed_print * 2.5" },
"speed_layer_0": { "value": 20.0 }, "speed_layer_0": { "value": 20.0 },
"speed_print_layer_0": { "value": "speed_layer_0" },
"speed_travel_layer_0": { "value": "100 if speed_layer_0 < 20 else 150 if speed_layer_0 > 30 else speed_layer_0 * 5" }, "speed_travel_layer_0": { "value": "100 if speed_layer_0 < 20 else 150 if speed_layer_0 > 30 else speed_layer_0 * 5" },
"speed_prime_tower": { "value": "speed_topbottom" }, "speed_prime_tower": { "value": "speed_topbottom" },
"speed_support": { "value": "speed_wall_0" }, "speed_support": { "value": "speed_wall_0" },
"speed_support_interface": { "value": "speed_topbottom" }, "speed_support_interface": { "value": "speed_topbottom" },
"speed_z_hop": { "value": 5 }, "speed_z_hop": { "value": 5 },
"skirt_brim_speed": { "value": "speed_layer_0" },
"line_width": { "value": "machine_nozzle_size * 1.1" }, "line_width": { "value": "machine_nozzle_size * 1.1" },
"optimize_wall_printing_order": { "value": "True" }, "optimize_wall_printing_order": { "value": true },
"material_initial_print_temperature": { "value": "material_print_temperature" }, "material_initial_print_temperature": { "value": "material_print_temperature" },
"material_final_print_temperature": { "value": "material_print_temperature" }, "material_final_print_temperature": { "value": "material_print_temperature" },
"material_flow": { "value": 100 },
"z_seam_type": { "value": "'back'" }, "z_seam_type": { "value": "'back'" },
"z_seam_corner": { "value": "'z_seam_corner_none'" }, "z_seam_corner": { "value": "'z_seam_corner_none'" },
"infill_sparse_density": { "value": "20" },
"infill_pattern": { "value": "'lines' if infill_sparse_density > 50 else 'cubic'" }, "infill_pattern": { "value": "'lines' if infill_sparse_density > 50 else 'cubic'" },
"infill_before_walls": { "value": false }, "infill_before_walls": { "value": false },
"infill_overlap": { "value": 30.0 }, "infill_overlap": { "value": 30.0 },
@ -193,15 +134,18 @@
"fill_outline_gaps": { "value": false }, "fill_outline_gaps": { "value": false },
"retraction_speed": { "retraction_speed":
{
"maximum_value_warning": "machine_max_feedrate_e if retraction_enable else float('inf')", "maximum_value_warning": "machine_max_feedrate_e if retraction_enable else float('inf')",
"maximum_value": 200 "maximum_value": 200
}, },
"retraction_retract_speed": { "retraction_retract_speed":
{
"maximum_value_warning": "machine_max_feedrate_e if retraction_enable else float('inf')", "maximum_value_warning": "machine_max_feedrate_e if retraction_enable else float('inf')",
"maximum_value": 200 "maximum_value": 200
}, },
"retraction_prime_speed": { "retraction_prime_speed":
{
"maximum_value_warning": "machine_max_feedrate_e if retraction_enable else float('inf')", "maximum_value_warning": "machine_max_feedrate_e if retraction_enable else float('inf')",
"maximum_value": 200 "maximum_value": 200
}, },
@ -215,13 +159,12 @@
"travel_retract_before_outer_wall": { "value": true }, "travel_retract_before_outer_wall": { "value": true },
"retraction_amount": { "value": 2 }, "retraction_amount": { "value": 2 },
"retraction_enable": { "value": true },
"retraction_count_max": { "value": 100 }, "retraction_count_max": { "value": 100 },
"retraction_extrusion_window": { "value": 10 }, "retraction_extrusion_window": { "value": 10 },
"retraction_min_travel": { "value": 1.5 }, "retraction_min_travel": { "value": 1.5 },
"cool_fan_full_at_height": { "value": "layer_height_0 + 2 * layer_height" }, "cool_fan_full_at_height": { "value": "layer_height_0 + 2 * layer_height" },
"cool_fan_enabled": { "value": true },
"cool_min_layer_time": { "value": 10 }, "cool_min_layer_time": { "value": 10 },
"adhesion_type": { "value": "'none' if support_enable else 'skirt'" }, "adhesion_type": { "value": "'none' if support_enable else 'skirt'" },
@ -236,14 +179,13 @@
"meshfix_maximum_travel_resolution": { "value": "meshfix_maximum_resolution" }, "meshfix_maximum_travel_resolution": { "value": "meshfix_maximum_resolution" },
"support_angle": { "value": "math.floor(math.degrees(math.atan(line_width / 2.0 / layer_height)))" }, "support_angle": { "value": "math.floor(math.degrees(math.atan(line_width / 2.0 / layer_height)))" },
"support_pattern": { "value": "'zigzag'" },
"support_infill_rate": { "value": "0 if support_enable and support_structure == 'tree' else 20" }, "support_infill_rate": { "value": "0 if support_enable and support_structure == 'tree' else 20" },
"support_use_towers": { "value": false }, "support_use_towers": { "value": false },
"support_xy_distance": { "value": "wall_line_width_0 * 2" }, "support_xy_distance": { "value": "wall_line_width_0 * 2" },
"support_xy_distance_overhang": { "value": "wall_line_width_0" }, "support_xy_distance_overhang": { "value": "wall_line_width_0" },
"support_z_distance": { "value": "layer_height if layer_height >= 0.16 else layer_height * 2" }, "support_z_distance": { "value": "layer_height if layer_height >= 0.16 else layer_height * 2" },
"support_xy_overrides_z": { "value": "'xy_overrides_z'" }, "support_xy_overrides_z": { "value": "'xy_overrides_z'" },
"support_wall_count": { "value": 1 },
"support_brim_enable": { "value": true }, "support_brim_enable": { "value": true },
"support_brim_width": { "value": 4 }, "support_brim_width": { "value": 4 },
@ -256,7 +198,6 @@
"minimum_interface_area": { "value": 10 }, "minimum_interface_area": { "value": 10 },
"top_bottom_thickness": {"value": "layer_height_0 + layer_height * 3" }, "top_bottom_thickness": {"value": "layer_height_0 + layer_height * 3" },
"wall_thickness": {"value": "line_width * 2" } "wall_thickness": {"value": "line_width * 2" }
} }
} }

View File

@ -23,55 +23,6 @@
"supports_usb_connection": false, "supports_usb_connection": false,
"supports_network_connection": false, "supports_network_connection": false,
"exclude_materials": [ "exclude_materials": [
"ultimaker_abs_black",
"ultimaker_abs_blue",
"ultimaker_abs_green",
"ultimaker_abs_grey",
"ultimaker_abs_orange",
"ultimaker_abs_pearl-gold",
"ultimaker_abs_red",
"ultimaker_abs_silver-metallic",
"ultimaker_abs_white",
"ultimaker_abs_yellow",
"ultimaker_bam",
"ultimaker_cpe_black",
"ultimaker_cpe_blue",
"ultimaker_cpe_dark-grey",
"ultimaker_cpe_green",
"ultimaker_cpe_light-grey",
"ultimaker_cpe_plus_black",
"ultimaker_cpe_plus_transparent",
"ultimaker_cpe_plus_white",
"ultimaker_cpe_red",
"ultimaker_cpe_transparent",
"ultimaker_cpe_white",
"ultimaker_cpe_yellow",
"ultimaker_nylon_black",
"ultimaker_nylon_transparent",
"ultimaker_pc_black",
"ultimaker_pc_transparent",
"ultimaker_pc_white",
"ultimaker_pla_black",
"ultimaker_pla_blue",
"ultimaker_pla_green",
"ultimaker_pla_magenta",
"ultimaker_pla_orange",
"ultimaker_pla_pearl-white",
"ultimaker_pla_red",
"ultimaker_pla_silver-metallic",
"ultimaker_pla_transparent",
"ultimaker_pla_white",
"ultimaker_pla_yellow",
"ultimaker_pp_transparent",
"ultimaker_pva",
"ultimaker_tough_pla_black",
"ultimaker_tough_pla_green",
"ultimaker_tough_pla_red",
"ultimaker_tough_pla_white",
"ultimaker_tpu_black",
"ultimaker_tpu_blue",
"ultimaker_tpu_red",
"ultimaker_tpu_white",
"chromatik_pla", "chromatik_pla",
"dsm_arnitel2045_175", "dsm_arnitel2045_175",
"dsm_novamid1070_175", "dsm_novamid1070_175",
@ -128,55 +79,6 @@
"tizyx_pla", "tizyx_pla",
"tizyx_pla_bois", "tizyx_pla_bois",
"tizyx_pva", "tizyx_pva",
"ultimaker_abs_black",
"ultimaker_abs_blue",
"ultimaker_abs_green",
"ultimaker_abs_grey",
"ultimaker_abs_orange",
"ultimaker_abs_pearl-gold",
"ultimaker_abs_red",
"ultimaker_abs_silver-metallic",
"ultimaker_abs_white",
"ultimaker_abs_yellow",
"ultimaker_bam",
"ultimaker_cpe_black",
"ultimaker_cpe_blue",
"ultimaker_cpe_dark-grey",
"ultimaker_cpe_green",
"ultimaker_cpe_light-grey",
"ultimaker_cpe_plus_black",
"ultimaker_cpe_plus_transparent",
"ultimaker_cpe_plus_white",
"ultimaker_cpe_red",
"ultimaker_cpe_transparent",
"ultimaker_cpe_white",
"ultimaker_cpe_yellow",
"ultimaker_nylon_black",
"ultimaker_nylon_transparent",
"ultimaker_pc_black",
"ultimaker_pc_transparent",
"ultimaker_pc_white",
"ultimaker_pla_black",
"ultimaker_pla_blue",
"ultimaker_pla_green",
"ultimaker_pla_magenta",
"ultimaker_pla_orange",
"ultimaker_pla_pearl-white",
"ultimaker_pla_red",
"ultimaker_pla_silver-metallic",
"ultimaker_pla_transparent",
"ultimaker_pla_white",
"ultimaker_pla_yellow",
"ultimaker_pp_transparent",
"ultimaker_pva",
"ultimaker_tough_pla_black",
"ultimaker_tough_pla_green",
"ultimaker_tough_pla_red",
"ultimaker_tough_pla_white",
"ultimaker_tpu_black",
"ultimaker_tpu_blue",
"ultimaker_tpu_red",
"ultimaker_tpu_white",
"verbatim_bvoh_175", "verbatim_bvoh_175",
"Vertex_Delta_ABS", "Vertex_Delta_ABS",
"Vertex_Delta_PET", "Vertex_Delta_PET",

View File

@ -2,7 +2,8 @@
"version": 2, "version": 2,
"name": "Atom 3", "name": "Atom 3",
"inherits": "fdmprinter", "inherits": "fdmprinter",
"metadata": { "metadata":
{
"visible": true, "visible": true,
"author": "Daniel Kurth", "author": "Daniel Kurth",
"manufacturer": "Layer One", "manufacturer": "Layer One",
@ -22,10 +23,8 @@
}, },
"overrides":
{
"overrides": {
"machine_name": { "default_value": "Atom 3" }, "machine_name": { "default_value": "Atom 3" },
"machine_show_variants": { "default_value": true}, "machine_show_variants": { "default_value": true},
"machine_shape": { "default_value": "elliptic" }, "machine_shape": { "default_value": "elliptic" },
@ -48,45 +47,36 @@
"machine_gcode_flavor": { "RepRap (Marlin/Sprinter)": "Marlin" }, "machine_gcode_flavor": { "RepRap (Marlin/Sprinter)": "Marlin" },
"machine_heated_bed": { "default_value": true }, "machine_heated_bed": { "default_value": true },
"material_diameter": { "default_value": 1.75}, "material_diameter": { "default_value": 1.75},
"machine_start_gcode": { "machine_start_gcode":
{
"default_value": ";MACHINE START CODE\nG21 ;metric values\nG90 ;absolute positioning\nG28 ;home\nG1 Z5 F9000\n;MACHINE START CODE" "default_value": ";MACHINE START CODE\nG21 ;metric values\nG90 ;absolute positioning\nG28 ;home\nG1 Z5 F9000\n;MACHINE START CODE"
}, },
"machine_end_gcode": { "machine_end_gcode":
{
"default_value": ";MACHINE END CODE\nG91 ;relative positioning\nG1 E-1 F300 ;retract filament release pressure\nG1 Z+1.0 E-5 F9000 ;move up a and retract more\nG90 ;absolute positioning\nG28; home\nM84 ;steppers off\n;MACHINE END CODE" "default_value": ";MACHINE END CODE\nG91 ;relative positioning\nG1 E-1 F300 ;retract filament release pressure\nG1 Z+1.0 E-5 F9000 ;move up a and retract more\nG90 ;absolute positioning\nG28; home\nM84 ;steppers off\n;MACHINE END CODE"
}, },
"layer_height": {"default_value": 0.2 }, "layer_height": {"default_value": 0.2 },
"layer_height_0": { "layer_height_0":
{
"default_value": 0.2, "default_value": 0.2,
"value": "layer_height" "value": "layer_height"
}, },
"line_width": { "value": "machine_nozzle_size"}, "line_width": { "value": "machine_nozzle_size"},
"infill_line_width": { "value":"line_width"},
"initial_layer_line_width_factor": { "default_value": 100}, "initial_layer_line_width_factor": { "default_value": 100},
"top_bottom_thickness": { "default_value": 1.0}, "top_bottom_thickness": { "default_value": 1.0},
"infill_sparse_density": { "default_value": 17}, "infill_sparse_density": { "default_value": 17},
"infill_before_walls": { "value": false}, "infill_before_walls": { "value": false},
"zig_zaggify_infill": { "value": true}, "zig_zaggify_infill": { "value": true},
"default_material_print_temperature": { "default_value": 200 }, "default_material_print_temperature": { "default_value": 200 },
"material_print_temperature_layer_0": { "value": "material_print_temperature + 0"},
"material_initial_print_temperature": { "value": "material_print_temperature_layer_0"}, "material_initial_print_temperature": { "value": "material_print_temperature_layer_0"},
"material_final_print_temperature": { "value": "material_print_temperature"}, "material_final_print_temperature": { "value": "material_print_temperature"},
"default_material_bed_temperature": { "default_material_bed_temperature":
{
"default_value": 60, "default_value": 60,
"minimum_value": "0", "minimum_value": "0",
"minimum_value_warning": "build_volume_temperature", "minimum_value_warning": "build_volume_temperature",
@ -95,16 +85,13 @@
}, },
"material_bed_temperature": "material_bed_temperature":
{ {
"value": "round(default_material_bed_temperature-((-0.202*default_material_bed_temperature)+7.16)) if default_material_bed_temperature > 40 else default_material_bed_temperature", "value": "round(default_material_bed_temperature-(-0.202 * default_material_bed_temperature + 7.16)) if default_material_bed_temperature > 40 else default_material_bed_temperature",
"minimum_value": "0", "minimum_value": "0",
"minimum_value_warning": "build_volume_temperature", "minimum_value_warning": "build_volume_temperature",
"maximum_value_warning": "115", "maximum_value_warning": "115",
"maximum_value": "120" "maximum_value": "120"
}, },
"speed_print": { "default_value": 40}, "speed_print": { "default_value": 40},
"speed_wall": { "value": "speed_print * 0.75"}, "speed_wall": { "value": "speed_print * 0.75"},
"speed_wall_0": { "value": "speed_print * 0.5"}, "speed_wall_0": { "value": "speed_print * 0.5"},
@ -112,48 +99,42 @@
"speed_layer_0": { "value": "20"}, "speed_layer_0": { "value": "20"},
"speed_slowdown_layers": { "default_value": 1}, "speed_slowdown_layers": { "default_value": 1},
"retraction_amount":
{
"retraction_amount": {
"default_value": 7, "default_value": 7,
"maximum_value_warning": 9 }, "maximum_value_warning": 9
"retraction_speed": { },
"retraction_speed":
{
"default_value": 70, "default_value": 70,
"maximum_value_warning": 80 "maximum_value_warning": 80
}, },
"retraction_hop_enabled": { "default_value": true}, "retraction_hop_enabled": { "default_value": true},
"retraction_hop": { "default_value": 0.5}, "retraction_hop": { "default_value": 0.5},
"cool_min_layer_time": { "default_value": 5}, "cool_min_layer_time": { "default_value": 5},
"cool_min_speed": { "default_value": 10}, "cool_min_speed": { "default_value": 10},
"cool_lift_head": { "default_value": false}, "cool_lift_head": { "default_value": false},
"support_type": { "default_value": "everywhere"}, "support_type": { "default_value": "everywhere"},
"support_angle": { "default_value": 60}, "support_angle": { "default_value": 60},
"support_z_distance": { "value": "layer_height"}, "support_z_distance": { "value": "layer_height"},
"support_xy_distance_overhang":{"value": "machine_nozzle_size"}, "support_xy_distance_overhang":{"value": "machine_nozzle_size"},
"adhesion_type": { "default_value": "skirt"}, "adhesion_type": { "default_value": "skirt"},
"skirt_brim_minimal_length": { "skirt_brim_minimal_length":
{
"default_value": 750, "default_value": 750,
"value": "60/(layer_height_0*line_width)", "value": "60 / (layer_height_0 * line_width)",
"minimum_value": "0", "minimum_value": "0",
"minimum_value_warning": "25", "minimum_value_warning": "25",
"maximum_value_warning": "4000" "maximum_value_warning": "4000"
}, },
"skirt_gap": { "skirt_gap":
"default_value": "1`", {
"value": "3*wall_line_width_0" "default_value": 1,
"value": "3 * wall_line_width_0"
} }
} }
} }

View File

@ -59,15 +59,11 @@
"jerk_enabled": { "value": false }, "jerk_enabled": { "value": false },
"speed_print": { "value": 50.0 } , "speed_print": { "value": 50.0 } ,
"speed_infill": { "value": "speed_print" },
"speed_wall": { "value": "speed_print / 2" },
"speed_wall_0": { "value": "speed_wall" },
"speed_wall_x": { "value": "speed_wall" }, "speed_wall_x": { "value": "speed_wall" },
"speed_topbottom": { "value": "speed_print / 2" },
"speed_roofing": { "value": "speed_topbottom" }, "speed_roofing": { "value": "speed_topbottom" },
"speed_travel": { "value": "150.0 if speed_print < 60 else 250.0 if speed_print > 100 else speed_print * 2.5" }, "speed_travel": { "value": "150.0 if speed_print < 60 else 250.0 if speed_print > 100 else speed_print * 2.5" },
"speed_layer_0": { "value": 20.0 }, "speed_layer_0": { "value": 20.0 },
"speed_print_layer_0": { "value": "speed_layer_0" },
"speed_travel_layer_0": { "value": "100 if speed_layer_0 < 20 else 150 if speed_layer_0 > 30 else speed_layer_0 * 5" }, "speed_travel_layer_0": { "value": "100 if speed_layer_0 < 20 else 150 if speed_layer_0 > 30 else speed_layer_0 * 5" },
"speed_prime_tower": { "value": "speed_topbottom" }, "speed_prime_tower": { "value": "speed_topbottom" },
"speed_support": { "value": "speed_wall_0" }, "speed_support": { "value": "speed_wall_0" },
@ -76,19 +72,16 @@
"skirt_brim_speed": { "value": "speed_layer_0" }, "skirt_brim_speed": { "value": "speed_layer_0" },
"line_width": { "value": "machine_nozzle_size" },
"optimize_wall_printing_order": { "value": "True" }, "optimize_wall_printing_order": { "value": "True" },
"material_initial_print_temperature": { "value": "material_print_temperature" }, "material_initial_print_temperature": { "value": "material_print_temperature" },
"material_final_print_temperature": { "value": "material_print_temperature" }, "material_final_print_temperature": { "value": "material_print_temperature" },
"material_flow": { "value": 100 },
"z_seam_type": { "value": "'sharpest_corner'" }, "z_seam_type": { "value": "'sharpest_corner'" },
"z_seam_corner": { "value": "'z_seam_corner_inner'" }, "z_seam_corner": { "value": "'z_seam_corner_inner'" },
"infill_line_width": { "value": "line_width * 1.2" }, "infill_line_width": { "value": "line_width * 1.2" },
"infill_sparse_density": { "value": "20" },
"infill_pattern": { "value": "'lines' if infill_sparse_density > 50 else 'cubic'" }, "infill_pattern": { "value": "'lines' if infill_sparse_density > 50 else 'cubic'" },
"infill_before_walls": { "value": false }, "infill_before_walls": { "value": false },
"infill_overlap": { "value": 30.0 }, "infill_overlap": { "value": 30.0 },
@ -111,7 +104,6 @@
"maximum_value": 200 "maximum_value": 200
}, },
"retraction_hop_enabled": { "value": "False" },
"retraction_hop": { "value": 0.2 }, "retraction_hop": { "value": 0.2 },
"retraction_combing": { "value": "'off' if retraction_hop_enabled else 'noskin'" }, "retraction_combing": { "value": "'off' if retraction_hop_enabled else 'noskin'" },
"retraction_combing_max_distance": { "value": 30 }, "retraction_combing_max_distance": { "value": 30 },
@ -119,13 +111,11 @@
"travel_avoid_supports": { "value": true }, "travel_avoid_supports": { "value": true },
"travel_retract_before_outer_wall": { "value": true }, "travel_retract_before_outer_wall": { "value": true },
"retraction_enable": { "value": true },
"retraction_count_max": { "value": 100 }, "retraction_count_max": { "value": 100 },
"retraction_extrusion_window": { "value": 10 }, "retraction_extrusion_window": { "value": 10 },
"retraction_min_travel": { "value": 1.5 }, "retraction_min_travel": { "value": 1.5 },
"cool_fan_full_at_height": { "value": "layer_height_0 + 2 * layer_height" }, "cool_fan_full_at_height": { "value": "layer_height_0 + 2 * layer_height" },
"cool_fan_enabled": { "value": true },
"cool_min_layer_time": { "value": 10 }, "cool_min_layer_time": { "value": 10 },
"adhesion_type": { "value": "'skirt'" }, "adhesion_type": { "value": "'skirt'" },

View File

@ -165,9 +165,6 @@
"skin_overlap": { "skin_overlap": {
"value": 10.0 "value": 10.0
}, },
"speed_infill": {
"value": "speed_print"
},
"speed_travel_layer_0": { "speed_travel_layer_0": {
"value": "math.ceil(speed_travel * 0.4)" "value": "math.ceil(speed_travel * 0.4)"
}, },

View File

@ -35,7 +35,7 @@
"switch_extruder_retraction_speeds": {"default_value": 15 }, "switch_extruder_retraction_speeds": {"default_value": 15 },
"switch_extruder_retraction_amount": {"value": 1 }, "switch_extruder_retraction_amount": {"value": 1 },
"acceleration_enabled": { "value": true },
"speed_travel": { "value": "100" }, "speed_travel": { "value": "100" },
"speed_layer_0": { "value": "20" }, "speed_layer_0": { "value": "20" },
"speed_prime_tower": { "value": "speed_topbottom" }, "speed_prime_tower": { "value": "speed_topbottom" },
@ -54,7 +54,6 @@
"prime_blob_enable": { "enabled": true }, "prime_blob_enable": { "enabled": true },
"acceleration_enabled": { "value": "True" },
"acceleration_layer_0": { "value": "acceleration_topbottom" }, "acceleration_layer_0": { "value": "acceleration_topbottom" },
"acceleration_prime_tower": { "value": "math.ceil(acceleration_print * 2000 / 4000)" }, "acceleration_prime_tower": { "value": "math.ceil(acceleration_print * 2000 / 4000)" },
"acceleration_print": { "value": "3000" }, "acceleration_print": { "value": "3000" },

View File

@ -28,7 +28,7 @@
"infill_pattern": {"value": "'triangles'" }, "infill_pattern": {"value": "'triangles'" },
"infill_before_walls": {"value": false }, "infill_before_walls": {"value": false },
"acceleration_enabled": { "value": true },
"default_material_print_temperature": { "value": "215" }, "default_material_print_temperature": { "value": "215" },
"material_print_temperature_layer_0": { "value": "material_print_temperature + 5" }, "material_print_temperature_layer_0": { "value": "material_print_temperature + 5" },
"material_standby_temperature": { "value": "material_print_temperature" }, "material_standby_temperature": { "value": "material_print_temperature" },
@ -54,7 +54,6 @@
"prime_blob_enable": { "enabled": true }, "prime_blob_enable": { "enabled": true },
"acceleration_enabled": { "value": "True" },
"acceleration_layer_0": { "value": "acceleration_topbottom" }, "acceleration_layer_0": { "value": "acceleration_topbottom" },
"acceleration_prime_tower": { "value": "math.ceil(acceleration_print * 2000 / 4000)" }, "acceleration_prime_tower": { "value": "math.ceil(acceleration_print * 2000 / 4000)" },
"acceleration_print": { "value": "3000" }, "acceleration_print": { "value": "3000" },

View File

@ -34,7 +34,7 @@
"switch_extruder_retraction_speeds": {"default_value": 15 }, "switch_extruder_retraction_speeds": {"default_value": 15 },
"switch_extruder_retraction_amount": {"value": 1 }, "switch_extruder_retraction_amount": {"value": 1 },
"acceleration_enabled": { "value": true },
"speed_travel": { "value": "100" }, "speed_travel": { "value": "100" },
"speed_layer_0": { "value": "20" }, "speed_layer_0": { "value": "20" },
"speed_prime_tower": { "value": "speed_topbottom" }, "speed_prime_tower": { "value": "speed_topbottom" },
@ -53,7 +53,6 @@
"prime_blob_enable": { "enabled": true }, "prime_blob_enable": { "enabled": true },
"acceleration_enabled": { "value": "True" },
"acceleration_layer_0": { "value": "acceleration_topbottom" }, "acceleration_layer_0": { "value": "acceleration_topbottom" },
"acceleration_prime_tower": { "value": "math.ceil(acceleration_print * 2000 / 4000)" }, "acceleration_prime_tower": { "value": "math.ceil(acceleration_print * 2000 / 4000)" },
"acceleration_print": { "value": "3000" }, "acceleration_print": { "value": "3000" },

View File

@ -41,7 +41,6 @@
"machine_gcode_flavor": { "default_value": "RepRap (Marlin/Sprinter)" }, "machine_gcode_flavor": { "default_value": "RepRap (Marlin/Sprinter)" },
"material_print_temp_wait": { "default_value": false }, "material_print_temp_wait": { "default_value": false },
"material_bed_temp_wait": { "default_value": false }, "material_bed_temp_wait": { "default_value": false },
"prime_tower_enable": { "default_value": false },
"prime_tower_min_volume": { "value": "0.7" }, "prime_tower_min_volume": { "value": "0.7" },
"prime_tower_size": { "value": 24.0 }, "prime_tower_size": { "value": 24.0 },
"prime_tower_position_x": { "value": "125" }, "prime_tower_position_x": { "value": "125" },

View File

@ -36,9 +36,8 @@
"min_infill_area": { "default_value": 2.0 }, "min_infill_area": { "default_value": 2.0 },
"retract_at_layer_change": { "default_value": true }, "retract_at_layer_change": { "default_value": true },
"default_material_print_temperature": { "default_value": 210 }, "default_material_print_temperature": { "default_value": 210 },
"material_print_temperature": { "value": 210 },
"material_final_print_temperature": { "value": 210 }, "material_final_print_temperature": { "value": 210 },
"material_bed_temperature": { "value": 70 }, "default_material_bed_temperature": { "value": 70 },
"material_bed_temperature_layer_0": { "value": 70 }, "material_bed_temperature_layer_0": { "value": 70 },
"material_flow_layer_0": {"value": 140}, "material_flow_layer_0": {"value": 140},
"retraction_amount": { "default_value": 10 }, "retraction_amount": { "default_value": 10 },

View File

@ -68,61 +68,13 @@
"tizyx_abs", "tizyx_abs",
"tizyx_pla", "tizyx_pla",
"tizyx_pla_bois", "tizyx_pla_bois",
"ultimaker_abs_black",
"ultimaker_abs_blue",
"ultimaker_abs_green",
"ultimaker_abs_grey",
"ultimaker_abs_orange",
"ultimaker_abs_pearl-gold",
"ultimaker_abs_red",
"ultimaker_abs_silver-metallic",
"ultimaker_abs_white",
"ultimaker_abs_yellow",
"ultimaker_bam",
"ultimaker_cpe_black",
"ultimaker_cpe_blue",
"ultimaker_cpe_dark-grey",
"ultimaker_cpe_green",
"ultimaker_cpe_light-grey",
"ultimaker_cpe_plus_black",
"ultimaker_cpe_plus_transparent",
"ultimaker_cpe_plus_white",
"ultimaker_cpe_red",
"ultimaker_cpe_transparent",
"ultimaker_cpe_white",
"ultimaker_cpe_yellow",
"ultimaker_nylon_black",
"ultimaker_nylon_transparent",
"ultimaker_pc_black",
"ultimaker_pc_transparent",
"ultimaker_pc_white",
"ultimaker_pla_black",
"ultimaker_pla_blue",
"ultimaker_pla_green",
"ultimaker_pla_magenta",
"ultimaker_pla_orange",
"ultimaker_pla_pearl-white",
"ultimaker_pla_red",
"ultimaker_pla_silver-metallic",
"ultimaker_pla_transparent",
"ultimaker_pla_white",
"ultimaker_pla_yellow",
"ultimaker_pp_transparent",
"ultimaker_pva",
"ultimaker_tough_pla_black",
"ultimaker_tough_pla_green",
"ultimaker_tough_pla_red",
"ultimaker_tough_pla_white",
"ultimaker_tpu_black",
"ultimaker_tpu_blue",
"ultimaker_tpu_red",
"ultimaker_tpu_white",
"verbatim_bvoh_175", "verbatim_bvoh_175",
"zyyx_pro_flex", "zyyx_pro_flex",
"zyyx_pro_pla" "zyyx_pro_pla"
] ]
}, },
"overrides": { "overrides":
{
"machine_name": { "default_value": "Creawsome Base Printer" }, "machine_name": { "default_value": "Creawsome Base Printer" },
"machine_start_gcode": { "default_value": "M201 X500.00 Y500.00 Z100.00 E5000.00 ;Setup machine max acceleration\nM203 X500.00 Y500.00 Z10.00 E50.00 ;Setup machine max feedrate\nM204 P500.00 R1000.00 T500.00 ;Setup Print/Retract/Travel acceleration\nM205 X8.00 Y8.00 Z0.40 E5.00 ;Setup Jerk\nM220 S100 ;Reset Feedrate\nM221 S100 ;Reset Flowrate\n\nG28 ;Home\n\nG92 E0 ;Reset Extruder\nG1 Z2.0 F3000 ;Move Z Axis up\nG1 X10.1 Y20 Z0.28 F5000.0 ;Move to start position\nG1 X10.1 Y200.0 Z0.28 F1500.0 E15 ;Draw the first line\nG1 X10.4 Y200.0 Z0.28 F5000.0 ;Move to side a little\nG1 X10.4 Y20 Z0.28 F1500.0 E30 ;Draw the second line\nG92 E0 ;Reset Extruder\nG1 Z2.0 F3000 ;Move Z Axis up\n" }, "machine_start_gcode": { "default_value": "M201 X500.00 Y500.00 Z100.00 E5000.00 ;Setup machine max acceleration\nM203 X500.00 Y500.00 Z10.00 E50.00 ;Setup machine max feedrate\nM204 P500.00 R1000.00 T500.00 ;Setup Print/Retract/Travel acceleration\nM205 X8.00 Y8.00 Z0.40 E5.00 ;Setup Jerk\nM220 S100 ;Reset Feedrate\nM221 S100 ;Reset Flowrate\n\nG28 ;Home\n\nG92 E0 ;Reset Extruder\nG1 Z2.0 F3000 ;Move Z Axis up\nG1 X10.1 Y20 Z0.28 F5000.0 ;Move to start position\nG1 X10.1 Y200.0 Z0.28 F1500.0 E15 ;Draw the first line\nG1 X10.4 Y200.0 Z0.28 F5000.0 ;Move to side a little\nG1 X10.4 Y20 Z0.28 F1500.0 E30 ;Draw the second line\nG92 E0 ;Reset Extruder\nG1 Z2.0 F3000 ;Move Z Axis up\n" },
"machine_end_gcode": { "default_value": "G91 ;Relative positioning\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 positioning\n\nG1 X0 Y{machine_depth} ;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_end_gcode": { "default_value": "G91 ;Relative positioning\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 positioning\n\nG1 X0 Y{machine_depth} ;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" },
@ -158,36 +110,25 @@
"acceleration_enabled": { "value": false }, "acceleration_enabled": { "value": false },
"jerk_enabled": { "value": false }, "jerk_enabled": { "value": false },
"speed_print": { "value": 50.0 } , "speed_print": { "value": 50.0 },
"speed_infill": { "value": "speed_print" },
"speed_wall": { "value": "speed_print / 2" },
"speed_wall_0": { "value": "speed_wall" },
"speed_wall_x": { "value": "speed_wall" }, "speed_wall_x": { "value": "speed_wall" },
"speed_topbottom": { "value": "speed_print / 2" },
"speed_roofing": { "value": "speed_topbottom" }, "speed_roofing": { "value": "speed_topbottom" },
"speed_travel": { "value": "150.0 if speed_print < 60 else 250.0 if speed_print > 100 else speed_print * 2.5" }, "speed_travel": { "value": "150.0 if speed_print < 60 else 250.0 if speed_print > 100 else speed_print * 2.5" },
"speed_layer_0": { "value": 20.0 }, "speed_layer_0": { "value": 20.0 },
"speed_print_layer_0": { "value": "speed_layer_0" },
"speed_travel_layer_0": { "value": "100 if speed_layer_0 < 20 else 150 if speed_layer_0 > 30 else speed_layer_0 * 5" }, "speed_travel_layer_0": { "value": "100 if speed_layer_0 < 20 else 150 if speed_layer_0 > 30 else speed_layer_0 * 5" },
"speed_prime_tower": { "value": "speed_topbottom" }, "speed_prime_tower": { "value": "speed_topbottom" },
"speed_support": { "value": "speed_wall_0" }, "speed_support": { "value": "speed_wall_0" },
"speed_support_interface": { "value": "speed_topbottom" }, "speed_support_interface": { "value": "speed_topbottom" },
"speed_z_hop": { "value": 5 }, "speed_z_hop": { "value": 5 },
"skirt_brim_speed": { "value": "speed_layer_0" },
"line_width": { "value": "machine_nozzle_size" },
"optimize_wall_printing_order": { "value": "True" }, "optimize_wall_printing_order": { "value": "True" },
"material_initial_print_temperature": { "value": "material_print_temperature" }, "material_initial_print_temperature": { "value": "material_print_temperature" },
"material_final_print_temperature": { "value": "material_print_temperature" }, "material_final_print_temperature": { "value": "material_print_temperature" },
"material_flow": { "value": 100 },
"z_seam_type": { "value": "'back'" }, "z_seam_type": { "value": "'back'" },
"z_seam_corner": { "value": "'z_seam_corner_weighted'" }, "z_seam_corner": { "value": "'z_seam_corner_weighted'" },
"infill_sparse_density": { "value": "20" },
"infill_pattern": { "value": "'lines' if infill_sparse_density > 50 else 'cubic'" }, "infill_pattern": { "value": "'lines' if infill_sparse_density > 50 else 'cubic'" },
"infill_before_walls": { "value": false }, "infill_before_walls": { "value": false },
"infill_overlap": { "value": 30.0 }, "infill_overlap": { "value": 30.0 },
@ -197,34 +138,33 @@
"fill_outline_gaps": { "value": false }, "fill_outline_gaps": { "value": false },
"retraction_speed": { "retraction_speed":
{
"maximum_value_warning": "machine_max_feedrate_e if retraction_enable else float('inf')", "maximum_value_warning": "machine_max_feedrate_e if retraction_enable else float('inf')",
"maximum_value": 200 "maximum_value": 200
}, },
"retraction_retract_speed": { "retraction_retract_speed":
{
"maximum_value_warning": "machine_max_feedrate_e if retraction_enable else float('inf')", "maximum_value_warning": "machine_max_feedrate_e if retraction_enable else float('inf')",
"maximum_value": 200 "maximum_value": 200
}, },
"retraction_prime_speed": { "retraction_prime_speed":
{
"maximum_value_warning": "machine_max_feedrate_e if retraction_enable else float('inf')", "maximum_value_warning": "machine_max_feedrate_e if retraction_enable else float('inf')",
"maximum_value": 200 "maximum_value": 200
}, },
"retraction_hop_enabled": { "value": "False" },
"retraction_hop": { "value": 0.2 }, "retraction_hop": { "value": 0.2 },
"retraction_combing": { "value": "'off' if retraction_hop_enabled else 'noskin'" }, "retraction_combing": { "value": "'off' if retraction_hop_enabled else 'noskin'" },
"retraction_combing_max_distance": { "value": 30 }, "retraction_combing_max_distance": { "value": 30 },
"travel_avoid_other_parts": { "value": true }, "travel_avoid_other_parts": { "value": true },
"travel_avoid_supports": { "value": true }, "travel_avoid_supports": { "value": true },
"travel_retract_before_outer_wall": { "value": true }, "travel_retract_before_outer_wall": { "value": true },
"retraction_enable": { "value": true },
"retraction_count_max": { "value": 100 }, "retraction_count_max": { "value": 100 },
"retraction_extrusion_window": { "value": 10 }, "retraction_extrusion_window": { "value": 10 },
"retraction_min_travel": { "value": 1.5 }, "retraction_min_travel": { "value": 1.5 },
"cool_fan_full_at_height": { "value": "layer_height_0 + 2 * layer_height" }, "cool_fan_full_at_height": { "value": "layer_height_0 + 2 * layer_height" },
"cool_fan_enabled": { "value": true },
"cool_min_layer_time": { "value": 10 }, "cool_min_layer_time": { "value": 10 },
"adhesion_type": { "value": "'skirt'" }, "adhesion_type": { "value": "'skirt'" },
@ -238,7 +178,7 @@
"meshfix_maximum_resolution": { "value": "0.25" }, "meshfix_maximum_resolution": { "value": "0.25" },
"meshfix_maximum_travel_resolution": { "value": "meshfix_maximum_resolution" }, "meshfix_maximum_travel_resolution": { "value": "meshfix_maximum_resolution" },
"support_angle": { "value": "math.floor(math.degrees(math.atan(line_width/2.0/layer_height)))" }, "support_angle": { "value": "math.floor(math.degrees(math.atan(line_width / 2.0 /layer_height)))" },
"support_pattern": { "value": "'zigzag'" }, "support_pattern": { "value": "'zigzag'" },
"support_infill_rate": { "value": "0 if support_enable and support_structure == 'tree' else 20" }, "support_infill_rate": { "value": "0 if support_enable and support_structure == 'tree' else 20" },
"support_use_towers": { "value": false }, "support_use_towers": { "value": false },

View File

@ -9,17 +9,6 @@
"machine_width": { "default_value": 260 }, "machine_width": { "default_value": 260 },
"machine_depth": { "default_value": 260 }, "machine_depth": { "default_value": 260 },
"machine_height": { "default_value": 400 }, "machine_height": { "default_value": 400 },
"z_seam_type": { "value": "'sharpest_corner'"},
"z_seam_corner": { "value": "'z_seam_corner_inner'"},
"infill_sparse_density": { "value": "10"},
"infill_pattern": { "value": "'lines' if infill_sparse_density > 50 else 'grid'"},
"infill_overlap":{"value": 10},
"material_print_temperature":{"value": 220},
"material_bed_temperature":{"value": 50},
"retraction_amount":{"value": 10},
"speed_travel": { "value": 80.0 },
"coasting_enable": { "value": true},
"coasting_min_volume": { "value": 0.5},
"machine_head_with_fans_polygon": { "default_value": [ "machine_head_with_fans_polygon": { "default_value": [
[-26, 34], [-26, 34],
[-26, -32], [-26, -32],
@ -30,9 +19,8 @@
"gantry_height": { "value": 25 }, "gantry_height": { "value": 25 },
"speed_print": { "value": 50.0 }, "speed_print": { "value": 80.0 }
"speed_wall": { "value": 30.0 }
}, },
"metadata": { "metadata": {
"quality_definition": "creality_base", "quality_definition": "creality_base",

View File

@ -24,7 +24,7 @@
"xy_offset": { "value": 0.1 }, "xy_offset": { "value": 0.1 },
"xy_offset_layer_0": { "value": -0.1 }, "xy_offset_layer_0": { "value": -0.1 },
"hole_xy_offset": { "value": 0.15 }, "hole_xy_offset": { "value": 0.15 },
"material_print_temperature": { "value": 200 }, "material_print_temperature_layer_0": { "value": "default_material_bed_temperature + 15" },
"speed_travel": { "value": 100 }, "speed_travel": { "value": 100 },
"speed_layer_0": { "value": 25 }, "speed_layer_0": { "value": 25 },
"acceleration_enabled": { "value": true }, "acceleration_enabled": { "value": true },

View File

@ -30,9 +30,6 @@
}, },
"cool_fan_speed": { "value": 50 }, "cool_fan_speed": { "value": 50 },
"coasting_enable": { "value": true },
"coasting_volume": { "value": 0.05 },
"coasting_min_volume": { "value": 1.0 },
"jerk_enabled": { "value": false } "jerk_enabled": { "value": false }
} }
} }

View File

@ -25,7 +25,7 @@
"machine_max_jerk_e": { "value": 5 }, "machine_max_jerk_e": { "value": 5 },
"machine_max_feedrate_e": { "default_value": 200 }, "machine_max_feedrate_e": { "default_value": 200 },
"machine_steps_per_mm_e": { "default_value": 139.5 }, "machine_steps_per_mm_e": { "default_value": 139.5 },
"acceleration_enabled": { "value": "True" }, "acceleration_enabled": { "value": true },
"acceleration_print": { "value": "1250", "maximum_value": 1500 }, "acceleration_print": { "value": "1250", "maximum_value": 1500 },
"acceleration_layer_0": { "value": "acceleration_topbottom", "maximum_value": 1500 }, "acceleration_layer_0": { "value": "acceleration_topbottom", "maximum_value": 1500 },
"acceleration_topbottom": { "value": "math.ceil(acceleration_print * 1000 / 1250)", "maximum_value": 1500 }, "acceleration_topbottom": { "value": "math.ceil(acceleration_print * 1000 / 1250)", "maximum_value": 1500 },

View File

@ -25,7 +25,7 @@
"machine_end_gcode": { "default_value": ";---------------------------------------\n;Deltacomb end script\n;---------------------------------------\nG91 ;relative positioning\nG1 X8.0 E-10 F6000 ;wipe filament+material retraction\nG1 Z2 E9 ;Lift and start filament shaping\nG1 E-9\nG1 E8\nG1 E-8\nG1 E-10 F110\nG1 E-40 F5000 ; move to park position\nG28 ;home all axes (max endstops)\nM84 ;steppers off" }, "machine_end_gcode": { "default_value": ";---------------------------------------\n;Deltacomb end script\n;---------------------------------------\nG91 ;relative positioning\nG1 X8.0 E-10 F6000 ;wipe filament+material retraction\nG1 Z2 E9 ;Lift and start filament shaping\nG1 E-9\nG1 E8\nG1 E-8\nG1 E-10 F110\nG1 E-40 F5000 ; move to park position\nG28 ;home all axes (max endstops)\nM84 ;steppers off" },
"switch_extruder_retraction_amount": { "value": "0" }, "switch_extruder_retraction_amount": { "value": "0" },
"prime_tower_min_volume": { "value": "50" }, "prime_tower_min_volume": { "value": "50" },
"prime_tower_enable": { "value": "1" }, "prime_tower_enable": { "value": true },
"material_initial_print_temperature": { "value": "material_print_temperature" }, "material_initial_print_temperature": { "value": "material_print_temperature" },
"material_final_print_temperature": { "value": "material_print_temperature" }, "material_final_print_temperature": { "value": "material_print_temperature" },
"material_standby_temperature": { "value": "material_print_temperature" } "material_standby_temperature": { "value": "material_print_temperature" }

View File

@ -25,7 +25,7 @@
"machine_end_gcode": { "default_value": ";---------------------------------------\n;Deltacomb end script\n;---------------------------------------\nG91 ;relative positioning\nG1 X8.0 E-10 F6000 ;wipe filament+material retraction\nG1 Z2 E9 ;Lift and start filament shaping\nG1 E-9\nG1 E8\nG1 E-8\nG1 E-10 F110\nG1 E-40 F5000 ; move to park position\nG28 ;home all axes (max endstops)\nM84 ;steppers off" }, "machine_end_gcode": { "default_value": ";---------------------------------------\n;Deltacomb end script\n;---------------------------------------\nG91 ;relative positioning\nG1 X8.0 E-10 F6000 ;wipe filament+material retraction\nG1 Z2 E9 ;Lift and start filament shaping\nG1 E-9\nG1 E8\nG1 E-8\nG1 E-10 F110\nG1 E-40 F5000 ; move to park position\nG28 ;home all axes (max endstops)\nM84 ;steppers off" },
"switch_extruder_retraction_amount": { "value": "0" }, "switch_extruder_retraction_amount": { "value": "0" },
"prime_tower_min_volume": { "value": "50" }, "prime_tower_min_volume": { "value": "50" },
"prime_tower_enable": { "value": "1" }, "prime_tower_enable": { "value": true },
"material_initial_print_temperature": { "value": "material_print_temperature" }, "material_initial_print_temperature": { "value": "material_print_temperature" },
"material_final_print_temperature": { "value": "material_print_temperature" }, "material_final_print_temperature": { "value": "material_print_temperature" },
"material_standby_temperature": { "value": "material_print_temperature" } "material_standby_temperature": { "value": "material_print_temperature" }

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