mirror of
https://git.mirrors.martin98.com/https://github.com/Ultimaker/Cura
synced 2025-09-27 18:53:14 +08:00
Merge branch 'master' into feature_support_line_directions
# Conflicts: # resources/definitions/fdmprinter.def.json
This commit is contained in:
commit
f805a9df10
43
.github/ISSUE_TEMPLATE/bug-report.md
vendored
Normal file
43
.github/ISSUE_TEMPLATE/bug-report.md
vendored
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
---
|
||||||
|
name: Bug report
|
||||||
|
about: Create a report to help us fix issues.
|
||||||
|
title: ''
|
||||||
|
labels: 'Type: Bug'
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Processing an issue will go much faster when this is filled out, and issues which do not use this template WILL BE REMOVED and no fix will be considered!
|
||||||
|
|
||||||
|
Before filing, PLEASE check if the issue already exists (either open or closed) by using the search bar on the issues page. If it does, comment there. Even if it's closed, we can reopen it based on your comment.
|
||||||
|
|
||||||
|
Also, please note the application version in the title of the issue. For example: "[3.2.1] Cannot connect to 3rd-party printer". Please do NOT write things like "Request:" or "[BUG]" in the title; this is what labels are for.
|
||||||
|
|
||||||
|
It is also helpful to attach a project (.3mf or .curaproject) file and Cura log file so we can debug issues quicker. Information about how to find the log file can be found at https://github.com/Ultimaker/Cura#logging-issues
|
||||||
|
|
||||||
|
To upload a project, try changing the extension to e.g. .curaproject.3mf.zip so that GitHub accepts uploading the file. Otherwise, we recommend http://wetransfer.com, but other file hosts like Google Drive or Dropbox work well too.
|
||||||
|
|
||||||
|
Thank you for using Cura!
|
||||||
|
-->
|
||||||
|
|
||||||
|
**Application version**
|
||||||
|
<!-- The version of the application this issue occurs with -->
|
||||||
|
|
||||||
|
**Platform**
|
||||||
|
<!-- Information about the operating system the issue occurs on. Include at least the operating system. In the case of visual glitches/issues, also include information about your graphics drivers and GPU. -->
|
||||||
|
|
||||||
|
**Printer**
|
||||||
|
<!-- Which printer was selected in Cura? If possible, please attach project file as .curaproject.3mf.zip -->
|
||||||
|
|
||||||
|
**Reproduction steps**
|
||||||
|
<!-- How did you encounter the bug? -->
|
||||||
|
|
||||||
|
**Actual results**
|
||||||
|
<!-- What happens after the above steps have been followed -->
|
||||||
|
|
||||||
|
**Expected results**
|
||||||
|
<!-- What should happen after the above steps have been followed -->
|
||||||
|
|
||||||
|
**Additional information**
|
||||||
|
<!-- Extra information relevant to the issue, like screenshots. Don't forget to attach the log files with this issue report. -->
|
22
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
22
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
---
|
||||||
|
name: Feature request
|
||||||
|
about: Suggest an idea for this project
|
||||||
|
title: ''
|
||||||
|
labels: 'Type: New Feature'
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Is your feature request related to a problem? Please describe.**
|
||||||
|
<!-- A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] -->
|
||||||
|
|
||||||
|
**Describe the solution you'd like**
|
||||||
|
<!--A clear and concise description of what you want to happen. If possible, describe why you think this is a good solution.-->
|
||||||
|
|
||||||
|
**Describe alternatives you've considered**
|
||||||
|
<!-- A clear and concise description of any alternative solutions or features you've considered. Again, if possible, think about why these alternatives are not working out. -->
|
||||||
|
|
||||||
|
**Affected users and/or printers**
|
||||||
|
<!-- Who do you think will benefit from this? Is everyone going to benefit from these changes? Only a few people? -->
|
||||||
|
**Additional context**
|
||||||
|
<!-- Add any other context or screenshots about the feature request here. -->
|
2
.gitignore
vendored
2
.gitignore
vendored
@ -42,7 +42,6 @@ plugins/cura-siemensnx-plugin
|
|||||||
plugins/CuraBlenderPlugin
|
plugins/CuraBlenderPlugin
|
||||||
plugins/CuraCloudPlugin
|
plugins/CuraCloudPlugin
|
||||||
plugins/CuraDrivePlugin
|
plugins/CuraDrivePlugin
|
||||||
plugins/CuraDrive
|
|
||||||
plugins/CuraLiveScriptingPlugin
|
plugins/CuraLiveScriptingPlugin
|
||||||
plugins/CuraOpenSCADPlugin
|
plugins/CuraOpenSCADPlugin
|
||||||
plugins/CuraPrintProfileCreator
|
plugins/CuraPrintProfileCreator
|
||||||
@ -72,3 +71,4 @@ run.sh
|
|||||||
.scannerwork/
|
.scannerwork/
|
||||||
CuraEngine
|
CuraEngine
|
||||||
|
|
||||||
|
/.coverage
|
||||||
|
16
.gitlab-ci.yml
Normal file
16
.gitlab-ci.yml
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
image: registry.gitlab.com/ultimaker/cura/cura-build-environment:centos7
|
||||||
|
|
||||||
|
stages:
|
||||||
|
- build
|
||||||
|
|
||||||
|
build and test linux:
|
||||||
|
stage: build
|
||||||
|
tags:
|
||||||
|
- cura
|
||||||
|
- docker
|
||||||
|
- linux
|
||||||
|
script:
|
||||||
|
- docker/build.sh
|
||||||
|
artifacts:
|
||||||
|
paths:
|
||||||
|
- build
|
@ -1,11 +1,10 @@
|
|||||||
project(cura NONE)
|
project(cura)
|
||||||
cmake_minimum_required(VERSION 2.8.12)
|
cmake_minimum_required(VERSION 3.6)
|
||||||
|
|
||||||
set(CMAKE_MODULE_PATH ${CMAKE_SOURCE_DIR}/cmake/
|
|
||||||
${CMAKE_MODULE_PATH})
|
|
||||||
|
|
||||||
include(GNUInstallDirs)
|
include(GNUInstallDirs)
|
||||||
|
|
||||||
|
list(APPEND CMAKE_MODULE_PATH ${CMAKE_SOURCE_DIR}/cmake)
|
||||||
|
|
||||||
set(URANIUM_DIR "${CMAKE_SOURCE_DIR}/../Uranium" CACHE DIRECTORY "The location of the Uranium repository")
|
set(URANIUM_DIR "${CMAKE_SOURCE_DIR}/../Uranium" CACHE DIRECTORY "The location of the Uranium repository")
|
||||||
set(URANIUM_SCRIPTS_DIR "${URANIUM_DIR}/scripts" CACHE DIRECTORY "The location of the scripts directory of the Uranium repository")
|
set(URANIUM_SCRIPTS_DIR "${URANIUM_DIR}/scripts" CACHE DIRECTORY "The location of the scripts directory of the Uranium repository")
|
||||||
|
|
||||||
@ -17,15 +16,38 @@ if(CURA_DEBUGMODE)
|
|||||||
set(_cura_debugmode "ON")
|
set(_cura_debugmode "ON")
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
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_VERSION "master" CACHE STRING "Version name of Cura")
|
||||||
set(CURA_BUILDTYPE "" CACHE STRING "Build type of Cura, eg. 'PPA'")
|
set(CURA_BUILDTYPE "" CACHE STRING "Build type of Cura, eg. 'PPA'")
|
||||||
set(CURA_SDK_VERSION "" CACHE STRING "SDK version of Cura")
|
|
||||||
set(CURA_CLOUD_API_ROOT "" CACHE STRING "Alternative Cura cloud API root")
|
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_API_VERSION "" CACHE STRING "Alternative Cura cloud API version")
|
||||||
|
set(CURA_CLOUD_ACCOUNT_API_ROOT "" CACHE STRING "Alternative Cura cloud account API version")
|
||||||
|
|
||||||
configure_file(${CMAKE_SOURCE_DIR}/cura.desktop.in ${CMAKE_BINARY_DIR}/cura.desktop @ONLY)
|
configure_file(${CMAKE_SOURCE_DIR}/cura.desktop.in ${CMAKE_BINARY_DIR}/cura.desktop @ONLY)
|
||||||
|
|
||||||
configure_file(cura/CuraVersion.py.in CuraVersion.py @ONLY)
|
configure_file(cura/CuraVersion.py.in CuraVersion.py @ONLY)
|
||||||
|
|
||||||
|
|
||||||
|
# FIXME: Remove the code for CMake <3.12 once we have switched over completely.
|
||||||
|
# FindPython3 is a new module since CMake 3.12. It deprecates FindPythonInterp and FindPythonLibs. The FindPython3
|
||||||
|
# module is copied from the CMake repository here so in CMake <3.12 we can still use it.
|
||||||
|
if(${CMAKE_VERSION} VERSION_LESS 3.12)
|
||||||
|
# Use FindPythonInterp and FindPythonLibs for CMake <3.12
|
||||||
|
find_package(PythonInterp 3 REQUIRED)
|
||||||
|
|
||||||
|
set(Python3_EXECUTABLE ${PYTHON_EXECUTABLE})
|
||||||
|
|
||||||
|
set(Python3_VERSION ${PYTHON_VERSION_STRING})
|
||||||
|
set(Python3_VERSION_MAJOR ${PYTHON_VERSION_MAJOR})
|
||||||
|
set(Python3_VERSION_MINOR ${PYTHON_VERSION_MINOR})
|
||||||
|
set(Python3_VERSION_PATCH ${PYTHON_VERSION_PATCH})
|
||||||
|
else()
|
||||||
|
# Use FindPython3 for CMake >=3.12
|
||||||
|
find_package(Python3 REQUIRED COMPONENTS Interpreter Development)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
|
||||||
if(NOT ${URANIUM_DIR} STREQUAL "")
|
if(NOT ${URANIUM_DIR} STREQUAL "")
|
||||||
set(CMAKE_MODULE_PATH "${URANIUM_DIR}/cmake")
|
set(CMAKE_MODULE_PATH "${URANIUM_DIR}/cmake")
|
||||||
endif()
|
endif()
|
||||||
@ -38,12 +60,12 @@ if(NOT ${URANIUM_SCRIPTS_DIR} STREQUAL "")
|
|||||||
CREATE_TRANSLATION_TARGETS()
|
CREATE_TRANSLATION_TARGETS()
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
find_package(PythonInterp 3.5.0 REQUIRED)
|
|
||||||
|
|
||||||
install(DIRECTORY resources
|
install(DIRECTORY resources
|
||||||
DESTINATION ${CMAKE_INSTALL_DATADIR}/cura)
|
DESTINATION ${CMAKE_INSTALL_DATADIR}/cura)
|
||||||
install(DIRECTORY plugins
|
install(DIRECTORY plugins
|
||||||
DESTINATION lib${LIB_SUFFIX}/cura)
|
DESTINATION lib${LIB_SUFFIX}/cura)
|
||||||
|
|
||||||
if(NOT APPLE AND NOT WIN32)
|
if(NOT APPLE AND NOT WIN32)
|
||||||
install(FILES cura_app.py
|
install(FILES cura_app.py
|
||||||
DESTINATION ${CMAKE_INSTALL_BINDIR}
|
DESTINATION ${CMAKE_INSTALL_BINDIR}
|
||||||
@ -51,16 +73,16 @@ if(NOT APPLE AND NOT WIN32)
|
|||||||
RENAME cura)
|
RENAME cura)
|
||||||
if(EXISTS /etc/debian_version)
|
if(EXISTS /etc/debian_version)
|
||||||
install(DIRECTORY cura
|
install(DIRECTORY cura
|
||||||
DESTINATION lib${LIB_SUFFIX}/python${PYTHON_VERSION_MAJOR}/dist-packages
|
DESTINATION lib${LIB_SUFFIX}/python${Python3_VERSION_MAJOR}/dist-packages
|
||||||
FILES_MATCHING PATTERN *.py)
|
FILES_MATCHING PATTERN *.py)
|
||||||
install(FILES ${CMAKE_BINARY_DIR}/CuraVersion.py
|
install(FILES ${CMAKE_BINARY_DIR}/CuraVersion.py
|
||||||
DESTINATION lib${LIB_SUFFIX}/python${PYTHON_VERSION_MAJOR}/dist-packages/cura)
|
DESTINATION lib${LIB_SUFFIX}/python${Python3_VERSION_MAJOR}/dist-packages/cura)
|
||||||
else()
|
else()
|
||||||
install(DIRECTORY cura
|
install(DIRECTORY cura
|
||||||
DESTINATION lib${LIB_SUFFIX}/python${PYTHON_VERSION_MAJOR}.${PYTHON_VERSION_MINOR}/site-packages
|
DESTINATION lib${LIB_SUFFIX}/python${Python3_VERSION_MAJOR}.${Python3_VERSION_MINOR}/site-packages
|
||||||
FILES_MATCHING PATTERN *.py)
|
FILES_MATCHING PATTERN *.py)
|
||||||
install(FILES ${CMAKE_BINARY_DIR}/CuraVersion.py
|
install(FILES ${CMAKE_BINARY_DIR}/CuraVersion.py
|
||||||
DESTINATION lib${LIB_SUFFIX}/python${PYTHON_VERSION_MAJOR}.${PYTHON_VERSION_MINOR}/site-packages/cura)
|
DESTINATION lib${LIB_SUFFIX}/python${Python3_VERSION_MAJOR}.${Python3_VERSION_MINOR}/site-packages/cura)
|
||||||
endif()
|
endif()
|
||||||
install(FILES ${CMAKE_BINARY_DIR}/cura.desktop
|
install(FILES ${CMAKE_BINARY_DIR}/cura.desktop
|
||||||
DESTINATION ${CMAKE_INSTALL_DATADIR}/applications)
|
DESTINATION ${CMAKE_INSTALL_DATADIR}/applications)
|
||||||
@ -76,8 +98,8 @@ else()
|
|||||||
DESTINATION ${CMAKE_INSTALL_BINDIR}
|
DESTINATION ${CMAKE_INSTALL_BINDIR}
|
||||||
PERMISSIONS OWNER_READ OWNER_WRITE OWNER_EXECUTE GROUP_READ GROUP_EXECUTE WORLD_READ WORLD_EXECUTE)
|
PERMISSIONS OWNER_READ OWNER_WRITE OWNER_EXECUTE GROUP_READ GROUP_EXECUTE WORLD_READ WORLD_EXECUTE)
|
||||||
install(DIRECTORY cura
|
install(DIRECTORY cura
|
||||||
DESTINATION lib${LIB_SUFFIX}/python${PYTHON_VERSION_MAJOR}.${PYTHON_VERSION_MINOR}/site-packages
|
DESTINATION lib${LIB_SUFFIX}/python${Python3_VERSION_MAJOR}.${Python3_VERSION_MINOR}/site-packages
|
||||||
FILES_MATCHING PATTERN *.py)
|
FILES_MATCHING PATTERN *.py)
|
||||||
install(FILES ${CMAKE_BINARY_DIR}/CuraVersion.py
|
install(FILES ${CMAKE_BINARY_DIR}/CuraVersion.py
|
||||||
DESTINATION lib${LIB_SUFFIX}/python${PYTHON_VERSION_MAJOR}.${PYTHON_VERSION_MINOR}/site-packages/cura)
|
DESTINATION lib${LIB_SUFFIX}/python${Python3_VERSION_MAJOR}.${Python3_VERSION_MINOR}/site-packages/cura)
|
||||||
endif()
|
endif()
|
||||||
|
101
Jenkinsfile
vendored
101
Jenkinsfile
vendored
@ -1,8 +1,11 @@
|
|||||||
parallel_nodes(['linux && cura', 'windows && cura']) {
|
parallel_nodes(['linux && cura', 'windows && cura'])
|
||||||
timeout(time: 2, unit: "HOURS") {
|
{
|
||||||
|
timeout(time: 2, unit: "HOURS")
|
||||||
|
{
|
||||||
|
|
||||||
// Prepare building
|
// Prepare building
|
||||||
stage('Prepare') {
|
stage('Prepare')
|
||||||
|
{
|
||||||
// Ensure we start with a clean build directory.
|
// Ensure we start with a clean build directory.
|
||||||
step([$class: 'WsCleanup'])
|
step([$class: 'WsCleanup'])
|
||||||
|
|
||||||
@ -11,37 +14,17 @@ parallel_nodes(['linux && cura', 'windows && cura']) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// If any error occurs during building, we want to catch it and continue with the "finale" stage.
|
// If any error occurs during building, we want to catch it and continue with the "finale" stage.
|
||||||
catchError {
|
catchError
|
||||||
stage('Pre Checks') {
|
{
|
||||||
if (isUnix()) {
|
|
||||||
// Check shortcut keys
|
|
||||||
try {
|
|
||||||
sh """
|
|
||||||
echo 'Check for duplicate shortcut keys in all translation files.'
|
|
||||||
${env.CURA_ENVIRONMENT_PATH}/master/bin/python3 scripts/check_shortcut_keys.py
|
|
||||||
"""
|
|
||||||
} catch(e) {
|
|
||||||
currentBuild.result = "UNSTABLE"
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check setting visibilities
|
|
||||||
try {
|
|
||||||
sh """
|
|
||||||
echo 'Check for duplicate shortcut keys in all translation files.'
|
|
||||||
${env.CURA_ENVIRONMENT_PATH}/master/bin/python3 scripts/check_setting_visibility.py
|
|
||||||
"""
|
|
||||||
} catch(e) {
|
|
||||||
currentBuild.result = "UNSTABLE"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Building and testing should happen in a subdirectory.
|
// Building and testing should happen in a subdirectory.
|
||||||
dir('build') {
|
dir('build')
|
||||||
|
{
|
||||||
// Perform the "build". Since Uranium is Python code, this basically only ensures CMake is setup.
|
// Perform the "build". Since Uranium is Python code, this basically only ensures CMake is setup.
|
||||||
stage('Build') {
|
stage('Build')
|
||||||
|
{
|
||||||
def branch = env.BRANCH_NAME
|
def branch = env.BRANCH_NAME
|
||||||
if(!fileExists("${env.CURA_ENVIRONMENT_PATH}/${branch}")) {
|
if(!fileExists("${env.CURA_ENVIRONMENT_PATH}/${branch}"))
|
||||||
|
{
|
||||||
branch = "master"
|
branch = "master"
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -51,52 +34,27 @@ parallel_nodes(['linux && cura', 'windows && cura']) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Try and run the unit tests. If this stage fails, we consider the build to be "unstable".
|
// Try and run the unit tests. If this stage fails, we consider the build to be "unstable".
|
||||||
stage('Unit Test') {
|
stage('Unit Test')
|
||||||
if (isUnix()) {
|
{
|
||||||
// For Linux to show everything
|
if (isUnix())
|
||||||
def branch = env.BRANCH_NAME
|
{
|
||||||
if(!fileExists("${env.CURA_ENVIRONMENT_PATH}/${branch}")) {
|
// For Linux
|
||||||
branch = "master"
|
|
||||||
}
|
|
||||||
def uranium_dir = get_workspace_dir("Ultimaker/Uranium/${branch}")
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
sh """
|
sh 'make CTEST_OUTPUT_ON_FAILURE=TRUE test'
|
||||||
cd ..
|
} catch(e)
|
||||||
export PYTHONPATH=.:"${uranium_dir}"
|
{
|
||||||
${env.CURA_ENVIRONMENT_PATH}/${branch}/bin/pytest -x --verbose --full-trace --capture=no ./tests
|
|
||||||
"""
|
|
||||||
} catch(e) {
|
|
||||||
currentBuild.result = "UNSTABLE"
|
currentBuild.result = "UNSTABLE"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
else
|
||||||
|
{
|
||||||
// For Windows
|
// For Windows
|
||||||
try {
|
try
|
||||||
|
{
|
||||||
// This also does code style checks.
|
// This also does code style checks.
|
||||||
bat 'ctest -V'
|
bat 'ctest -V'
|
||||||
} catch(e) {
|
} catch(e)
|
||||||
currentBuild.result = "UNSTABLE"
|
{
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
stage('Code Style') {
|
|
||||||
if (isUnix()) {
|
|
||||||
// For Linux to show everything
|
|
||||||
def branch = env.BRANCH_NAME
|
|
||||||
if(!fileExists("${env.CURA_ENVIRONMENT_PATH}/${branch}")) {
|
|
||||||
branch = "master"
|
|
||||||
}
|
|
||||||
def uranium_dir = get_workspace_dir("Ultimaker/Uranium/${branch}")
|
|
||||||
|
|
||||||
try {
|
|
||||||
sh """
|
|
||||||
cd ..
|
|
||||||
export PYTHONPATH=.:"${uranium_dir}"
|
|
||||||
${env.CURA_ENVIRONMENT_PATH}/${branch}/bin/python3 run_mypy.py
|
|
||||||
"""
|
|
||||||
} catch(e) {
|
|
||||||
currentBuild.result = "UNSTABLE"
|
currentBuild.result = "UNSTABLE"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -105,7 +63,8 @@ parallel_nodes(['linux && cura', 'windows && cura']) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Perform any post-build actions like notification and publishing of unit tests.
|
// Perform any post-build actions like notification and publishing of unit tests.
|
||||||
stage('Finalize') {
|
stage('Finalize')
|
||||||
|
{
|
||||||
// Publish the test results to Jenkins.
|
// Publish the test results to Jenkins.
|
||||||
junit allowEmptyResults: true, testResults: 'build/junit*.xml'
|
junit allowEmptyResults: true, testResults: 'build/junit*.xml'
|
||||||
|
|
||||||
|
@ -20,8 +20,9 @@ Dependencies
|
|||||||
------------
|
------------
|
||||||
* [Uranium](https://github.com/Ultimaker/Uranium) Cura is built on top of the Uranium framework.
|
* [Uranium](https://github.com/Ultimaker/Uranium) Cura is built on top of the Uranium framework.
|
||||||
* [CuraEngine](https://github.com/Ultimaker/CuraEngine) This will be needed at runtime to perform the actual slicing.
|
* [CuraEngine](https://github.com/Ultimaker/CuraEngine) This will be needed at runtime to perform the actual slicing.
|
||||||
|
* [fdm_materials](https://github.com/Ultimaker/fdm_materials) Required to load a printer that has swappable material profiles.
|
||||||
* [PySerial](https://github.com/pyserial/pyserial) Only required for USB printing support.
|
* [PySerial](https://github.com/pyserial/pyserial) Only required for USB printing support.
|
||||||
* [python-zeroconf](https://github.com/jstasiak/python-zeroconf) Only required to detect mDNS-enabled printers
|
* [python-zeroconf](https://github.com/jstasiak/python-zeroconf) Only required to detect mDNS-enabled printers.
|
||||||
|
|
||||||
Build scripts
|
Build scripts
|
||||||
-------------
|
-------------
|
||||||
|
@ -1,10 +1,23 @@
|
|||||||
# Copyright (c) 2018 Ultimaker B.V.
|
# Copyright (c) 2018 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.
|
||||||
|
|
||||||
enable_testing()
|
include(CTest)
|
||||||
include(CMakeParseArguments)
|
include(CMakeParseArguments)
|
||||||
|
|
||||||
find_package(PythonInterp 3.5.0 REQUIRED)
|
# FIXME: Remove the code for CMake <3.12 once we have switched over completely.
|
||||||
|
# FindPython3 is a new module since CMake 3.12. It deprecates FindPythonInterp and FindPythonLibs. The FindPython3
|
||||||
|
# module is copied from the CMake repository here so in CMake <3.12 we can still use it.
|
||||||
|
if(${CMAKE_VERSION} VERSION_LESS 3.12)
|
||||||
|
# Use FindPythonInterp and FindPythonLibs for CMake <3.12
|
||||||
|
find_package(PythonInterp 3 REQUIRED)
|
||||||
|
|
||||||
|
set(Python3_EXECUTABLE ${PYTHON_EXECUTABLE})
|
||||||
|
else()
|
||||||
|
# Use FindPython3 for CMake >=3.12
|
||||||
|
find_package(Python3 REQUIRED COMPONENTS Interpreter Development)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
add_custom_target(test-verbose COMMAND ${CMAKE_CTEST_COMMAND} --verbose)
|
||||||
|
|
||||||
function(cura_add_test)
|
function(cura_add_test)
|
||||||
set(_single_args NAME DIRECTORY PYTHONPATH)
|
set(_single_args NAME DIRECTORY PYTHONPATH)
|
||||||
@ -34,7 +47,7 @@ function(cura_add_test)
|
|||||||
if (NOT ${test_exists})
|
if (NOT ${test_exists})
|
||||||
add_test(
|
add_test(
|
||||||
NAME ${_NAME}
|
NAME ${_NAME}
|
||||||
COMMAND ${PYTHON_EXECUTABLE} -m pytest --verbose --full-trace --capture=no --no-print-log --junitxml=${CMAKE_BINARY_DIR}/junit-${_NAME}.xml ${_DIRECTORY}
|
COMMAND ${Python3_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 LANG=C)
|
||||||
set_tests_properties(${_NAME} PROPERTIES ENVIRONMENT "PYTHONPATH=${_PYTHONPATH}")
|
set_tests_properties(${_NAME} PROPERTIES ENVIRONMENT "PYTHONPATH=${_PYTHONPATH}")
|
||||||
@ -57,5 +70,13 @@ endforeach()
|
|||||||
#Add code style test.
|
#Add code style test.
|
||||||
add_test(
|
add_test(
|
||||||
NAME "code-style"
|
NAME "code-style"
|
||||||
COMMAND ${PYTHON_EXECUTABLE} run_mypy.py WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
|
COMMAND ${Python3_EXECUTABLE} run_mypy.py
|
||||||
|
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
|
||||||
|
)
|
||||||
|
|
||||||
|
#Add test for whether the shortcut alt-keys are unique in every translation.
|
||||||
|
add_test(
|
||||||
|
NAME "shortcut-keys"
|
||||||
|
COMMAND ${Python3_EXECUTABLE} scripts/check_shortcut_keys.py
|
||||||
|
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
|
||||||
)
|
)
|
19
contributing.md
Normal file
19
contributing.md
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
Submitting bug reports
|
||||||
|
----------------------
|
||||||
|
Please submit bug reports for all of Cura and CuraEngine to the [Cura repository](https://github.com/Ultimaker/Cura/issues). There will be a template there to fill in. Depending on the type of issue, we will usually ask for the [Cura log](Logging Issues) or a project file.
|
||||||
|
|
||||||
|
If a bug report would contain private information, such as a proprietary 3D model, you may also e-mail us. Ask for contact information in the issue.
|
||||||
|
|
||||||
|
Bugs related to supporting certain types of printers can usually not be solved by the Cura maintainers, since we don't have access to every 3D printer model in the world either. We have to rely on external contributors to fix this. If it's something simple and obvious, such as a mistake in the start g-code, then we can directly fix it for you, but e.g. issues with USB cable connectivity are impossible for us to debug.
|
||||||
|
|
||||||
|
Requesting features
|
||||||
|
-------------------
|
||||||
|
The issue template in the Cura repository does not apply to feature requests. You can ignore it.
|
||||||
|
|
||||||
|
When requesting a feature, please describe clearly what you need and why you think this is valuable to users or what problem it solves.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
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`.
|
@ -13,6 +13,6 @@ TryExec=@CMAKE_INSTALL_FULL_BINDIR@/cura
|
|||||||
Icon=cura-icon
|
Icon=cura-icon
|
||||||
Terminal=false
|
Terminal=false
|
||||||
Type=Application
|
Type=Application
|
||||||
MimeType=model/stl;application/vnd.ms-3mfdocument;application/prs.wavefront-obj;image/bmp;image/gif;image/jpeg;image/png;model/x3d+xml;
|
MimeType=model/stl;application/vnd.ms-3mfdocument;application/prs.wavefront-obj;image/bmp;image/gif;image/jpeg;image/png;model/x3d+xml;text/x-gcode;
|
||||||
Categories=Graphics;
|
Categories=Graphics;
|
||||||
Keywords=3D;Printing;Slicer;
|
Keywords=3D;Printing;Slicer;
|
||||||
|
@ -19,4 +19,12 @@
|
|||||||
<glob-deleteall/>
|
<glob-deleteall/>
|
||||||
<glob pattern="*.obj"/>
|
<glob pattern="*.obj"/>
|
||||||
</mime-type>
|
</mime-type>
|
||||||
|
<mime-type type="text/x-gcode">
|
||||||
|
<sub-class-of type="text/plain"/>
|
||||||
|
<comment>Gcode file</comment>
|
||||||
|
<icon name="unknown"/>
|
||||||
|
<glob-deleteall/>
|
||||||
|
<glob pattern="*.gcode"/>
|
||||||
|
<glob pattern="*.g"/>
|
||||||
|
</mime-type>
|
||||||
</mime-info>
|
</mime-info>
|
@ -6,6 +6,7 @@ from PyQt5.QtCore import QObject, pyqtSignal, pyqtSlot, pyqtProperty
|
|||||||
|
|
||||||
from UM.i18n import i18nCatalog
|
from UM.i18n import i18nCatalog
|
||||||
from UM.Message import Message
|
from UM.Message import Message
|
||||||
|
from cura import UltimakerCloudAuthentication
|
||||||
|
|
||||||
from cura.OAuth2.AuthorizationService import AuthorizationService
|
from cura.OAuth2.AuthorizationService import AuthorizationService
|
||||||
from cura.OAuth2.Models import OAuth2Settings
|
from cura.OAuth2.Models import OAuth2Settings
|
||||||
@ -28,6 +29,7 @@ i18n_catalog = i18nCatalog("cura")
|
|||||||
class Account(QObject):
|
class Account(QObject):
|
||||||
# Signal emitted when user logged in or out.
|
# Signal emitted when user logged in or out.
|
||||||
loginStateChanged = pyqtSignal(bool)
|
loginStateChanged = pyqtSignal(bool)
|
||||||
|
accessTokenChanged = pyqtSignal()
|
||||||
|
|
||||||
def __init__(self, application: "CuraApplication", parent = None) -> None:
|
def __init__(self, application: "CuraApplication", parent = None) -> None:
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
@ -37,15 +39,16 @@ class Account(QObject):
|
|||||||
self._logged_in = False
|
self._logged_in = False
|
||||||
|
|
||||||
self._callback_port = 32118
|
self._callback_port = 32118
|
||||||
self._oauth_root = "https://account.ultimaker.com"
|
self._oauth_root = UltimakerCloudAuthentication.CuraCloudAccountAPIRoot
|
||||||
self._cloud_api_root = "https://api.ultimaker.com"
|
|
||||||
|
|
||||||
self._oauth_settings = OAuth2Settings(
|
self._oauth_settings = OAuth2Settings(
|
||||||
OAUTH_SERVER_URL= self._oauth_root,
|
OAUTH_SERVER_URL= self._oauth_root,
|
||||||
CALLBACK_PORT=self._callback_port,
|
CALLBACK_PORT=self._callback_port,
|
||||||
CALLBACK_URL="http://localhost:{}/callback".format(self._callback_port),
|
CALLBACK_URL="http://localhost:{}/callback".format(self._callback_port),
|
||||||
CLIENT_ID="um---------------ultimaker_cura_drive_plugin",
|
CLIENT_ID="um----------------------------ultimaker_cura",
|
||||||
CLIENT_SCOPES="user.read drive.backups.read drive.backups.write",
|
CLIENT_SCOPES="account.user.read drive.backup.read drive.backup.write packages.download "
|
||||||
|
"packages.rating.read packages.rating.write connect.cluster.read connect.cluster.write "
|
||||||
|
"cura.printjob.read cura.printjob.write cura.mesh.read cura.mesh.write",
|
||||||
AUTH_DATA_PREFERENCE_KEY="general/ultimaker_auth_data",
|
AUTH_DATA_PREFERENCE_KEY="general/ultimaker_auth_data",
|
||||||
AUTH_SUCCESS_REDIRECT="{}/app/auth-success".format(self._oauth_root),
|
AUTH_SUCCESS_REDIRECT="{}/app/auth-success".format(self._oauth_root),
|
||||||
AUTH_FAILED_REDIRECT="{}/app/auth-error".format(self._oauth_root)
|
AUTH_FAILED_REDIRECT="{}/app/auth-error".format(self._oauth_root)
|
||||||
@ -55,11 +58,19 @@ class Account(QObject):
|
|||||||
|
|
||||||
def initialize(self) -> None:
|
def initialize(self) -> None:
|
||||||
self._authorization_service.initialize(self._application.getPreferences())
|
self._authorization_service.initialize(self._application.getPreferences())
|
||||||
|
|
||||||
self._authorization_service.onAuthStateChanged.connect(self._onLoginStateChanged)
|
self._authorization_service.onAuthStateChanged.connect(self._onLoginStateChanged)
|
||||||
self._authorization_service.onAuthenticationError.connect(self._onLoginStateChanged)
|
self._authorization_service.onAuthenticationError.connect(self._onLoginStateChanged)
|
||||||
|
self._authorization_service.accessTokenChanged.connect(self._onAccessTokenChanged)
|
||||||
self._authorization_service.loadAuthDataFromPreferences()
|
self._authorization_service.loadAuthDataFromPreferences()
|
||||||
|
|
||||||
|
def _onAccessTokenChanged(self):
|
||||||
|
self.accessTokenChanged.emit()
|
||||||
|
|
||||||
|
## Returns a boolean indicating whether the given authentication is applied against staging or not.
|
||||||
|
@property
|
||||||
|
def is_staging(self) -> bool:
|
||||||
|
return "staging" in self._oauth_root
|
||||||
|
|
||||||
@pyqtProperty(bool, notify=loginStateChanged)
|
@pyqtProperty(bool, notify=loginStateChanged)
|
||||||
def isLoggedIn(self) -> bool:
|
def isLoggedIn(self) -> bool:
|
||||||
return self._logged_in
|
return self._logged_in
|
||||||
@ -70,6 +81,9 @@ class Account(QObject):
|
|||||||
self._error_message.hide()
|
self._error_message.hide()
|
||||||
self._error_message = Message(error_message, title = i18n_catalog.i18nc("@info:title", "Login failed"))
|
self._error_message = Message(error_message, title = i18n_catalog.i18nc("@info:title", "Login failed"))
|
||||||
self._error_message.show()
|
self._error_message.show()
|
||||||
|
self._logged_in = False
|
||||||
|
self.loginStateChanged.emit(False)
|
||||||
|
return
|
||||||
|
|
||||||
if self._logged_in != logged_in:
|
if self._logged_in != logged_in:
|
||||||
self._logged_in = logged_in
|
self._logged_in = logged_in
|
||||||
@ -96,7 +110,7 @@ class Account(QObject):
|
|||||||
return None
|
return None
|
||||||
return user_profile.profile_image_url
|
return user_profile.profile_image_url
|
||||||
|
|
||||||
@pyqtProperty(str, notify=loginStateChanged)
|
@pyqtProperty(str, notify=accessTokenChanged)
|
||||||
def accessToken(self) -> Optional[str]:
|
def accessToken(self) -> Optional[str]:
|
||||||
return self._authorization_service.getAccessToken()
|
return self._authorization_service.getAccessToken()
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
# Copyright (c) 2018 Ultimaker B.V.
|
# Copyright (c) 2018 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 Tuple, Optional, TYPE_CHECKING
|
from typing import Tuple, Optional, TYPE_CHECKING, Dict, Any
|
||||||
|
|
||||||
from cura.Backups.BackupsManager import BackupsManager
|
from cura.Backups.BackupsManager import BackupsManager
|
||||||
|
|
||||||
@ -24,12 +24,12 @@ class Backups:
|
|||||||
## Create a new back-up using the BackupsManager.
|
## Create a new back-up using the BackupsManager.
|
||||||
# \return Tuple containing a ZIP file with the back-up data and a dict
|
# \return Tuple containing a ZIP file with the back-up data and a dict
|
||||||
# with metadata about the back-up.
|
# with metadata about the back-up.
|
||||||
def createBackup(self) -> Tuple[Optional[bytes], Optional[dict]]:
|
def createBackup(self) -> Tuple[Optional[bytes], Optional[Dict[str, Any]]]:
|
||||||
return self.manager.createBackup()
|
return self.manager.createBackup()
|
||||||
|
|
||||||
## Restore a back-up using the BackupsManager.
|
## Restore a back-up using the BackupsManager.
|
||||||
# \param zip_file A ZIP file containing the actual back-up data.
|
# \param zip_file A ZIP file containing the actual back-up data.
|
||||||
# \param meta_data Some metadata needed for restoring a back-up, like the
|
# \param meta_data Some metadata needed for restoring a back-up, like the
|
||||||
# Cura version number.
|
# Cura version number.
|
||||||
def restoreBackup(self, zip_file: bytes, meta_data: dict) -> None:
|
def restoreBackup(self, zip_file: bytes, meta_data: Dict[str, Any]) -> None:
|
||||||
return self.manager.restoreBackup(zip_file, meta_data)
|
return self.manager.restoreBackup(zip_file, meta_data)
|
||||||
|
@ -3,7 +3,6 @@
|
|||||||
|
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from UM.PluginRegistry import PluginRegistry
|
|
||||||
from cura.API.Interface.Settings import Settings
|
from cura.API.Interface.Settings import Settings
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
@ -23,9 +22,6 @@ if TYPE_CHECKING:
|
|||||||
|
|
||||||
class Interface:
|
class Interface:
|
||||||
|
|
||||||
# For now we use the same API version to be consistent.
|
|
||||||
VERSION = PluginRegistry.APIVersion
|
|
||||||
|
|
||||||
def __init__(self, application: "CuraApplication") -> None:
|
def __init__(self, application: "CuraApplication") -> None:
|
||||||
# API methods specific to the settings portion of the UI
|
# API methods specific to the settings portion of the UI
|
||||||
self.settings = Settings(application)
|
self.settings = Settings(application)
|
||||||
|
@ -4,7 +4,6 @@ from typing import Optional, TYPE_CHECKING
|
|||||||
|
|
||||||
from PyQt5.QtCore import QObject, pyqtProperty
|
from PyQt5.QtCore import QObject, pyqtProperty
|
||||||
|
|
||||||
from UM.PluginRegistry import PluginRegistry
|
|
||||||
from cura.API.Backups import Backups
|
from cura.API.Backups import Backups
|
||||||
from cura.API.Interface import Interface
|
from cura.API.Interface import Interface
|
||||||
from cura.API.Account import Account
|
from cura.API.Account import Account
|
||||||
@ -22,7 +21,6 @@ if TYPE_CHECKING:
|
|||||||
class CuraAPI(QObject):
|
class CuraAPI(QObject):
|
||||||
|
|
||||||
# For now we use the same API version to be consistent.
|
# For now we use the same API version to be consistent.
|
||||||
VERSION = PluginRegistry.APIVersion
|
|
||||||
__instance = None # type: "CuraAPI"
|
__instance = None # type: "CuraAPI"
|
||||||
_application = None # type: CuraApplication
|
_application = None # type: CuraApplication
|
||||||
|
|
||||||
|
48
cura/ApplicationMetadata.py
Normal file
48
cura/ApplicationMetadata.py
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
# Copyright (c) 2018 Ultimaker B.V.
|
||||||
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
|
# ---------
|
||||||
|
# General constants used in Cura
|
||||||
|
# ---------
|
||||||
|
DEFAULT_CURA_APP_NAME = "cura"
|
||||||
|
DEFAULT_CURA_DISPLAY_NAME = "Ultimaker Cura"
|
||||||
|
DEFAULT_CURA_VERSION = "master"
|
||||||
|
DEFAULT_CURA_BUILD_TYPE = ""
|
||||||
|
DEFAULT_CURA_DEBUG_MODE = False
|
||||||
|
DEFAULT_CURA_SDK_VERSION = "6.2.0"
|
||||||
|
|
||||||
|
try:
|
||||||
|
from cura.CuraVersion import CuraAppName # type: ignore
|
||||||
|
if CuraAppName == "":
|
||||||
|
CuraAppName = DEFAULT_CURA_APP_NAME
|
||||||
|
except ImportError:
|
||||||
|
CuraAppName = DEFAULT_CURA_APP_NAME
|
||||||
|
|
||||||
|
try:
|
||||||
|
from cura.CuraVersion import CuraAppDisplayName # type: ignore
|
||||||
|
if CuraAppDisplayName == "":
|
||||||
|
CuraAppDisplayName = DEFAULT_CURA_DISPLAY_NAME
|
||||||
|
except ImportError:
|
||||||
|
CuraAppDisplayName = DEFAULT_CURA_DISPLAY_NAME
|
||||||
|
|
||||||
|
try:
|
||||||
|
from cura.CuraVersion import CuraVersion # type: ignore
|
||||||
|
if CuraVersion == "":
|
||||||
|
CuraVersion = DEFAULT_CURA_VERSION
|
||||||
|
except ImportError:
|
||||||
|
CuraVersion = DEFAULT_CURA_VERSION # [CodeStyle: Reflecting imported value]
|
||||||
|
|
||||||
|
try:
|
||||||
|
from cura.CuraVersion import CuraBuildType # type: ignore
|
||||||
|
except ImportError:
|
||||||
|
CuraBuildType = DEFAULT_CURA_BUILD_TYPE
|
||||||
|
|
||||||
|
try:
|
||||||
|
from cura.CuraVersion import CuraDebugMode # type: ignore
|
||||||
|
except ImportError:
|
||||||
|
CuraDebugMode = DEFAULT_CURA_DEBUG_MODE
|
||||||
|
|
||||||
|
# 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
|
||||||
|
# CuraVersion.py.in template.
|
||||||
|
CuraSDKVersion = "6.2.0"
|
@ -66,6 +66,11 @@ class Arrange:
|
|||||||
continue
|
continue
|
||||||
vertices = vertices.getMinkowskiHull(Polygon.approximatedCircle(min_offset))
|
vertices = vertices.getMinkowskiHull(Polygon.approximatedCircle(min_offset))
|
||||||
points = copy.deepcopy(vertices._points)
|
points = copy.deepcopy(vertices._points)
|
||||||
|
|
||||||
|
# After scaling (like up to 0.1 mm) the node might not have points
|
||||||
|
if len(points) == 0:
|
||||||
|
continue
|
||||||
|
|
||||||
shape_arr = ShapeArray.fromPolygon(points, scale = scale)
|
shape_arr = ShapeArray.fromPolygon(points, scale = scale)
|
||||||
arranger.place(0, 0, shape_arr)
|
arranger.place(0, 0, shape_arr)
|
||||||
|
|
||||||
@ -212,11 +217,6 @@ class Arrange:
|
|||||||
prio_slice = self._priority[min_y:max_y, min_x:max_x]
|
prio_slice = self._priority[min_y:max_y, min_x:max_x]
|
||||||
prio_slice[new_occupied] = 999
|
prio_slice[new_occupied] = 999
|
||||||
|
|
||||||
# If you want to see how the rasterized arranger build plate looks like, uncomment this code
|
|
||||||
# numpy.set_printoptions(linewidth=500, edgeitems=200)
|
|
||||||
# print(self._occupied.shape)
|
|
||||||
# print(self._occupied)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def isEmpty(self):
|
def isEmpty(self):
|
||||||
return self._is_empty
|
return self._is_empty
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
# Copyright (c) 2018 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 UM.Application import Application
|
from UM.Application import Application
|
||||||
@ -48,7 +48,6 @@ class ArrangeArray:
|
|||||||
return self._count
|
return self._count
|
||||||
|
|
||||||
def get(self, index):
|
def get(self, index):
|
||||||
print(self._arrange)
|
|
||||||
return self._arrange[index]
|
return self._arrange[index]
|
||||||
|
|
||||||
def getFirstEmpty(self):
|
def getFirstEmpty(self):
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
# Copyright (c) 2017 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 UM.Application import Application
|
from UM.Application import Application
|
||||||
@ -39,10 +39,17 @@ class ArrangeObjectsJob(Job):
|
|||||||
|
|
||||||
arranger = Arrange.create(x = machine_width, y = machine_depth, fixed_nodes = self._fixed_nodes, min_offset = self._min_offset)
|
arranger = Arrange.create(x = machine_width, y = machine_depth, fixed_nodes = self._fixed_nodes, min_offset = self._min_offset)
|
||||||
|
|
||||||
|
# Build set to exclude children (those get arranged together with the parents).
|
||||||
|
included_as_child = set()
|
||||||
|
for node in self._nodes:
|
||||||
|
included_as_child.update(node.getAllChildren())
|
||||||
|
|
||||||
# Collect nodes to be placed
|
# Collect nodes to be placed
|
||||||
nodes_arr = [] # fill with (size, node, offset_shape_arr, hull_shape_arr)
|
nodes_arr = [] # fill with (size, node, offset_shape_arr, hull_shape_arr)
|
||||||
for node in self._nodes:
|
for node in self._nodes:
|
||||||
offset_shape_arr, hull_shape_arr = ShapeArray.fromNode(node, min_offset = self._min_offset)
|
if node in included_as_child:
|
||||||
|
continue
|
||||||
|
offset_shape_arr, hull_shape_arr = ShapeArray.fromNode(node, min_offset = self._min_offset, include_children = True)
|
||||||
if offset_shape_arr is None:
|
if offset_shape_arr is None:
|
||||||
Logger.log("w", "Node [%s] could not be converted to an array for arranging...", str(node))
|
Logger.log("w", "Node [%s] could not be converted to an array for arranging...", str(node))
|
||||||
continue
|
continue
|
||||||
|
@ -1,12 +1,18 @@
|
|||||||
|
#Copyright (c) 2019 Ultimaker B.V.
|
||||||
|
#Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
import numpy
|
import numpy
|
||||||
import copy
|
import copy
|
||||||
|
from typing import Optional, Tuple, TYPE_CHECKING
|
||||||
|
|
||||||
from UM.Math.Polygon import Polygon
|
from UM.Math.Polygon import Polygon
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from UM.Scene.SceneNode import SceneNode
|
||||||
|
|
||||||
## Polygon representation as an array for use with Arrange
|
## Polygon representation as an array for use with Arrange
|
||||||
class ShapeArray:
|
class ShapeArray:
|
||||||
def __init__(self, arr, offset_x, offset_y, scale = 1):
|
def __init__(self, arr: numpy.array, offset_x: float, offset_y: float, scale: float = 1) -> None:
|
||||||
self.arr = arr
|
self.arr = arr
|
||||||
self.offset_x = offset_x
|
self.offset_x = offset_x
|
||||||
self.offset_y = offset_y
|
self.offset_y = offset_y
|
||||||
@ -16,7 +22,7 @@ class ShapeArray:
|
|||||||
# \param vertices
|
# \param vertices
|
||||||
# \param scale scale the coordinates
|
# \param scale scale the coordinates
|
||||||
@classmethod
|
@classmethod
|
||||||
def fromPolygon(cls, vertices, scale = 1):
|
def fromPolygon(cls, vertices: numpy.array, scale: float = 1) -> "ShapeArray":
|
||||||
# scale
|
# scale
|
||||||
vertices = vertices * scale
|
vertices = vertices * scale
|
||||||
# flip y, x -> x, y
|
# flip y, x -> x, y
|
||||||
@ -42,7 +48,7 @@ class ShapeArray:
|
|||||||
# \param min_offset offset for the offset ShapeArray
|
# \param min_offset offset for the offset ShapeArray
|
||||||
# \param scale scale the coordinates
|
# \param scale scale the coordinates
|
||||||
@classmethod
|
@classmethod
|
||||||
def fromNode(cls, node, min_offset, scale = 0.5):
|
def fromNode(cls, node: "SceneNode", min_offset: float, scale: float = 0.5, include_children: bool = False) -> Tuple[Optional["ShapeArray"], Optional["ShapeArray"]]:
|
||||||
transform = node._transformation
|
transform = node._transformation
|
||||||
transform_x = transform._data[0][3]
|
transform_x = transform._data[0][3]
|
||||||
transform_y = transform._data[2][3]
|
transform_y = transform._data[2][3]
|
||||||
@ -52,6 +58,21 @@ class ShapeArray:
|
|||||||
return None, None
|
return None, None
|
||||||
# For one_at_a_time printing you need the convex hull head.
|
# For one_at_a_time printing you need the convex hull head.
|
||||||
hull_head_verts = node.callDecoration("getConvexHullHead") or hull_verts
|
hull_head_verts = node.callDecoration("getConvexHullHead") or hull_verts
|
||||||
|
if hull_head_verts is None:
|
||||||
|
hull_head_verts = Polygon()
|
||||||
|
|
||||||
|
# If the child-nodes are included, adjust convex hulls as well:
|
||||||
|
if include_children:
|
||||||
|
children = node.getAllChildren()
|
||||||
|
if not children is None:
|
||||||
|
for child in children:
|
||||||
|
# 'Inefficient' combination of convex hulls through known code rather than mess it up:
|
||||||
|
child_hull = child.callDecoration("getConvexHull")
|
||||||
|
if not child_hull is None:
|
||||||
|
hull_verts = hull_verts.unionConvexHulls(child_hull)
|
||||||
|
child_hull_head = child.callDecoration("getConvexHullHead") or child_hull
|
||||||
|
if not child_hull_head is None:
|
||||||
|
hull_head_verts = hull_head_verts.unionConvexHulls(child_hull_head)
|
||||||
|
|
||||||
offset_verts = hull_head_verts.getMinkowskiHull(Polygon.approximatedCircle(min_offset))
|
offset_verts = hull_head_verts.getMinkowskiHull(Polygon.approximatedCircle(min_offset))
|
||||||
offset_points = copy.deepcopy(offset_verts._points) # x, y
|
offset_points = copy.deepcopy(offset_verts._points) # x, y
|
||||||
@ -73,7 +94,7 @@ class ShapeArray:
|
|||||||
# \param shape numpy format shape, [x-size, y-size]
|
# \param shape numpy format shape, [x-size, y-size]
|
||||||
# \param vertices
|
# \param vertices
|
||||||
@classmethod
|
@classmethod
|
||||||
def arrayFromPolygon(cls, shape, vertices):
|
def arrayFromPolygon(cls, shape: Tuple[int, int], vertices: numpy.array) -> numpy.array:
|
||||||
base_array = numpy.zeros(shape, dtype = numpy.int32) # Initialize your array of zeros
|
base_array = numpy.zeros(shape, dtype = numpy.int32) # Initialize your array of zeros
|
||||||
|
|
||||||
fill = numpy.ones(base_array.shape) * True # Initialize boolean array defining shape fill
|
fill = numpy.ones(base_array.shape) * True # Initialize boolean array defining shape fill
|
||||||
@ -96,9 +117,9 @@ class ShapeArray:
|
|||||||
# \param p2 2-tuple with x, y for point 2
|
# \param p2 2-tuple with x, y for point 2
|
||||||
# \param base_array boolean array to project the line on
|
# \param base_array boolean array to project the line on
|
||||||
@classmethod
|
@classmethod
|
||||||
def _check(cls, p1, p2, base_array):
|
def _check(cls, p1: numpy.array, p2: numpy.array, base_array: numpy.array) -> bool:
|
||||||
if p1[0] == p2[0] and p1[1] == p2[1]:
|
if p1[0] == p2[0] and p1[1] == p2[1]:
|
||||||
return
|
return False
|
||||||
idxs = numpy.indices(base_array.shape) # Create 3D array of indices
|
idxs = numpy.indices(base_array.shape) # Create 3D array of indices
|
||||||
|
|
||||||
p1 = p1.astype(float)
|
p1 = p1.astype(float)
|
||||||
@ -117,4 +138,3 @@ class ShapeArray:
|
|||||||
max_col_idx = (idxs[0] - p1[0]) / (p2[0] - p1[0]) * (p2[1] - p1[1]) + p1[1]
|
max_col_idx = (idxs[0] - p1[0]) / (p2[0] - p1[0]) * (p2[1] - p1[1]) + p1[1]
|
||||||
sign = numpy.sign(p2[0] - p1[0])
|
sign = numpy.sign(p2[0] - p1[0])
|
||||||
return idxs[1] * sign <= max_col_idx * sign
|
return idxs[1] * sign <= max_col_idx * sign
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
# Copyright (c) 2016 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 PyQt5.QtCore import QTimer
|
from PyQt5.QtCore import QTimer
|
||||||
@ -16,9 +16,10 @@ class AutoSave:
|
|||||||
self._application.getPreferences().addPreference("cura/autosave_delay", 1000 * 10)
|
self._application.getPreferences().addPreference("cura/autosave_delay", 1000 * 10)
|
||||||
|
|
||||||
self._change_timer = QTimer()
|
self._change_timer = QTimer()
|
||||||
self._change_timer.setInterval(self._application.getPreferences().getValue("cura/autosave_delay"))
|
self._change_timer.setInterval(int(self._application.getPreferences().getValue("cura/autosave_delay")))
|
||||||
self._change_timer.setSingleShot(True)
|
self._change_timer.setSingleShot(True)
|
||||||
|
|
||||||
|
self._enabled = True
|
||||||
self._saving = False
|
self._saving = False
|
||||||
|
|
||||||
def initialize(self):
|
def initialize(self):
|
||||||
@ -32,6 +33,13 @@ class AutoSave:
|
|||||||
if not self._saving:
|
if not self._saving:
|
||||||
self._change_timer.start()
|
self._change_timer.start()
|
||||||
|
|
||||||
|
def setEnabled(self, enabled: bool) -> None:
|
||||||
|
self._enabled = enabled
|
||||||
|
if self._enabled:
|
||||||
|
self._change_timer.start()
|
||||||
|
else:
|
||||||
|
self._change_timer.stop()
|
||||||
|
|
||||||
def _onGlobalStackChanged(self):
|
def _onGlobalStackChanged(self):
|
||||||
if self._global_stack:
|
if self._global_stack:
|
||||||
self._global_stack.propertyChanged.disconnect(self._triggerTimer)
|
self._global_stack.propertyChanged.disconnect(self._triggerTimer)
|
||||||
|
@ -46,10 +46,11 @@ class Backup:
|
|||||||
|
|
||||||
# We copy the preferences file to the user data directory in Linux as it's in a different location there.
|
# We copy the preferences file to the user data directory in Linux as it's in a different location there.
|
||||||
# When restoring a backup on Linux, we move it back.
|
# When restoring a backup on Linux, we move it back.
|
||||||
if Platform.isLinux():
|
if Platform.isLinux(): #TODO: This should check for the config directory not being the same as the data directory, rather than hard-coding that to Linux systems.
|
||||||
preferences_file_name = self._application.getApplicationName()
|
preferences_file_name = self._application.getApplicationName()
|
||||||
preferences_file = Resources.getPath(Resources.Preferences, "{}.cfg".format(preferences_file_name))
|
preferences_file = Resources.getPath(Resources.Preferences, "{}.cfg".format(preferences_file_name))
|
||||||
backup_preferences_file = os.path.join(version_data_dir, "{}.cfg".format(preferences_file_name))
|
backup_preferences_file = os.path.join(version_data_dir, "{}.cfg".format(preferences_file_name))
|
||||||
|
if os.path.exists(preferences_file) and (not os.path.exists(backup_preferences_file) or not os.path.samefile(preferences_file, backup_preferences_file)):
|
||||||
Logger.log("d", "Copying preferences file from %s to %s", preferences_file, backup_preferences_file)
|
Logger.log("d", "Copying preferences file from %s to %s", preferences_file, backup_preferences_file)
|
||||||
shutil.copyfile(preferences_file, backup_preferences_file)
|
shutil.copyfile(preferences_file, backup_preferences_file)
|
||||||
|
|
||||||
@ -115,12 +116,13 @@ class Backup:
|
|||||||
|
|
||||||
current_version = self._application.getVersion()
|
current_version = self._application.getVersion()
|
||||||
version_to_restore = self.meta_data.get("cura_release", "master")
|
version_to_restore = self.meta_data.get("cura_release", "master")
|
||||||
if current_version != version_to_restore:
|
|
||||||
# Cannot restore version older or newer than current because settings might have changed.
|
if current_version < version_to_restore:
|
||||||
# Restoring this will cause a lot of issues so we don't allow this for now.
|
# Cannot restore version newer than current because settings might have changed.
|
||||||
|
Logger.log("d", "Tried to restore a Cura backup of version {version_to_restore} with cura version {current_version}".format(version_to_restore = version_to_restore, current_version = current_version))
|
||||||
self._showMessage(
|
self._showMessage(
|
||||||
self.catalog.i18nc("@info:backup_failed",
|
self.catalog.i18nc("@info:backup_failed",
|
||||||
"Tried to restore a Cura backup that does not match your current version."))
|
"Tried to restore a Cura backup that is higher than the current version."))
|
||||||
return False
|
return False
|
||||||
|
|
||||||
version_data_dir = Resources.getDataStoragePath()
|
version_data_dir = Resources.getDataStoragePath()
|
||||||
@ -146,5 +148,9 @@ class Backup:
|
|||||||
Logger.log("d", "Removing current data in location: %s", target_path)
|
Logger.log("d", "Removing current data in location: %s", target_path)
|
||||||
Resources.factoryReset()
|
Resources.factoryReset()
|
||||||
Logger.log("d", "Extracting backup to location: %s", target_path)
|
Logger.log("d", "Extracting backup to location: %s", target_path)
|
||||||
|
try:
|
||||||
archive.extractall(target_path)
|
archive.extractall(target_path)
|
||||||
|
except PermissionError:
|
||||||
|
Logger.logException("e", "Unable to extract the backup due to permission errors")
|
||||||
|
return False
|
||||||
return True
|
return True
|
||||||
|
@ -51,8 +51,18 @@ class BackupsManager:
|
|||||||
## Here we try to disable the auto-save plug-in as it might interfere with
|
## Here we try to disable the auto-save plug-in as it might interfere with
|
||||||
# restoring a back-up.
|
# restoring a back-up.
|
||||||
def _disableAutoSave(self) -> None:
|
def _disableAutoSave(self) -> None:
|
||||||
self._application.setSaveDataEnabled(False)
|
auto_save = self._application.getAutoSave()
|
||||||
|
# The auto save is only not created if the application has not yet started.
|
||||||
|
if auto_save:
|
||||||
|
auto_save.setEnabled(False)
|
||||||
|
else:
|
||||||
|
Logger.log("e", "Unable to disable the autosave as application init has not been completed")
|
||||||
|
|
||||||
## Re-enable auto-save after we're done.
|
## Re-enable auto-save after we're done.
|
||||||
def _enableAutoSave(self) -> None:
|
def _enableAutoSave(self) -> None:
|
||||||
self._application.setSaveDataEnabled(True)
|
auto_save = self._application.getAutoSave()
|
||||||
|
# The auto save is only not created if the application has not yet started.
|
||||||
|
if auto_save:
|
||||||
|
auto_save.setEnabled(True)
|
||||||
|
else:
|
||||||
|
Logger.log("e", "Unable to enable the autosave as application init has not been completed")
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -1,29 +0,0 @@
|
|||||||
from PyQt5.QtGui import QImage
|
|
||||||
from PyQt5.QtQuick import QQuickImageProvider
|
|
||||||
from PyQt5.QtCore import QSize
|
|
||||||
|
|
||||||
from UM.Application import Application
|
|
||||||
|
|
||||||
|
|
||||||
class CameraImageProvider(QQuickImageProvider):
|
|
||||||
def __init__(self):
|
|
||||||
super().__init__(QQuickImageProvider.Image)
|
|
||||||
|
|
||||||
## Request a new image.
|
|
||||||
def requestImage(self, id, size):
|
|
||||||
for output_device in Application.getInstance().getOutputDeviceManager().getOutputDevices():
|
|
||||||
try:
|
|
||||||
image = output_device.activePrinter.camera.getImage()
|
|
||||||
if image.isNull():
|
|
||||||
image = QImage()
|
|
||||||
|
|
||||||
return image, QSize(15, 15)
|
|
||||||
except AttributeError:
|
|
||||||
try:
|
|
||||||
image = output_device.activeCamera.getImage()
|
|
||||||
|
|
||||||
return image, QSize(15, 15)
|
|
||||||
except AttributeError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
return QImage(), QSize(15, 15)
|
|
@ -36,18 +36,14 @@ else:
|
|||||||
except ImportError:
|
except ImportError:
|
||||||
CuraDebugMode = False # [CodeStyle: Reflecting imported value]
|
CuraDebugMode = False # [CodeStyle: Reflecting imported value]
|
||||||
|
|
||||||
# List of exceptions that should be considered "fatal" and abort the program.
|
# List of exceptions that should not be considered "fatal" and abort the program.
|
||||||
# These are primarily some exception types that we simply cannot really recover from
|
# These are primarily some exception types that we simply skip
|
||||||
# (MemoryError and SystemError) and exceptions that indicate grave errors in the
|
skip_exception_types = [
|
||||||
# code that cause the Python interpreter to fail (SyntaxError, ImportError).
|
SystemExit,
|
||||||
fatal_exception_types = [
|
KeyboardInterrupt,
|
||||||
MemoryError,
|
GeneratorExit
|
||||||
SyntaxError,
|
|
||||||
ImportError,
|
|
||||||
SystemError,
|
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class CrashHandler:
|
class CrashHandler:
|
||||||
crash_url = "https://stats.ultimaker.com/api/cura"
|
crash_url = "https://stats.ultimaker.com/api/cura"
|
||||||
|
|
||||||
@ -70,7 +66,7 @@ class CrashHandler:
|
|||||||
# If Cura has fully started, we only show fatal errors.
|
# If Cura has fully started, we only show fatal errors.
|
||||||
# If Cura has not fully started yet, we always show the early crash dialog. Otherwise, Cura will just crash
|
# If Cura has not fully started yet, we always show the early crash dialog. Otherwise, Cura will just crash
|
||||||
# without any information.
|
# without any information.
|
||||||
if has_started and exception_type not in fatal_exception_types:
|
if has_started and exception_type in skip_exception_types:
|
||||||
return
|
return
|
||||||
|
|
||||||
if not has_started:
|
if not has_started:
|
||||||
@ -323,7 +319,8 @@ class CrashHandler:
|
|||||||
|
|
||||||
def _userDescriptionWidget(self):
|
def _userDescriptionWidget(self):
|
||||||
group = QGroupBox()
|
group = QGroupBox()
|
||||||
group.setTitle(catalog.i18nc("@title:groupbox", "User description"))
|
group.setTitle(catalog.i18nc("@title:groupbox", "User description" +
|
||||||
|
" (Note: Developers may not speak your language, please use English if possible)"))
|
||||||
layout = QVBoxLayout()
|
layout = QVBoxLayout()
|
||||||
|
|
||||||
# When sending the report, the user comments will be collected
|
# When sending the report, the user comments will be collected
|
||||||
@ -387,7 +384,7 @@ class CrashHandler:
|
|||||||
Application.getInstance().callLater(self._show)
|
Application.getInstance().callLater(self._show)
|
||||||
|
|
||||||
def _show(self):
|
def _show(self):
|
||||||
# When the exception is not in the fatal_exception_types list, the dialog is not created, so we don't need to show it
|
# When the exception is in the skip_exception_types list, the dialog is not created, so we don't need to show it
|
||||||
if self.dialog:
|
if self.dialog:
|
||||||
self.dialog.exec_()
|
self.dialog.exec_()
|
||||||
os._exit(1)
|
os._exit(1)
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
from PyQt5.QtCore import QObject, QUrl
|
from PyQt5.QtCore import QObject, QUrl
|
||||||
from PyQt5.QtGui import QDesktopServices
|
from PyQt5.QtGui import QDesktopServices
|
||||||
from typing import List, TYPE_CHECKING
|
from typing import List, cast
|
||||||
|
|
||||||
from UM.Event import CallFunctionEvent
|
from UM.Event import CallFunctionEvent
|
||||||
from UM.FlameProfiler import pyqtSlot
|
from UM.FlameProfiler import pyqtSlot
|
||||||
@ -23,9 +23,8 @@ from cura.Settings.ExtruderManager import ExtruderManager
|
|||||||
from cura.Operations.SetBuildPlateNumberOperation import SetBuildPlateNumberOperation
|
from cura.Operations.SetBuildPlateNumberOperation import SetBuildPlateNumberOperation
|
||||||
|
|
||||||
from UM.Logger import Logger
|
from UM.Logger import Logger
|
||||||
|
from UM.Scene.SceneNode import SceneNode
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
from UM.Scene.SceneNode import SceneNode
|
|
||||||
|
|
||||||
class CuraActions(QObject):
|
class CuraActions(QObject):
|
||||||
def __init__(self, parent: QObject = None) -> None:
|
def __init__(self, parent: QObject = None) -> None:
|
||||||
@ -36,12 +35,12 @@ class CuraActions(QObject):
|
|||||||
# Starting a web browser from a signal handler connected to a menu will crash on windows.
|
# Starting a web browser from a signal handler connected to a menu will crash on windows.
|
||||||
# So instead, defer the call to the next run of the event loop, since that does work.
|
# So instead, defer the call to the next run of the event loop, since that does work.
|
||||||
# Note that weirdly enough, only signal handlers that open a web browser fail like that.
|
# Note that weirdly enough, only signal handlers that open a web browser fail like that.
|
||||||
event = CallFunctionEvent(self._openUrl, [QUrl("http://ultimaker.com/en/support/software")], {})
|
event = CallFunctionEvent(self._openUrl, [QUrl("https://ultimaker.com/en/resources/manuals/software")], {})
|
||||||
cura.CuraApplication.CuraApplication.getInstance().functionEvent(event)
|
cura.CuraApplication.CuraApplication.getInstance().functionEvent(event)
|
||||||
|
|
||||||
@pyqtSlot()
|
@pyqtSlot()
|
||||||
def openBugReportPage(self) -> None:
|
def openBugReportPage(self) -> None:
|
||||||
event = CallFunctionEvent(self._openUrl, [QUrl("http://github.com/Ultimaker/Cura/issues")], {})
|
event = CallFunctionEvent(self._openUrl, [QUrl("https://github.com/Ultimaker/Cura/issues")], {})
|
||||||
cura.CuraApplication.CuraApplication.getInstance().functionEvent(event)
|
cura.CuraApplication.CuraApplication.getInstance().functionEvent(event)
|
||||||
|
|
||||||
## Reset camera position and direction to default
|
## Reset camera position and direction to default
|
||||||
@ -61,8 +60,10 @@ class CuraActions(QObject):
|
|||||||
operation = GroupedOperation()
|
operation = GroupedOperation()
|
||||||
for node in Selection.getAllSelectedObjects():
|
for node in Selection.getAllSelectedObjects():
|
||||||
current_node = node
|
current_node = node
|
||||||
while current_node.getParent() and current_node.getParent().callDecoration("isGroup"):
|
parent_node = current_node.getParent()
|
||||||
current_node = current_node.getParent()
|
while parent_node and parent_node.callDecoration("isGroup"):
|
||||||
|
current_node = parent_node
|
||||||
|
parent_node = current_node.getParent()
|
||||||
|
|
||||||
# This was formerly done with SetTransformOperation but because of
|
# This was formerly done with SetTransformOperation but because of
|
||||||
# unpredictable matrix deconstruction it was possible that mirrors
|
# unpredictable matrix deconstruction it was possible that mirrors
|
||||||
@ -150,13 +151,13 @@ class CuraActions(QObject):
|
|||||||
|
|
||||||
root = cura.CuraApplication.CuraApplication.getInstance().getController().getScene().getRoot()
|
root = cura.CuraApplication.CuraApplication.getInstance().getController().getScene().getRoot()
|
||||||
|
|
||||||
nodes_to_change = []
|
nodes_to_change = [] # type: List[SceneNode]
|
||||||
for node in Selection.getAllSelectedObjects():
|
for node in Selection.getAllSelectedObjects():
|
||||||
parent_node = node # Find the parent node to change instead
|
parent_node = node # Find the parent node to change instead
|
||||||
while parent_node.getParent() != root:
|
while parent_node.getParent() != root:
|
||||||
parent_node = parent_node.getParent()
|
parent_node = cast(SceneNode, parent_node.getParent())
|
||||||
|
|
||||||
for single_node in BreadthFirstIterator(parent_node): #type: ignore #Ignore type error because iter() should get called automatically by Python syntax.
|
for single_node in BreadthFirstIterator(parent_node): # type: ignore #Ignore type error because iter() should get called automatically by Python syntax.
|
||||||
nodes_to_change.append(single_node)
|
nodes_to_change.append(single_node)
|
||||||
|
|
||||||
if not nodes_to_change:
|
if not nodes_to_change:
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -1,9 +1,11 @@
|
|||||||
# Copyright (c) 2015 Ultimaker B.V.
|
# Copyright (c) 2018 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.
|
||||||
|
|
||||||
|
CuraAppName = "@CURA_APP_NAME@"
|
||||||
|
CuraAppDisplayName = "@CURA_APP_DISPLAY_NAME@"
|
||||||
CuraVersion = "@CURA_VERSION@"
|
CuraVersion = "@CURA_VERSION@"
|
||||||
CuraBuildType = "@CURA_BUILDTYPE@"
|
CuraBuildType = "@CURA_BUILDTYPE@"
|
||||||
CuraDebugMode = True if "@_cura_debugmode@" == "ON" else False
|
CuraDebugMode = True if "@_cura_debugmode@" == "ON" else False
|
||||||
CuraSDKVersion = "@CURA_SDK_VERSION@"
|
|
||||||
CuraCloudAPIRoot = "@CURA_CLOUD_API_ROOT@"
|
CuraCloudAPIRoot = "@CURA_CLOUD_API_ROOT@"
|
||||||
CuraCloudAPIVersion = "@CURA_CLOUD_API_VERSION@"
|
CuraCloudAPIVersion = "@CURA_CLOUD_API_VERSION@"
|
||||||
|
CuraCloudAccountAPIRoot = "@CURA_CLOUD_ACCOUNT_API_ROOT@"
|
||||||
|
34
cura/CuraView.py
Normal file
34
cura/CuraView.py
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
# Copyright (c) 2018 Ultimaker B.V.
|
||||||
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
|
from PyQt5.QtCore import pyqtProperty, QUrl
|
||||||
|
|
||||||
|
from UM.Resources import Resources
|
||||||
|
from UM.View.View import View
|
||||||
|
|
||||||
|
from cura.CuraApplication import CuraApplication
|
||||||
|
|
||||||
|
|
||||||
|
# Since Cura has a few pre-defined "space claims" for the locations of certain components, we've provided some structure
|
||||||
|
# to indicate this.
|
||||||
|
# MainComponent works in the same way the MainComponent of a stage.
|
||||||
|
# the stageMenuComponent returns an item that should be used somehwere in the stage menu. It's up to the active stage
|
||||||
|
# to actually do something with this.
|
||||||
|
class CuraView(View):
|
||||||
|
def __init__(self, parent = None, use_empty_menu_placeholder: bool = False) -> None:
|
||||||
|
super().__init__(parent)
|
||||||
|
|
||||||
|
self._empty_menu_placeholder_url = QUrl.fromLocalFile(Resources.getPath(CuraApplication.ResourceTypes.QmlFiles,
|
||||||
|
"EmptyViewMenuComponent.qml"))
|
||||||
|
self._use_empty_menu_placeholder = use_empty_menu_placeholder
|
||||||
|
|
||||||
|
@pyqtProperty(QUrl, constant = True)
|
||||||
|
def mainComponent(self) -> QUrl:
|
||||||
|
return self.getDisplayComponent("main")
|
||||||
|
|
||||||
|
@pyqtProperty(QUrl, constant = True)
|
||||||
|
def stageMenuComponent(self) -> QUrl:
|
||||||
|
url = self.getDisplayComponent("menu")
|
||||||
|
if not url.toString() and self._use_empty_menu_placeholder:
|
||||||
|
url = self._empty_menu_placeholder_url
|
||||||
|
return url
|
@ -7,43 +7,36 @@ from UM.Mesh.MeshBuilder import MeshBuilder
|
|||||||
from .LayerData import LayerData
|
from .LayerData import LayerData
|
||||||
|
|
||||||
import numpy
|
import numpy
|
||||||
|
from typing import Dict, Optional
|
||||||
|
|
||||||
|
|
||||||
## Builder class for constructing a LayerData object
|
## Builder class for constructing a LayerData object
|
||||||
class LayerDataBuilder(MeshBuilder):
|
class LayerDataBuilder(MeshBuilder):
|
||||||
def __init__(self):
|
def __init__(self) -> None:
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self._layers = {}
|
self._layers = {} # type: Dict[int, Layer]
|
||||||
self._element_counts = {}
|
self._element_counts = {} # type: Dict[int, int]
|
||||||
|
|
||||||
def addLayer(self, layer):
|
def addLayer(self, layer: int) -> None:
|
||||||
if layer not in self._layers:
|
if layer not in self._layers:
|
||||||
self._layers[layer] = Layer(layer)
|
self._layers[layer] = Layer(layer)
|
||||||
|
|
||||||
def addPolygon(self, layer, polygon_type, data, line_width, line_thickness, line_feedrate):
|
def getLayer(self, layer: int) -> Optional[Layer]:
|
||||||
if layer not in self._layers:
|
return self._layers.get(layer)
|
||||||
self.addLayer(layer)
|
|
||||||
|
|
||||||
p = LayerPolygon(self, polygon_type, data, line_width, line_thickness, line_feedrate)
|
def getLayers(self) -> Dict[int, Layer]:
|
||||||
self._layers[layer].polygons.append(p)
|
|
||||||
|
|
||||||
def getLayer(self, layer):
|
|
||||||
if layer in self._layers:
|
|
||||||
return self._layers[layer]
|
|
||||||
|
|
||||||
def getLayers(self):
|
|
||||||
return self._layers
|
return self._layers
|
||||||
|
|
||||||
def getElementCounts(self):
|
def getElementCounts(self) -> Dict[int, int]:
|
||||||
return self._element_counts
|
return self._element_counts
|
||||||
|
|
||||||
def setLayerHeight(self, layer, height):
|
def setLayerHeight(self, layer: int, height: float) -> None:
|
||||||
if layer not in self._layers:
|
if layer not in self._layers:
|
||||||
self.addLayer(layer)
|
self.addLayer(layer)
|
||||||
|
|
||||||
self._layers[layer].setHeight(height)
|
self._layers[layer].setHeight(height)
|
||||||
|
|
||||||
def setLayerThickness(self, layer, thickness):
|
def setLayerThickness(self, layer: int, thickness: float) -> None:
|
||||||
if layer not in self._layers:
|
if layer not in self._layers:
|
||||||
self.addLayer(layer)
|
self.addLayer(layer)
|
||||||
|
|
||||||
@ -71,7 +64,7 @@ class LayerDataBuilder(MeshBuilder):
|
|||||||
vertex_offset = 0
|
vertex_offset = 0
|
||||||
index_offset = 0
|
index_offset = 0
|
||||||
for layer, data in sorted(self._layers.items()):
|
for layer, data in sorted(self._layers.items()):
|
||||||
( vertex_offset, index_offset ) = data.build( vertex_offset, index_offset, vertices, colors, line_dimensions, feedrates, extruders, line_types, indices)
|
vertex_offset, index_offset = data.build(vertex_offset, index_offset, vertices, colors, line_dimensions, feedrates, extruders, line_types, indices)
|
||||||
self._element_counts[layer] = data.elementCount
|
self._element_counts[layer] = data.elementCount
|
||||||
|
|
||||||
self.addVertices(vertices)
|
self.addVertices(vertices)
|
||||||
|
@ -1,13 +1,25 @@
|
|||||||
|
# Copyright (c) 2019 Ultimaker B.V.
|
||||||
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
|
from typing import Optional
|
||||||
from UM.Scene.SceneNodeDecorator import SceneNodeDecorator
|
from UM.Scene.SceneNodeDecorator import SceneNodeDecorator
|
||||||
|
|
||||||
|
from cura.LayerData import LayerData
|
||||||
|
|
||||||
|
|
||||||
## Simple decorator to indicate a scene node holds layer data.
|
## Simple decorator to indicate a scene node holds layer data.
|
||||||
class LayerDataDecorator(SceneNodeDecorator):
|
class LayerDataDecorator(SceneNodeDecorator):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self._layer_data = None
|
self._layer_data = None # type: Optional[LayerData]
|
||||||
|
|
||||||
def getLayerData(self):
|
def getLayerData(self) -> Optional["LayerData"]:
|
||||||
return self._layer_data
|
return self._layer_data
|
||||||
|
|
||||||
def setLayerData(self, layer_data):
|
def setLayerData(self, layer_data: LayerData) -> None:
|
||||||
self._layer_data = layer_data
|
self._layer_data = layer_data
|
||||||
|
|
||||||
|
def __deepcopy__(self, memo) -> "LayerDataDecorator":
|
||||||
|
copied_decorator = LayerDataDecorator()
|
||||||
|
copied_decorator._layer_data = self._layer_data
|
||||||
|
return copied_decorator
|
||||||
|
@ -1,10 +1,12 @@
|
|||||||
# Copyright (c) 2017 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 UM.Application import Application
|
from UM.Application import Application
|
||||||
from typing import Any
|
from typing import Any, Optional
|
||||||
import numpy
|
import numpy
|
||||||
|
|
||||||
|
from UM.Logger import Logger
|
||||||
|
|
||||||
|
|
||||||
class LayerPolygon:
|
class LayerPolygon:
|
||||||
NoneType = 0
|
NoneType = 0
|
||||||
@ -18,22 +20,24 @@ class LayerPolygon:
|
|||||||
MoveCombingType = 8
|
MoveCombingType = 8
|
||||||
MoveRetractionType = 9
|
MoveRetractionType = 9
|
||||||
SupportInterfaceType = 10
|
SupportInterfaceType = 10
|
||||||
__number_of_types = 11
|
PrimeTowerType = 11
|
||||||
|
__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)
|
||||||
|
|
||||||
## LayerPolygon, used in ProcessSlicedLayersJob
|
## LayerPolygon, used in ProcessSlicedLayersJob
|
||||||
# \param extruder
|
# \param extruder The position of the extruder
|
||||||
# \param line_types array with line_types
|
# \param line_types array with line_types
|
||||||
# \param data new_points
|
# \param data new_points
|
||||||
# \param line_widths array with line widths
|
# \param line_widths array with line widths
|
||||||
# \param line_thicknesses: array with type as index and thickness as value
|
# \param line_thicknesses: array with type as index and thickness as value
|
||||||
# \param line_feedrates array with line feedrates
|
# \param line_feedrates array with line feedrates
|
||||||
def __init__(self, extruder, line_types, data, line_widths, line_thicknesses, line_feedrates):
|
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:
|
||||||
self._extruder = extruder
|
self._extruder = extruder
|
||||||
self._types = line_types
|
self._types = line_types
|
||||||
for i in range(len(self._types)):
|
for i in range(len(self._types)):
|
||||||
if self._types[i] >= self.__number_of_types: #Got faulty line data from the engine.
|
if self._types[i] >= self.__number_of_types: # Got faulty line data from the engine.
|
||||||
|
Logger.log("w", "Found an unknown line type: %s", i)
|
||||||
self._types[i] = self.NoneType
|
self._types[i] = self.NoneType
|
||||||
self._data = data
|
self._data = data
|
||||||
self._line_widths = line_widths
|
self._line_widths = line_widths
|
||||||
@ -53,23 +57,23 @@ class LayerPolygon:
|
|||||||
# 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 alot of memory usage.
|
# re-used and can save alot of memory usage.
|
||||||
self._color_map = LayerPolygon.getColorMap()
|
self._color_map = LayerPolygon.getColorMap()
|
||||||
self._colors = self._color_map[self._types]
|
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._isInfillOrSkinTypeMap = numpy.array([0, 0, 0, 1, 0, 0, 1, 1, 0, 0, 1], dtype=numpy.bool)
|
self._isInfillOrSkinTypeMap = numpy.array([0, 0, 0, 1, 0, 0, 1, 1, 0, 0, 1], dtype = numpy.bool)
|
||||||
|
|
||||||
self._build_cache_line_mesh_mask = None
|
self._build_cache_line_mesh_mask = None # type: Optional[numpy.ndarray]
|
||||||
self._build_cache_needed_points = None
|
self._build_cache_needed_points = None # type: Optional[numpy.ndarray]
|
||||||
|
|
||||||
def buildCache(self):
|
def buildCache(self) -> None:
|
||||||
# For the line mesh we do not draw Infill or Jumps. Therefore those lines are filtered out.
|
# For the line mesh we do not draw Infill or Jumps. Therefore those lines are filtered out.
|
||||||
self._build_cache_line_mesh_mask = numpy.ones(self._jump_mask.shape, dtype=bool)
|
self._build_cache_line_mesh_mask = numpy.ones(self._jump_mask.shape, dtype = bool)
|
||||||
mesh_line_count = numpy.sum(self._build_cache_line_mesh_mask)
|
mesh_line_count = numpy.sum(self._build_cache_line_mesh_mask)
|
||||||
self._index_begin = 0
|
self._index_begin = 0
|
||||||
self._index_end = mesh_line_count
|
self._index_end = mesh_line_count
|
||||||
|
|
||||||
self._build_cache_needed_points = numpy.ones((len(self._types), 2), dtype=numpy.bool)
|
self._build_cache_needed_points = numpy.ones((len(self._types), 2), dtype = numpy.bool)
|
||||||
# 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
|
||||||
@ -90,10 +94,14 @@ class LayerPolygon:
|
|||||||
# \param extruders : vertex numpy array to be filled
|
# \param extruders : vertex numpy array to be filled
|
||||||
# \param line_types : vertex numpy array to be filled
|
# \param line_types : vertex numpy array to be filled
|
||||||
# \param indices : index numpy array to be filled
|
# \param indices : index numpy array to be filled
|
||||||
def build(self, vertex_offset, index_offset, vertices, colors, line_dimensions, feedrates, extruders, line_types, indices):
|
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:
|
||||||
if self._build_cache_line_mesh_mask is None or self._build_cache_needed_points is None:
|
if self._build_cache_line_mesh_mask is None or self._build_cache_needed_points is None:
|
||||||
self.buildCache()
|
self.buildCache()
|
||||||
|
|
||||||
|
if self._build_cache_line_mesh_mask is None or self._build_cache_needed_points is None:
|
||||||
|
Logger.log("w", "Failed to build cache for layer polygon")
|
||||||
|
return
|
||||||
|
|
||||||
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
|
||||||
|
|
||||||
@ -128,9 +136,9 @@ class LayerPolygon:
|
|||||||
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 neccecarily True value of needed_points_list[0,0] which causes an unwanted +1 in cumsum above.
|
# The -1 is to compensate for the neccecarily 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])
|
||||||
@ -236,7 +244,8 @@ class LayerPolygon:
|
|||||||
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
|
||||||
])
|
])
|
||||||
|
|
||||||
return cls.__color_map
|
return cls.__color_map
|
||||||
|
@ -1,13 +1,14 @@
|
|||||||
# Copyright (c) 2016 Ultimaker B.V.
|
# Copyright (c) 2016 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 PyQt5.QtCore import QObject, pyqtSlot, pyqtProperty, pyqtSignal
|
import os
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from PyQt5.QtCore import QObject, QUrl, pyqtSlot, pyqtProperty, pyqtSignal
|
||||||
|
|
||||||
|
from UM.Logger import Logger
|
||||||
from UM.PluginObject import PluginObject
|
from UM.PluginObject import PluginObject
|
||||||
from UM.PluginRegistry import PluginRegistry
|
from UM.PluginRegistry import PluginRegistry
|
||||||
from UM.Application import Application
|
|
||||||
|
|
||||||
import os
|
|
||||||
|
|
||||||
|
|
||||||
## Machine actions are actions that are added to a specific machine type. Examples of such actions are
|
## Machine actions are actions that are added to a specific machine type. Examples of such actions are
|
||||||
@ -19,7 +20,7 @@ class MachineAction(QObject, PluginObject):
|
|||||||
## Create a new Machine action.
|
## Create a new Machine action.
|
||||||
# \param key unique key of the machine action
|
# \param key unique key of the machine action
|
||||||
# \param label Human readable label used to identify the machine action.
|
# \param label Human readable label used to identify the machine action.
|
||||||
def __init__(self, key, label = ""):
|
def __init__(self, key: str, label: str = "") -> None:
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self._key = key
|
self._key = key
|
||||||
self._label = label
|
self._label = label
|
||||||
@ -30,14 +31,20 @@ class MachineAction(QObject, PluginObject):
|
|||||||
labelChanged = pyqtSignal()
|
labelChanged = pyqtSignal()
|
||||||
onFinished = pyqtSignal()
|
onFinished = pyqtSignal()
|
||||||
|
|
||||||
def getKey(self):
|
def getKey(self) -> str:
|
||||||
return self._key
|
return self._key
|
||||||
|
|
||||||
|
## Whether this action needs to ask the user anything.
|
||||||
|
# If not, we shouldn't present the user with certain screens which otherwise show up.
|
||||||
|
# Defaults to true to be in line with the old behaviour.
|
||||||
|
def needsUserInteraction(self) -> bool:
|
||||||
|
return True
|
||||||
|
|
||||||
@pyqtProperty(str, notify = labelChanged)
|
@pyqtProperty(str, notify = labelChanged)
|
||||||
def label(self):
|
def label(self) -> str:
|
||||||
return self._label
|
return self._label
|
||||||
|
|
||||||
def setLabel(self, label):
|
def setLabel(self, label: str) -> None:
|
||||||
if self._label != label:
|
if self._label != label:
|
||||||
self._label = label
|
self._label = label
|
||||||
self.labelChanged.emit()
|
self.labelChanged.emit()
|
||||||
@ -46,32 +53,46 @@ class MachineAction(QObject, PluginObject):
|
|||||||
# This should not be re-implemented by child classes, instead re-implement _reset.
|
# This should not be re-implemented by child classes, instead re-implement _reset.
|
||||||
# /sa _reset
|
# /sa _reset
|
||||||
@pyqtSlot()
|
@pyqtSlot()
|
||||||
def reset(self):
|
def reset(self) -> None:
|
||||||
self._finished = False
|
self._finished = False
|
||||||
self._reset()
|
self._reset()
|
||||||
|
|
||||||
## Protected implementation of reset.
|
## Protected implementation of reset.
|
||||||
# /sa reset()
|
# /sa reset()
|
||||||
def _reset(self):
|
def _reset(self) -> None:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@pyqtSlot()
|
@pyqtSlot()
|
||||||
def setFinished(self):
|
def setFinished(self) -> None:
|
||||||
self._finished = True
|
self._finished = True
|
||||||
self._reset()
|
self._reset()
|
||||||
self.onFinished.emit()
|
self.onFinished.emit()
|
||||||
|
|
||||||
@pyqtProperty(bool, notify = onFinished)
|
@pyqtProperty(bool, notify = onFinished)
|
||||||
def finished(self):
|
def finished(self) -> bool:
|
||||||
return self._finished
|
return self._finished
|
||||||
|
|
||||||
## Protected helper to create a view object based on provided QML.
|
## Protected helper to create a view object based on provided QML.
|
||||||
def _createViewFromQML(self):
|
def _createViewFromQML(self) -> Optional["QObject"]:
|
||||||
path = os.path.join(PluginRegistry.getInstance().getPluginPath(self.getPluginId()), self._qml_url)
|
plugin_path = PluginRegistry.getInstance().getPluginPath(self.getPluginId())
|
||||||
self._view = Application.getInstance().createQmlComponent(path, {"manager": self})
|
if plugin_path is None:
|
||||||
|
Logger.log("e", "Cannot create QML view: cannot find plugin path for plugin [%s]", self.getPluginId())
|
||||||
|
return None
|
||||||
|
path = os.path.join(plugin_path, self._qml_url)
|
||||||
|
|
||||||
@pyqtProperty(QObject, constant = True)
|
from cura.CuraApplication import CuraApplication
|
||||||
def displayItem(self):
|
view = CuraApplication.getInstance().createQmlComponent(path, {"manager": self})
|
||||||
if not self._view:
|
return view
|
||||||
self._createViewFromQML()
|
|
||||||
return self._view
|
@pyqtProperty(QUrl, constant = True)
|
||||||
|
def qmlPath(self) -> "QUrl":
|
||||||
|
plugin_path = PluginRegistry.getInstance().getPluginPath(self.getPluginId())
|
||||||
|
if plugin_path is None:
|
||||||
|
Logger.log("e", "Cannot create QML view: cannot find plugin path for plugin [%s]", self.getPluginId())
|
||||||
|
return QUrl("")
|
||||||
|
path = os.path.join(plugin_path, self._qml_url)
|
||||||
|
return QUrl.fromLocalFile(path)
|
||||||
|
|
||||||
|
@pyqtSlot(result = QObject)
|
||||||
|
def getDisplayItem(self) -> Optional["QObject"]:
|
||||||
|
return self._createViewFromQML()
|
||||||
|
@ -64,21 +64,21 @@ class MachineErrorChecker(QObject):
|
|||||||
|
|
||||||
def _onMachineChanged(self) -> None:
|
def _onMachineChanged(self) -> None:
|
||||||
if self._global_stack:
|
if self._global_stack:
|
||||||
self._global_stack.propertyChanged.disconnect(self.startErrorCheck)
|
self._global_stack.propertyChanged.disconnect(self.startErrorCheckPropertyChanged)
|
||||||
self._global_stack.containersChanged.disconnect(self.startErrorCheck)
|
self._global_stack.containersChanged.disconnect(self.startErrorCheck)
|
||||||
|
|
||||||
for extruder in self._global_stack.extruders.values():
|
for extruder in self._global_stack.extruders.values():
|
||||||
extruder.propertyChanged.disconnect(self.startErrorCheck)
|
extruder.propertyChanged.disconnect(self.startErrorCheckPropertyChanged)
|
||||||
extruder.containersChanged.disconnect(self.startErrorCheck)
|
extruder.containersChanged.disconnect(self.startErrorCheck)
|
||||||
|
|
||||||
self._global_stack = self._machine_manager.activeMachine
|
self._global_stack = self._machine_manager.activeMachine
|
||||||
|
|
||||||
if self._global_stack:
|
if self._global_stack:
|
||||||
self._global_stack.propertyChanged.connect(self.startErrorCheck)
|
self._global_stack.propertyChanged.connect(self.startErrorCheckPropertyChanged)
|
||||||
self._global_stack.containersChanged.connect(self.startErrorCheck)
|
self._global_stack.containersChanged.connect(self.startErrorCheck)
|
||||||
|
|
||||||
for extruder in self._global_stack.extruders.values():
|
for extruder in self._global_stack.extruders.values():
|
||||||
extruder.propertyChanged.connect(self.startErrorCheck)
|
extruder.propertyChanged.connect(self.startErrorCheckPropertyChanged)
|
||||||
extruder.containersChanged.connect(self.startErrorCheck)
|
extruder.containersChanged.connect(self.startErrorCheck)
|
||||||
|
|
||||||
hasErrorUpdated = pyqtSignal()
|
hasErrorUpdated = pyqtSignal()
|
||||||
@ -93,6 +93,13 @@ class MachineErrorChecker(QObject):
|
|||||||
def needToWaitForResult(self) -> bool:
|
def needToWaitForResult(self) -> bool:
|
||||||
return self._need_to_check or self._check_in_progress
|
return self._need_to_check or self._check_in_progress
|
||||||
|
|
||||||
|
# Start the error check for property changed
|
||||||
|
# this is seperate from the startErrorCheck because it ignores a number property types
|
||||||
|
def startErrorCheckPropertyChanged(self, key, property_name):
|
||||||
|
if property_name != "value":
|
||||||
|
return
|
||||||
|
self.startErrorCheck()
|
||||||
|
|
||||||
# Starts the error check timer to schedule a new error check.
|
# Starts the error check timer to schedule a new error check.
|
||||||
def startErrorCheck(self, *args) -> None:
|
def startErrorCheck(self, *args) -> None:
|
||||||
if not self._check_in_progress:
|
if not self._check_in_progress:
|
||||||
@ -120,7 +127,7 @@ class MachineErrorChecker(QObject):
|
|||||||
|
|
||||||
# Populate the (stack, key) tuples to check
|
# Populate the (stack, key) tuples to check
|
||||||
self._stacks_and_keys_to_check = deque()
|
self._stacks_and_keys_to_check = deque()
|
||||||
for stack in [global_stack] + list(global_stack.extruders.values()):
|
for stack in global_stack.extruders.values():
|
||||||
for key in stack.getAllKeys():
|
for key in stack.getAllKeys():
|
||||||
self._stacks_and_keys_to_check.append((stack, key))
|
self._stacks_and_keys_to_check.append((stack, key))
|
||||||
|
|
||||||
@ -161,7 +168,7 @@ class MachineErrorChecker(QObject):
|
|||||||
if validator_type:
|
if validator_type:
|
||||||
validator = validator_type(key)
|
validator = validator_type(key)
|
||||||
validation_state = validator(stack)
|
validation_state = validator(stack)
|
||||||
if validation_state in (ValidatorState.Exception, ValidatorState.MaximumError, ValidatorState.MinimumError):
|
if validation_state in (ValidatorState.Exception, ValidatorState.MaximumError, ValidatorState.MinimumError, ValidatorState.Invalid):
|
||||||
# Finish
|
# Finish
|
||||||
self._setResult(True)
|
self._setResult(True)
|
||||||
return
|
return
|
||||||
|
@ -21,6 +21,7 @@ from .VariantType import VariantType
|
|||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from UM.Settings.DefinitionContainer import DefinitionContainer
|
from UM.Settings.DefinitionContainer import DefinitionContainer
|
||||||
|
from UM.Settings.InstanceContainer import InstanceContainer
|
||||||
from cura.Settings.GlobalStack import GlobalStack
|
from cura.Settings.GlobalStack import GlobalStack
|
||||||
from cura.Settings.ExtruderStack import ExtruderStack
|
from cura.Settings.ExtruderStack import ExtruderStack
|
||||||
|
|
||||||
@ -92,7 +93,7 @@ class MaterialManager(QObject):
|
|||||||
self._container_registry.findContainersMetadata(type = "material") if
|
self._container_registry.findContainersMetadata(type = "material") if
|
||||||
metadata.get("GUID")} # type: Dict[str, Dict[str, Any]]
|
metadata.get("GUID")} # type: Dict[str, Dict[str, Any]]
|
||||||
|
|
||||||
self._material_group_map = dict() # type: Dict[str, MaterialGroup]
|
self._material_group_map = dict()
|
||||||
|
|
||||||
# Map #1
|
# Map #1
|
||||||
# root_material_id -> MaterialGroup
|
# root_material_id -> MaterialGroup
|
||||||
@ -102,6 +103,8 @@ class MaterialManager(QObject):
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
root_material_id = material_metadata.get("base_file", "")
|
root_material_id = material_metadata.get("base_file", "")
|
||||||
|
if root_material_id not in material_metadatas: #Not a registered material profile. Don't store this in the look-up tables.
|
||||||
|
continue
|
||||||
if root_material_id not in self._material_group_map:
|
if root_material_id not in self._material_group_map:
|
||||||
self._material_group_map[root_material_id] = MaterialGroup(root_material_id, MaterialNode(material_metadatas[root_material_id]))
|
self._material_group_map[root_material_id] = MaterialGroup(root_material_id, MaterialNode(material_metadatas[root_material_id]))
|
||||||
self._material_group_map[root_material_id].is_read_only = self._container_registry.isReadOnly(root_material_id)
|
self._material_group_map[root_material_id].is_read_only = self._container_registry.isReadOnly(root_material_id)
|
||||||
@ -117,7 +120,7 @@ class MaterialManager(QObject):
|
|||||||
|
|
||||||
# Map #1.5
|
# Map #1.5
|
||||||
# GUID -> material group list
|
# GUID -> material group list
|
||||||
self._guid_material_groups_map = defaultdict(list) # type: Dict[str, List[MaterialGroup]]
|
self._guid_material_groups_map = defaultdict(list)
|
||||||
for root_material_id, material_group in self._material_group_map.items():
|
for root_material_id, material_group in self._material_group_map.items():
|
||||||
guid = material_group.root_material_node.getMetaDataEntry("GUID", "")
|
guid = material_group.root_material_node.getMetaDataEntry("GUID", "")
|
||||||
self._guid_material_groups_map[guid].append(material_group)
|
self._guid_material_groups_map[guid].append(material_group)
|
||||||
@ -199,7 +202,7 @@ class MaterialManager(QObject):
|
|||||||
|
|
||||||
# Map #4
|
# Map #4
|
||||||
# "machine" -> "nozzle name" -> "buildplate name" -> "root material ID" -> specific material InstanceContainer
|
# "machine" -> "nozzle name" -> "buildplate name" -> "root material ID" -> specific material InstanceContainer
|
||||||
self._diameter_machine_nozzle_buildplate_material_map = dict() # type: Dict[str, Dict[str, MaterialNode]]
|
self._diameter_machine_nozzle_buildplate_material_map = dict()
|
||||||
for material_metadata in material_metadatas.values():
|
for material_metadata in material_metadatas.values():
|
||||||
self.__addMaterialMetadataIntoLookupTree(material_metadata)
|
self.__addMaterialMetadataIntoLookupTree(material_metadata)
|
||||||
|
|
||||||
@ -218,7 +221,7 @@ class MaterialManager(QObject):
|
|||||||
|
|
||||||
root_material_id = material_metadata["base_file"]
|
root_material_id = material_metadata["base_file"]
|
||||||
definition = material_metadata["definition"]
|
definition = material_metadata["definition"]
|
||||||
approximate_diameter = material_metadata["approximate_diameter"]
|
approximate_diameter = str(material_metadata["approximate_diameter"])
|
||||||
|
|
||||||
if approximate_diameter not in self._diameter_machine_nozzle_buildplate_material_map:
|
if approximate_diameter not in self._diameter_machine_nozzle_buildplate_material_map:
|
||||||
self._diameter_machine_nozzle_buildplate_material_map[approximate_diameter] = {}
|
self._diameter_machine_nozzle_buildplate_material_map[approximate_diameter] = {}
|
||||||
@ -298,9 +301,13 @@ class MaterialManager(QObject):
|
|||||||
def getRootMaterialIDWithoutDiameter(self, root_material_id: str) -> str:
|
def getRootMaterialIDWithoutDiameter(self, root_material_id: str) -> str:
|
||||||
return self._diameter_material_map.get(root_material_id, "")
|
return self._diameter_material_map.get(root_material_id, "")
|
||||||
|
|
||||||
def getMaterialGroupListByGUID(self, guid: str) -> Optional[list]:
|
def getMaterialGroupListByGUID(self, guid: str) -> Optional[List[MaterialGroup]]:
|
||||||
return self._guid_material_groups_map.get(guid)
|
return self._guid_material_groups_map.get(guid)
|
||||||
|
|
||||||
|
# Returns a dict of all material groups organized by root_material_id.
|
||||||
|
def getAllMaterialGroups(self) -> Dict[str, "MaterialGroup"]:
|
||||||
|
return self._material_group_map
|
||||||
|
|
||||||
#
|
#
|
||||||
# Return a dict with all root material IDs (k) and ContainerNodes (v) that's suitable for the given setup.
|
# Return a dict with all root material IDs (k) and ContainerNodes (v) that's suitable for the given setup.
|
||||||
#
|
#
|
||||||
@ -327,7 +334,6 @@ class MaterialManager(QObject):
|
|||||||
buildplate_node = nozzle_node.getChildNode(buildplate_name)
|
buildplate_node = nozzle_node.getChildNode(buildplate_name)
|
||||||
|
|
||||||
nodes_to_check = [buildplate_node, nozzle_node, machine_node, default_machine_node]
|
nodes_to_check = [buildplate_node, nozzle_node, machine_node, default_machine_node]
|
||||||
|
|
||||||
# Fallback mechanism of finding materials:
|
# Fallback mechanism of finding materials:
|
||||||
# 1. buildplate-specific material
|
# 1. buildplate-specific material
|
||||||
# 2. nozzle-specific material
|
# 2. nozzle-specific material
|
||||||
@ -365,7 +371,7 @@ class MaterialManager(QObject):
|
|||||||
nozzle_name = None
|
nozzle_name = None
|
||||||
if extruder_stack.variant.getId() != "empty_variant":
|
if extruder_stack.variant.getId() != "empty_variant":
|
||||||
nozzle_name = extruder_stack.variant.getName()
|
nozzle_name = extruder_stack.variant.getName()
|
||||||
diameter = extruder_stack.approximateMaterialDiameter
|
diameter = extruder_stack.getApproximateMaterialDiameter()
|
||||||
|
|
||||||
# Fetch the available materials (ContainerNode) for the current active machine and extruder setup.
|
# Fetch the available materials (ContainerNode) for the current active machine and extruder setup.
|
||||||
return self.getAvailableMaterials(machine.definition, nozzle_name, buildplate_name, diameter)
|
return self.getAvailableMaterials(machine.definition, nozzle_name, buildplate_name, diameter)
|
||||||
@ -446,6 +452,28 @@ class MaterialManager(QObject):
|
|||||||
material_diameter, root_material_id)
|
material_diameter, root_material_id)
|
||||||
return node
|
return node
|
||||||
|
|
||||||
|
# There are 2 ways to get fallback materials;
|
||||||
|
# - A fallback by type (@sa getFallbackMaterialIdByMaterialType), which adds the generic version of this material
|
||||||
|
# - A fallback by GUID; If a material has been duplicated, it should also check if the original materials do have
|
||||||
|
# a GUID. This should only be done if the material itself does not have a quality just yet.
|
||||||
|
def getFallBackMaterialIdsByMaterial(self, material: "InstanceContainer") -> List[str]:
|
||||||
|
results = [] # type: List[str]
|
||||||
|
|
||||||
|
material_groups = self.getMaterialGroupListByGUID(material.getMetaDataEntry("GUID"))
|
||||||
|
for material_group in material_groups: # type: ignore
|
||||||
|
if material_group.name != material.getId():
|
||||||
|
# If the material in the group is read only, put it at the front of the list (since that is the most
|
||||||
|
# likely one to get a result)
|
||||||
|
if material_group.is_read_only:
|
||||||
|
results.insert(0, material_group.name)
|
||||||
|
else:
|
||||||
|
results.append(material_group.name)
|
||||||
|
|
||||||
|
fallback = self.getFallbackMaterialIdByMaterialType(material.getMetaDataEntry("material"))
|
||||||
|
if fallback is not None:
|
||||||
|
results.append(fallback)
|
||||||
|
return results
|
||||||
|
|
||||||
#
|
#
|
||||||
# Used by QualityManager. Built-in quality profiles may be based on generic material IDs such as "generic_pla".
|
# Used by QualityManager. Built-in quality profiles may be based on generic material IDs such as "generic_pla".
|
||||||
# For materials such as ultimaker_pla_orange, no quality profiles may be found, so we should fall back to use
|
# For materials such as ultimaker_pla_orange, no quality profiles may be found, so we should fall back to use
|
||||||
@ -478,12 +506,22 @@ class MaterialManager(QObject):
|
|||||||
|
|
||||||
buildplate_name = global_stack.getBuildplateName()
|
buildplate_name = global_stack.getBuildplateName()
|
||||||
machine_definition = global_stack.definition
|
machine_definition = global_stack.definition
|
||||||
if extruder_definition is None:
|
|
||||||
extruder_definition = global_stack.extruders[position].definition
|
|
||||||
|
|
||||||
if extruder_definition and parseBool(global_stack.getMetaDataEntry("has_materials", False)):
|
# The extruder-compatible material diameter in the extruder definition may not be the correct value because
|
||||||
# At this point the extruder_definition is not None
|
# the user can change it in the definition_changes container.
|
||||||
material_diameter = extruder_definition.getProperty("material_diameter", "value")
|
if extruder_definition is None:
|
||||||
|
extruder_stack_or_definition = global_stack.extruders[position]
|
||||||
|
is_extruder_stack = True
|
||||||
|
else:
|
||||||
|
extruder_stack_or_definition = extruder_definition
|
||||||
|
is_extruder_stack = False
|
||||||
|
|
||||||
|
if extruder_stack_or_definition and parseBool(global_stack.getMetaDataEntry("has_materials", False)):
|
||||||
|
if is_extruder_stack:
|
||||||
|
material_diameter = extruder_stack_or_definition.getCompatibleMaterialDiameter()
|
||||||
|
else:
|
||||||
|
material_diameter = extruder_stack_or_definition.getProperty("material_diameter", "value")
|
||||||
|
|
||||||
if isinstance(material_diameter, SettingFunction):
|
if isinstance(material_diameter, SettingFunction):
|
||||||
material_diameter = material_diameter(global_stack)
|
material_diameter = material_diameter(global_stack)
|
||||||
approximate_material_diameter = str(round(material_diameter))
|
approximate_material_diameter = str(round(material_diameter))
|
||||||
@ -500,16 +538,40 @@ class MaterialManager(QObject):
|
|||||||
return
|
return
|
||||||
|
|
||||||
nodes_to_remove = [material_group.root_material_node] + material_group.derived_material_node_list
|
nodes_to_remove = [material_group.root_material_node] + material_group.derived_material_node_list
|
||||||
|
# Sort all nodes with respect to the container ID lengths in the ascending order so the base material container
|
||||||
|
# will be the first one to be removed. We need to do this to ensure that all containers get loaded & deleted.
|
||||||
|
nodes_to_remove = sorted(nodes_to_remove, key = lambda x: len(x.getMetaDataEntry("id", "")))
|
||||||
|
# Try to load all containers first. If there is any faulty ones, they will be put into the faulty container
|
||||||
|
# list, so removeContainer() can ignore those ones.
|
||||||
|
for node in nodes_to_remove:
|
||||||
|
container_id = node.getMetaDataEntry("id", "")
|
||||||
|
results = self._container_registry.findContainers(id = container_id)
|
||||||
|
if not results:
|
||||||
|
self._container_registry.addWrongContainerId(container_id)
|
||||||
for node in nodes_to_remove:
|
for node in nodes_to_remove:
|
||||||
self._container_registry.removeContainer(node.getMetaDataEntry("id", ""))
|
self._container_registry.removeContainer(node.getMetaDataEntry("id", ""))
|
||||||
|
|
||||||
#
|
#
|
||||||
# Methods for GUI
|
# Methods for GUI
|
||||||
#
|
#
|
||||||
|
@pyqtSlot("QVariant", result=bool)
|
||||||
|
def canMaterialBeRemoved(self, material_node: "MaterialNode"):
|
||||||
|
# Check if the material is active in any extruder train. In that case, the material shouldn't be removed!
|
||||||
|
# In the future we might enable this again, but right now, it's causing a ton of issues if we do (since it
|
||||||
|
# corrupts the configuration)
|
||||||
|
root_material_id = material_node.getMetaDataEntry("base_file")
|
||||||
|
material_group = self.getMaterialGroup(root_material_id)
|
||||||
|
if not material_group:
|
||||||
|
return False
|
||||||
|
|
||||||
|
nodes_to_remove = [material_group.root_material_node] + material_group.derived_material_node_list
|
||||||
|
ids_to_remove = [node.getMetaDataEntry("id", "") for node in nodes_to_remove]
|
||||||
|
|
||||||
|
for extruder_stack in self._container_registry.findContainerStacks(type="extruder_train"):
|
||||||
|
if extruder_stack.material.getId() in ids_to_remove:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
#
|
|
||||||
# Sets the new name for the given material.
|
|
||||||
#
|
|
||||||
@pyqtSlot("QVariant", str)
|
@pyqtSlot("QVariant", str)
|
||||||
def setMaterialName(self, material_node: "MaterialNode", name: str) -> None:
|
def setMaterialName(self, material_node: "MaterialNode", name: str) -> None:
|
||||||
root_material_id = material_node.getMetaDataEntry("base_file")
|
root_material_id = material_node.getMetaDataEntry("base_file")
|
||||||
@ -592,7 +654,6 @@ class MaterialManager(QObject):
|
|||||||
container_to_add.setDirty(True)
|
container_to_add.setDirty(True)
|
||||||
self._container_registry.addContainer(container_to_add)
|
self._container_registry.addContainer(container_to_add)
|
||||||
|
|
||||||
|
|
||||||
# if the duplicated material was favorite then the new material should also be added to favorite.
|
# if the duplicated material was favorite then the new material should also be added to favorite.
|
||||||
if root_material_id in self.getFavorites():
|
if root_material_id in self.getFavorites():
|
||||||
self.addFavorite(new_base_id)
|
self.addFavorite(new_base_id)
|
||||||
@ -612,8 +673,10 @@ class MaterialManager(QObject):
|
|||||||
machine_manager = self._application.getMachineManager()
|
machine_manager = self._application.getMachineManager()
|
||||||
extruder_stack = machine_manager.activeStack
|
extruder_stack = machine_manager.activeStack
|
||||||
|
|
||||||
|
machine_definition = self._application.getGlobalContainerStack().definition
|
||||||
|
root_material_id = machine_definition.getMetaDataEntry("preferred_material", default = "generic_pla")
|
||||||
|
|
||||||
approximate_diameter = str(extruder_stack.approximateMaterialDiameter)
|
approximate_diameter = str(extruder_stack.approximateMaterialDiameter)
|
||||||
root_material_id = "generic_pla"
|
|
||||||
root_material_id = self.getRootMaterialIDForDiameter(root_material_id, approximate_diameter)
|
root_material_id = self.getRootMaterialIDForDiameter(root_material_id, approximate_diameter)
|
||||||
material_group = self.getMaterialGroup(root_material_id)
|
material_group = self.getMaterialGroup(root_material_id)
|
||||||
|
|
||||||
@ -644,7 +707,11 @@ class MaterialManager(QObject):
|
|||||||
|
|
||||||
@pyqtSlot(str)
|
@pyqtSlot(str)
|
||||||
def removeFavorite(self, root_material_id: str) -> None:
|
def removeFavorite(self, root_material_id: str) -> None:
|
||||||
|
try:
|
||||||
self._favorites.remove(root_material_id)
|
self._favorites.remove(root_material_id)
|
||||||
|
except KeyError:
|
||||||
|
Logger.log("w", "Could not delete material %s from favorites as it was already deleted", root_material_id)
|
||||||
|
return
|
||||||
self.materialsUpdated.emit()
|
self.materialsUpdated.emit()
|
||||||
|
|
||||||
# Ensure all settings are saved.
|
# Ensure all settings are saved.
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
# Copyright (c) 2018 Ultimaker B.V.
|
# Copyright (c) 2018 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, Dict, Set
|
||||||
|
|
||||||
from PyQt5.QtCore import Qt, pyqtSignal, pyqtProperty
|
from PyQt5.QtCore import Qt, pyqtSignal, pyqtProperty
|
||||||
from UM.Qt.ListModel import ListModel
|
from UM.Qt.ListModel import ListModel
|
||||||
@ -9,13 +10,16 @@ from UM.Qt.ListModel import ListModel
|
|||||||
# Those 2 models are used by the material drop down menu to show generic materials and branded materials separately.
|
# Those 2 models are used by the material drop down menu to show generic materials and branded materials separately.
|
||||||
# The extruder position defined here is being used to bound a menu to the correct extruder. This is used in the top
|
# The extruder position defined here is being used to bound a menu to the correct extruder. This is used in the top
|
||||||
# bar menu "Settings" -> "Extruder nr" -> "Material" -> this menu
|
# bar menu "Settings" -> "Extruder nr" -> "Material" -> this menu
|
||||||
|
from cura.Machines.MaterialNode import MaterialNode
|
||||||
|
|
||||||
|
|
||||||
class BaseMaterialsModel(ListModel):
|
class BaseMaterialsModel(ListModel):
|
||||||
|
|
||||||
extruderPositionChanged = pyqtSignal()
|
extruderPositionChanged = pyqtSignal()
|
||||||
|
enabledChanged = pyqtSignal()
|
||||||
|
|
||||||
def __init__(self, parent = None):
|
def __init__(self, parent = None):
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
|
|
||||||
from cura.CuraApplication import CuraApplication
|
from cura.CuraApplication import CuraApplication
|
||||||
|
|
||||||
self._application = CuraApplication.getInstance()
|
self._application = CuraApplication.getInstance()
|
||||||
@ -54,8 +58,9 @@ class BaseMaterialsModel(ListModel):
|
|||||||
self._extruder_position = 0
|
self._extruder_position = 0
|
||||||
self._extruder_stack = None
|
self._extruder_stack = None
|
||||||
|
|
||||||
self._available_materials = None
|
self._available_materials = None # type: Optional[Dict[str, MaterialNode]]
|
||||||
self._favorite_ids = None
|
self._favorite_ids = set() # type: Set[str]
|
||||||
|
self._enabled = True
|
||||||
|
|
||||||
def _updateExtruderStack(self):
|
def _updateExtruderStack(self):
|
||||||
global_stack = self._machine_manager.activeMachine
|
global_stack = self._machine_manager.activeMachine
|
||||||
@ -64,9 +69,11 @@ class BaseMaterialsModel(ListModel):
|
|||||||
|
|
||||||
if self._extruder_stack is not None:
|
if self._extruder_stack is not None:
|
||||||
self._extruder_stack.pyqtContainersChanged.disconnect(self._update)
|
self._extruder_stack.pyqtContainersChanged.disconnect(self._update)
|
||||||
|
self._extruder_stack.approximateMaterialDiameterChanged.disconnect(self._update)
|
||||||
self._extruder_stack = global_stack.extruders.get(str(self._extruder_position))
|
self._extruder_stack = global_stack.extruders.get(str(self._extruder_position))
|
||||||
if self._extruder_stack is not None:
|
if self._extruder_stack is not None:
|
||||||
self._extruder_stack.pyqtContainersChanged.connect(self._update)
|
self._extruder_stack.pyqtContainersChanged.connect(self._update)
|
||||||
|
self._extruder_stack.approximateMaterialDiameterChanged.connect(self._update)
|
||||||
# Force update the model when the extruder stack changes
|
# Force update the model when the extruder stack changes
|
||||||
self._update()
|
self._update()
|
||||||
|
|
||||||
@ -80,6 +87,18 @@ class BaseMaterialsModel(ListModel):
|
|||||||
def extruderPosition(self) -> int:
|
def extruderPosition(self) -> int:
|
||||||
return self._extruder_position
|
return self._extruder_position
|
||||||
|
|
||||||
|
def setEnabled(self, enabled):
|
||||||
|
if self._enabled != enabled:
|
||||||
|
self._enabled = enabled
|
||||||
|
if self._enabled:
|
||||||
|
# ensure the data is there again.
|
||||||
|
self._update()
|
||||||
|
self.enabledChanged.emit()
|
||||||
|
|
||||||
|
@pyqtProperty(bool, fset=setEnabled, notify=enabledChanged)
|
||||||
|
def enabled(self):
|
||||||
|
return self._enabled
|
||||||
|
|
||||||
## This is an abstract method that needs to be implemented by the specific
|
## This is an abstract method that needs to be implemented by the specific
|
||||||
# models themselves.
|
# models themselves.
|
||||||
def _update(self):
|
def _update(self):
|
||||||
@ -91,7 +110,7 @@ class BaseMaterialsModel(ListModel):
|
|||||||
def _canUpdate(self):
|
def _canUpdate(self):
|
||||||
global_stack = self._machine_manager.activeMachine
|
global_stack = self._machine_manager.activeMachine
|
||||||
|
|
||||||
if global_stack is None:
|
if global_stack is None or not self._enabled:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
extruder_position = str(self._extruder_position)
|
extruder_position = str(self._extruder_position)
|
||||||
@ -100,7 +119,6 @@ class BaseMaterialsModel(ListModel):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
extruder_stack = global_stack.extruders[extruder_position]
|
extruder_stack = global_stack.extruders[extruder_position]
|
||||||
|
|
||||||
self._available_materials = self._material_manager.getAvailableMaterialsForMachineExtruder(global_stack, extruder_stack)
|
self._available_materials = self._material_manager.getAvailableMaterialsForMachineExtruder(global_stack, extruder_stack)
|
||||||
if self._available_materials is None:
|
if self._available_materials is None:
|
||||||
return False
|
return False
|
||||||
|
264
cura/Machines/Models/DiscoveredPrintersModel.py
Normal file
264
cura/Machines/Models/DiscoveredPrintersModel.py
Normal file
@ -0,0 +1,264 @@
|
|||||||
|
# Copyright (c) 2019 Ultimaker B.V.
|
||||||
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
|
from typing import Callable, Dict, List, Optional, TYPE_CHECKING
|
||||||
|
|
||||||
|
from PyQt5.QtCore import pyqtSlot, pyqtProperty, pyqtSignal, QObject, QTimer
|
||||||
|
|
||||||
|
from UM.i18n import i18nCatalog
|
||||||
|
from UM.Logger import Logger
|
||||||
|
from UM.Util import parseBool
|
||||||
|
from UM.OutputDevice.OutputDeviceManager import ManualDeviceAdditionAttempt
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from PyQt5.QtCore import QObject
|
||||||
|
from UM.OutputDevice.OutputDevicePlugin import OutputDevicePlugin
|
||||||
|
from cura.CuraApplication import CuraApplication
|
||||||
|
from cura.PrinterOutput.NetworkedPrinterOutputDevice import NetworkedPrinterOutputDevice
|
||||||
|
|
||||||
|
|
||||||
|
catalog = i18nCatalog("cura")
|
||||||
|
|
||||||
|
|
||||||
|
class DiscoveredPrinter(QObject):
|
||||||
|
|
||||||
|
def __init__(self, ip_address: str, key: str, name: str, create_callback: Callable[[str], None], machine_type: str,
|
||||||
|
device: "NetworkedPrinterOutputDevice", parent: Optional["QObject"] = None) -> None:
|
||||||
|
super().__init__(parent)
|
||||||
|
|
||||||
|
self._ip_address = ip_address
|
||||||
|
self._key = key
|
||||||
|
self._name = name
|
||||||
|
self.create_callback = create_callback
|
||||||
|
self._machine_type = machine_type
|
||||||
|
self._device = device
|
||||||
|
|
||||||
|
nameChanged = pyqtSignal()
|
||||||
|
|
||||||
|
def getKey(self) -> str:
|
||||||
|
return self._key
|
||||||
|
|
||||||
|
@pyqtProperty(str, notify = nameChanged)
|
||||||
|
def name(self) -> str:
|
||||||
|
return self._name
|
||||||
|
|
||||||
|
def setName(self, name: str) -> None:
|
||||||
|
if self._name != name:
|
||||||
|
self._name = name
|
||||||
|
self.nameChanged.emit()
|
||||||
|
|
||||||
|
@pyqtProperty(str, constant = True)
|
||||||
|
def address(self) -> str:
|
||||||
|
return self._ip_address
|
||||||
|
|
||||||
|
machineTypeChanged = pyqtSignal()
|
||||||
|
|
||||||
|
@pyqtProperty(str, notify = machineTypeChanged)
|
||||||
|
def machineType(self) -> str:
|
||||||
|
return self._machine_type
|
||||||
|
|
||||||
|
def setMachineType(self, machine_type: str) -> None:
|
||||||
|
if self._machine_type != machine_type:
|
||||||
|
self._machine_type = machine_type
|
||||||
|
self.machineTypeChanged.emit()
|
||||||
|
|
||||||
|
# Checks if the given machine type name in the available machine list.
|
||||||
|
# The machine type is a code name such as "ultimaker_3", while the machine type name is the human-readable name of
|
||||||
|
# the machine type, which is "Ultimaker 3" for "ultimaker_3".
|
||||||
|
def _hasHumanReadableMachineTypeName(self, machine_type_name: str) -> bool:
|
||||||
|
from cura.CuraApplication import CuraApplication
|
||||||
|
results = CuraApplication.getInstance().getContainerRegistry().findDefinitionContainersMetadata(name = machine_type_name)
|
||||||
|
return len(results) > 0
|
||||||
|
|
||||||
|
# Human readable machine type string
|
||||||
|
@pyqtProperty(str, notify = machineTypeChanged)
|
||||||
|
def readableMachineType(self) -> str:
|
||||||
|
from cura.CuraApplication import CuraApplication
|
||||||
|
machine_manager = CuraApplication.getInstance().getMachineManager()
|
||||||
|
# In ClusterUM3OutputDevice, when it updates a printer information, it updates the machine type using the field
|
||||||
|
# "machine_variant", and for some reason, it's not the machine type ID/codename/... but a human-readable string
|
||||||
|
# like "Ultimaker 3". The code below handles this case.
|
||||||
|
if self._hasHumanReadableMachineTypeName(self._machine_type):
|
||||||
|
readable_type = self._machine_type
|
||||||
|
else:
|
||||||
|
readable_type = self._getMachineTypeNameFromId(self._machine_type)
|
||||||
|
if not readable_type:
|
||||||
|
readable_type = catalog.i18nc("@label", "Unknown")
|
||||||
|
return readable_type
|
||||||
|
|
||||||
|
@pyqtProperty(bool, notify = machineTypeChanged)
|
||||||
|
def isUnknownMachineType(self) -> bool:
|
||||||
|
if self._hasHumanReadableMachineTypeName(self._machine_type):
|
||||||
|
readable_type = self._machine_type
|
||||||
|
else:
|
||||||
|
readable_type = self._getMachineTypeNameFromId(self._machine_type)
|
||||||
|
return not readable_type
|
||||||
|
|
||||||
|
def _getMachineTypeNameFromId(self, machine_type_id: str) -> str:
|
||||||
|
machine_type_name = ""
|
||||||
|
from cura.CuraApplication import CuraApplication
|
||||||
|
results = CuraApplication.getInstance().getContainerRegistry().findDefinitionContainersMetadata(id = machine_type_id)
|
||||||
|
if results:
|
||||||
|
machine_type_name = results[0]["name"]
|
||||||
|
return machine_type_name
|
||||||
|
|
||||||
|
@pyqtProperty(QObject, constant = True)
|
||||||
|
def device(self) -> "NetworkedPrinterOutputDevice":
|
||||||
|
return self._device
|
||||||
|
|
||||||
|
@pyqtProperty(bool, constant = True)
|
||||||
|
def isHostOfGroup(self) -> bool:
|
||||||
|
return getattr(self._device, "clusterSize", 1) > 0
|
||||||
|
|
||||||
|
@pyqtProperty(str, constant = True)
|
||||||
|
def sectionName(self) -> str:
|
||||||
|
if self.isUnknownMachineType or not self.isHostOfGroup:
|
||||||
|
return catalog.i18nc("@label", "The printer(s) below cannot be connected because they are part of a group")
|
||||||
|
else:
|
||||||
|
return catalog.i18nc("@label", "Available networked printers")
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Discovered printers are all the printers that were found on the network, which provide a more convenient way
|
||||||
|
# to add networked printers (Plugin finds a bunch of printers, user can select one from the list, plugin can then
|
||||||
|
# add that printer to Cura as the active one).
|
||||||
|
#
|
||||||
|
class DiscoveredPrintersModel(QObject):
|
||||||
|
|
||||||
|
def __init__(self, application: "CuraApplication", parent: Optional["QObject"] = None) -> None:
|
||||||
|
super().__init__(parent)
|
||||||
|
|
||||||
|
self._application = application
|
||||||
|
self._discovered_printer_by_ip_dict = dict() # type: Dict[str, DiscoveredPrinter]
|
||||||
|
|
||||||
|
self._plugin_for_manual_device = None # type: Optional[OutputDevicePlugin]
|
||||||
|
self._manual_device_address = ""
|
||||||
|
|
||||||
|
self._manual_device_request_timeout_in_seconds = 5 # timeout for adding a manual device in seconds
|
||||||
|
self._manual_device_request_timer = QTimer()
|
||||||
|
self._manual_device_request_timer.setInterval(self._manual_device_request_timeout_in_seconds * 1000)
|
||||||
|
self._manual_device_request_timer.setSingleShot(True)
|
||||||
|
self._manual_device_request_timer.timeout.connect(self._onManualRequestTimeout)
|
||||||
|
|
||||||
|
discoveredPrintersChanged = pyqtSignal()
|
||||||
|
|
||||||
|
@pyqtSlot(str)
|
||||||
|
def checkManualDevice(self, address: str) -> None:
|
||||||
|
if self.hasManualDeviceRequestInProgress:
|
||||||
|
Logger.log("i", "A manual device request for address [%s] is still in progress, do nothing",
|
||||||
|
self._manual_device_address)
|
||||||
|
return
|
||||||
|
|
||||||
|
priority_order = [
|
||||||
|
ManualDeviceAdditionAttempt.PRIORITY,
|
||||||
|
ManualDeviceAdditionAttempt.POSSIBLE,
|
||||||
|
] # type: List[ManualDeviceAdditionAttempt]
|
||||||
|
|
||||||
|
all_plugins_dict = self._application.getOutputDeviceManager().getAllOutputDevicePlugins()
|
||||||
|
|
||||||
|
can_add_manual_plugins = [item for item in filter(
|
||||||
|
lambda plugin_item: plugin_item.canAddManualDevice(address) in priority_order,
|
||||||
|
all_plugins_dict.values())]
|
||||||
|
|
||||||
|
if not can_add_manual_plugins:
|
||||||
|
Logger.log("d", "Could not find a plugin to accept adding %s manually via address.", address)
|
||||||
|
return
|
||||||
|
|
||||||
|
plugin = max(can_add_manual_plugins, key = lambda p: priority_order.index(p.canAddManualDevice(address)))
|
||||||
|
self._plugin_for_manual_device = plugin
|
||||||
|
self._plugin_for_manual_device.addManualDevice(address, callback = self._onManualDeviceRequestFinished)
|
||||||
|
self._manual_device_address = address
|
||||||
|
self._manual_device_request_timer.start()
|
||||||
|
self.hasManualDeviceRequestInProgressChanged.emit()
|
||||||
|
|
||||||
|
@pyqtSlot()
|
||||||
|
def cancelCurrentManualDeviceRequest(self) -> None:
|
||||||
|
self._manual_device_request_timer.stop()
|
||||||
|
|
||||||
|
if self._manual_device_address:
|
||||||
|
if self._plugin_for_manual_device is not None:
|
||||||
|
self._plugin_for_manual_device.removeManualDevice(self._manual_device_address, address = self._manual_device_address)
|
||||||
|
self._manual_device_address = ""
|
||||||
|
self._plugin_for_manual_device = None
|
||||||
|
self.hasManualDeviceRequestInProgressChanged.emit()
|
||||||
|
self.manualDeviceRequestFinished.emit(False)
|
||||||
|
|
||||||
|
def _onManualRequestTimeout(self) -> None:
|
||||||
|
Logger.log("w", "Manual printer [%s] request timed out. Cancel the current request.", self._manual_device_address)
|
||||||
|
self.cancelCurrentManualDeviceRequest()
|
||||||
|
|
||||||
|
hasManualDeviceRequestInProgressChanged = pyqtSignal()
|
||||||
|
|
||||||
|
@pyqtProperty(bool, notify = hasManualDeviceRequestInProgressChanged)
|
||||||
|
def hasManualDeviceRequestInProgress(self) -> bool:
|
||||||
|
return self._manual_device_address != ""
|
||||||
|
|
||||||
|
manualDeviceRequestFinished = pyqtSignal(bool, arguments = ["success"])
|
||||||
|
|
||||||
|
def _onManualDeviceRequestFinished(self, success: bool, address: str) -> None:
|
||||||
|
self._manual_device_request_timer.stop()
|
||||||
|
if address == self._manual_device_address:
|
||||||
|
self._manual_device_address = ""
|
||||||
|
self.hasManualDeviceRequestInProgressChanged.emit()
|
||||||
|
self.manualDeviceRequestFinished.emit(success)
|
||||||
|
|
||||||
|
@pyqtProperty("QVariantMap", notify = discoveredPrintersChanged)
|
||||||
|
def discoveredPrintersByAddress(self) -> Dict[str, DiscoveredPrinter]:
|
||||||
|
return self._discovered_printer_by_ip_dict
|
||||||
|
|
||||||
|
@pyqtProperty("QVariantList", notify = discoveredPrintersChanged)
|
||||||
|
def discoveredPrinters(self) -> List["DiscoveredPrinter"]:
|
||||||
|
item_list = list(
|
||||||
|
x for x in self._discovered_printer_by_ip_dict.values() if not parseBool(x.device.getProperty("temporary")))
|
||||||
|
|
||||||
|
# Split the printers into 2 lists and sort them ascending based on names.
|
||||||
|
available_list = []
|
||||||
|
not_available_list = []
|
||||||
|
for item in item_list:
|
||||||
|
if item.isUnknownMachineType or getattr(item.device, "clusterSize", 1) < 1:
|
||||||
|
not_available_list.append(item)
|
||||||
|
else:
|
||||||
|
available_list.append(item)
|
||||||
|
|
||||||
|
available_list.sort(key = lambda x: x.device.name)
|
||||||
|
not_available_list.sort(key = lambda x: x.device.name)
|
||||||
|
|
||||||
|
return available_list + not_available_list
|
||||||
|
|
||||||
|
def addDiscoveredPrinter(self, ip_address: str, key: str, name: str, create_callback: Callable[[str], None],
|
||||||
|
machine_type: str, device: "NetworkedPrinterOutputDevice") -> None:
|
||||||
|
if ip_address in self._discovered_printer_by_ip_dict:
|
||||||
|
Logger.log("e", "Printer with ip [%s] has already been added", ip_address)
|
||||||
|
return
|
||||||
|
|
||||||
|
discovered_printer = DiscoveredPrinter(ip_address, key, name, create_callback, machine_type, device, parent = self)
|
||||||
|
self._discovered_printer_by_ip_dict[ip_address] = discovered_printer
|
||||||
|
self.discoveredPrintersChanged.emit()
|
||||||
|
|
||||||
|
def updateDiscoveredPrinter(self, ip_address: str,
|
||||||
|
name: Optional[str] = None,
|
||||||
|
machine_type: Optional[str] = None) -> None:
|
||||||
|
if ip_address not in self._discovered_printer_by_ip_dict:
|
||||||
|
Logger.log("w", "Printer with ip [%s] is not known", ip_address)
|
||||||
|
return
|
||||||
|
|
||||||
|
item = self._discovered_printer_by_ip_dict[ip_address]
|
||||||
|
|
||||||
|
if name is not None:
|
||||||
|
item.setName(name)
|
||||||
|
if machine_type is not None:
|
||||||
|
item.setMachineType(machine_type)
|
||||||
|
|
||||||
|
def removeDiscoveredPrinter(self, ip_address: str) -> None:
|
||||||
|
if ip_address not in self._discovered_printer_by_ip_dict:
|
||||||
|
Logger.log("w", "Key [%s] does not exist in the discovered printers list.", ip_address)
|
||||||
|
return
|
||||||
|
|
||||||
|
del self._discovered_printer_by_ip_dict[ip_address]
|
||||||
|
self.discoveredPrintersChanged.emit()
|
||||||
|
|
||||||
|
# A convenience function for QML to create a machine (GlobalStack) out of the given discovered printer.
|
||||||
|
# This function invokes the given discovered printer's "create_callback" to do this.
|
||||||
|
@pyqtSlot("QVariant")
|
||||||
|
def createMachineFromDiscoveredPrinter(self, discovered_printer: "DiscoveredPrinter") -> None:
|
||||||
|
discovered_printer.create_callback(discovered_printer.getKey())
|
@ -1,31 +1,31 @@
|
|||||||
# Copyright (c) 2017 Ultimaker B.V.
|
# Copyright (c) 2018 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 PyQt5.QtCore import Qt, pyqtSignal, pyqtSlot, pyqtProperty, QTimer
|
from PyQt5.QtCore import Qt, pyqtSignal, pyqtProperty, QTimer
|
||||||
from typing import Iterable
|
from typing import Iterable, TYPE_CHECKING
|
||||||
|
|
||||||
from UM.i18n import i18nCatalog
|
from UM.i18n import i18nCatalog
|
||||||
import UM.Qt.ListModel
|
from UM.Qt.ListModel import ListModel
|
||||||
from UM.Application import Application
|
from UM.Application import Application
|
||||||
import UM.FlameProfiler
|
import UM.FlameProfiler
|
||||||
|
|
||||||
from cura.Settings.ExtruderStack import ExtruderStack # To listen to changes on the extruders.
|
if TYPE_CHECKING:
|
||||||
|
from cura.Settings.ExtruderStack import ExtruderStack # To listen to changes on the extruders.
|
||||||
|
|
||||||
catalog = i18nCatalog("cura")
|
catalog = i18nCatalog("cura")
|
||||||
|
|
||||||
|
|
||||||
## Model that holds extruders.
|
## Model that holds extruders.
|
||||||
#
|
#
|
||||||
# This model is designed for use by any list of extruders, but specifically
|
# This model is designed for use by any list of extruders, but specifically
|
||||||
# intended for drop-down lists of the current machine's extruders in place of
|
# intended for drop-down lists of the current machine's extruders in place of
|
||||||
# settings.
|
# settings.
|
||||||
class ExtrudersModel(UM.Qt.ListModel.ListModel):
|
class ExtrudersModel(ListModel):
|
||||||
# The ID of the container stack for the extruder.
|
# The ID of the container stack for the extruder.
|
||||||
IdRole = Qt.UserRole + 1
|
IdRole = Qt.UserRole + 1
|
||||||
|
|
||||||
## Human-readable name of the extruder.
|
## Human-readable name of the extruder.
|
||||||
NameRole = Qt.UserRole + 2
|
NameRole = Qt.UserRole + 2
|
||||||
## Is the extruder enabled?
|
|
||||||
EnabledRole = Qt.UserRole + 9
|
|
||||||
|
|
||||||
## Colour of the material loaded in the extruder.
|
## Colour of the material loaded in the extruder.
|
||||||
ColorRole = Qt.UserRole + 3
|
ColorRole = Qt.UserRole + 3
|
||||||
@ -47,6 +47,12 @@ class ExtrudersModel(UM.Qt.ListModel.ListModel):
|
|||||||
VariantRole = Qt.UserRole + 7
|
VariantRole = Qt.UserRole + 7
|
||||||
StackRole = Qt.UserRole + 8
|
StackRole = Qt.UserRole + 8
|
||||||
|
|
||||||
|
MaterialBrandRole = Qt.UserRole + 9
|
||||||
|
ColorNameRole = Qt.UserRole + 10
|
||||||
|
|
||||||
|
## Is the extruder enabled?
|
||||||
|
EnabledRole = Qt.UserRole + 11
|
||||||
|
|
||||||
## List of colours to display if there is no material or the material has no known
|
## List of colours to display if there is no material or the material has no known
|
||||||
# colour.
|
# colour.
|
||||||
defaultColors = ["#ffc924", "#86ec21", "#22eeee", "#245bff", "#9124ff", "#ff24c8"]
|
defaultColors = ["#ffc924", "#86ec21", "#22eeee", "#245bff", "#9124ff", "#ff24c8"]
|
||||||
@ -67,14 +73,13 @@ class ExtrudersModel(UM.Qt.ListModel.ListModel):
|
|||||||
self.addRoleName(self.MaterialRole, "material")
|
self.addRoleName(self.MaterialRole, "material")
|
||||||
self.addRoleName(self.VariantRole, "variant")
|
self.addRoleName(self.VariantRole, "variant")
|
||||||
self.addRoleName(self.StackRole, "stack")
|
self.addRoleName(self.StackRole, "stack")
|
||||||
|
self.addRoleName(self.MaterialBrandRole, "material_brand")
|
||||||
|
self.addRoleName(self.ColorNameRole, "color_name")
|
||||||
self._update_extruder_timer = QTimer()
|
self._update_extruder_timer = QTimer()
|
||||||
self._update_extruder_timer.setInterval(100)
|
self._update_extruder_timer.setInterval(100)
|
||||||
self._update_extruder_timer.setSingleShot(True)
|
self._update_extruder_timer.setSingleShot(True)
|
||||||
self._update_extruder_timer.timeout.connect(self.__updateExtruders)
|
self._update_extruder_timer.timeout.connect(self.__updateExtruders)
|
||||||
|
|
||||||
self._simple_names = False
|
|
||||||
|
|
||||||
self._active_machine_extruders = [] # type: Iterable[ExtruderStack]
|
self._active_machine_extruders = [] # type: Iterable[ExtruderStack]
|
||||||
self._add_optional_extruder = False
|
self._add_optional_extruder = False
|
||||||
|
|
||||||
@ -96,21 +101,6 @@ class ExtrudersModel(UM.Qt.ListModel.ListModel):
|
|||||||
def addOptionalExtruder(self):
|
def addOptionalExtruder(self):
|
||||||
return self._add_optional_extruder
|
return self._add_optional_extruder
|
||||||
|
|
||||||
## Set the simpleNames property.
|
|
||||||
def setSimpleNames(self, simple_names):
|
|
||||||
if simple_names != self._simple_names:
|
|
||||||
self._simple_names = simple_names
|
|
||||||
self.simpleNamesChanged.emit()
|
|
||||||
self._updateExtruders()
|
|
||||||
|
|
||||||
## Emitted when the simpleNames property changes.
|
|
||||||
simpleNamesChanged = pyqtSignal()
|
|
||||||
|
|
||||||
## Whether or not the model should show all definitions regardless of visibility.
|
|
||||||
@pyqtProperty(bool, fset = setSimpleNames, notify = simpleNamesChanged)
|
|
||||||
def simpleNames(self):
|
|
||||||
return self._simple_names
|
|
||||||
|
|
||||||
## Links to the stack-changed signal of the new extruders when an extruder
|
## Links to the stack-changed signal of the new extruders when an extruder
|
||||||
# is swapped out or added in the current machine.
|
# is swapped out or added in the current machine.
|
||||||
#
|
#
|
||||||
@ -119,17 +109,19 @@ class ExtrudersModel(UM.Qt.ListModel.ListModel):
|
|||||||
# that signal. Application.globalContainerStackChanged doesn't fill this
|
# that signal. Application.globalContainerStackChanged doesn't fill this
|
||||||
# signal; it's assumed to be the current printer in that case.
|
# signal; it's assumed to be the current printer in that case.
|
||||||
def _extrudersChanged(self, machine_id = None):
|
def _extrudersChanged(self, machine_id = None):
|
||||||
|
machine_manager = Application.getInstance().getMachineManager()
|
||||||
if machine_id is not None:
|
if machine_id is not None:
|
||||||
if Application.getInstance().getGlobalContainerStack() is None:
|
if machine_manager.activeMachine is None:
|
||||||
# No machine, don't need to update the current machine's extruders
|
# No machine, don't need to update the current machine's extruders
|
||||||
return
|
return
|
||||||
if machine_id != Application.getInstance().getGlobalContainerStack().getId():
|
if machine_id != machine_manager.activeMachine.getId():
|
||||||
# Not the current machine
|
# Not the current machine
|
||||||
return
|
return
|
||||||
|
|
||||||
# Unlink from old extruders
|
# Unlink from old extruders
|
||||||
for extruder in self._active_machine_extruders:
|
for extruder in self._active_machine_extruders:
|
||||||
extruder.containersChanged.disconnect(self._onExtruderStackContainersChanged)
|
extruder.containersChanged.disconnect(self._onExtruderStackContainersChanged)
|
||||||
|
extruder.enabledChanged.disconnect(self._updateExtruders)
|
||||||
|
|
||||||
# Link to new extruders
|
# Link to new extruders
|
||||||
self._active_machine_extruders = []
|
self._active_machine_extruders = []
|
||||||
@ -138,13 +130,14 @@ class ExtrudersModel(UM.Qt.ListModel.ListModel):
|
|||||||
if extruder is None: #This extruder wasn't loaded yet. This happens asynchronously while this model is constructed from QML.
|
if extruder is None: #This extruder wasn't loaded yet. This happens asynchronously while this model is constructed from QML.
|
||||||
continue
|
continue
|
||||||
extruder.containersChanged.connect(self._onExtruderStackContainersChanged)
|
extruder.containersChanged.connect(self._onExtruderStackContainersChanged)
|
||||||
|
extruder.enabledChanged.connect(self._updateExtruders)
|
||||||
self._active_machine_extruders.append(extruder)
|
self._active_machine_extruders.append(extruder)
|
||||||
|
|
||||||
self._updateExtruders() # Since the new extruders may have different properties, update our own model.
|
self._updateExtruders() # Since the new extruders may have different properties, update our own model.
|
||||||
|
|
||||||
def _onExtruderStackContainersChanged(self, container):
|
def _onExtruderStackContainersChanged(self, container):
|
||||||
# Update when there is an empty container or material change
|
# Update when there is an empty container or material or variant change
|
||||||
if container.getMetaDataEntry("type") == "material" or container.getMetaDataEntry("type") is None:
|
if container.getMetaDataEntry("type") in ["material", "variant", None]:
|
||||||
# The ExtrudersModel needs to be updated when the material-name or -color changes, because the user identifies extruders by material-name
|
# The ExtrudersModel needs to be updated when the material-name or -color changes, because the user identifies extruders by material-name
|
||||||
self._updateExtruders()
|
self._updateExtruders()
|
||||||
|
|
||||||
@ -160,7 +153,7 @@ class ExtrudersModel(UM.Qt.ListModel.ListModel):
|
|||||||
def __updateExtruders(self):
|
def __updateExtruders(self):
|
||||||
extruders_changed = False
|
extruders_changed = False
|
||||||
|
|
||||||
if self.rowCount() != 0:
|
if self.count != 0:
|
||||||
extruders_changed = True
|
extruders_changed = True
|
||||||
|
|
||||||
items = []
|
items = []
|
||||||
@ -172,7 +165,7 @@ class ExtrudersModel(UM.Qt.ListModel.ListModel):
|
|||||||
machine_extruder_count = global_container_stack.getProperty("machine_extruder_count", "value")
|
machine_extruder_count = global_container_stack.getProperty("machine_extruder_count", "value")
|
||||||
|
|
||||||
for extruder in Application.getInstance().getExtruderManager().getActiveExtruderStacks():
|
for extruder in Application.getInstance().getExtruderManager().getActiveExtruderStacks():
|
||||||
position = extruder.getMetaDataEntry("position", default = "0") # Get the position
|
position = extruder.getMetaDataEntry("position", default = "0")
|
||||||
try:
|
try:
|
||||||
position = int(position)
|
position = int(position)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
@ -183,7 +176,8 @@ class ExtrudersModel(UM.Qt.ListModel.ListModel):
|
|||||||
|
|
||||||
default_color = self.defaultColors[position] if 0 <= position < len(self.defaultColors) else self.defaultColors[0]
|
default_color = self.defaultColors[position] if 0 <= position < len(self.defaultColors) else self.defaultColors[0]
|
||||||
color = extruder.material.getMetaDataEntry("color_code", default = default_color) if extruder.material else default_color
|
color = extruder.material.getMetaDataEntry("color_code", default = default_color) if extruder.material else default_color
|
||||||
|
material_brand = extruder.material.getMetaDataEntry("brand", default = "generic")
|
||||||
|
color_name = extruder.material.getMetaDataEntry("color_name")
|
||||||
# construct an item with only the relevant information
|
# construct an item with only the relevant information
|
||||||
item = {
|
item = {
|
||||||
"id": extruder.getId(),
|
"id": extruder.getId(),
|
||||||
@ -195,6 +189,8 @@ class ExtrudersModel(UM.Qt.ListModel.ListModel):
|
|||||||
"material": extruder.material.getName() if extruder.material else "",
|
"material": extruder.material.getName() if extruder.material else "",
|
||||||
"variant": extruder.variant.getName() if extruder.variant else "", # e.g. print core
|
"variant": extruder.variant.getName() if extruder.variant else "", # e.g. print core
|
||||||
"stack": extruder,
|
"stack": extruder,
|
||||||
|
"material_brand": material_brand,
|
||||||
|
"color_name": color_name
|
||||||
}
|
}
|
||||||
|
|
||||||
items.append(item)
|
items.append(item)
|
||||||
@ -213,9 +209,14 @@ class ExtrudersModel(UM.Qt.ListModel.ListModel):
|
|||||||
"enabled": True,
|
"enabled": True,
|
||||||
"color": "#ffffff",
|
"color": "#ffffff",
|
||||||
"index": -1,
|
"index": -1,
|
||||||
"definition": ""
|
"definition": "",
|
||||||
|
"material": "",
|
||||||
|
"variant": "",
|
||||||
|
"stack": None,
|
||||||
|
"material_brand": "",
|
||||||
|
"color_name": "",
|
||||||
}
|
}
|
||||||
items.append(item)
|
items.append(item)
|
||||||
|
if self._items != items:
|
||||||
self.setItems(items)
|
self.setItems(items)
|
||||||
self.modelChanged.emit()
|
self.modelChanged.emit()
|
@ -1,20 +1,16 @@
|
|||||||
# Copyright (c) 2018 Ultimaker B.V.
|
# Copyright (c) 2018 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 UM.Logger import Logger
|
|
||||||
from cura.Machines.Models.BaseMaterialsModel import BaseMaterialsModel
|
from cura.Machines.Models.BaseMaterialsModel import BaseMaterialsModel
|
||||||
|
|
||||||
|
## Model that shows the list of favorite materials.
|
||||||
class FavoriteMaterialsModel(BaseMaterialsModel):
|
class FavoriteMaterialsModel(BaseMaterialsModel):
|
||||||
|
|
||||||
def __init__(self, parent = None):
|
def __init__(self, parent = None):
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
self._update()
|
self._update()
|
||||||
|
|
||||||
def _update(self):
|
def _update(self):
|
||||||
|
|
||||||
# Perform standard check and reset if the check fails
|
|
||||||
if not self._canUpdate():
|
if not self._canUpdate():
|
||||||
self.setItems([])
|
|
||||||
return
|
return
|
||||||
|
|
||||||
# Get updated list of favorites
|
# Get updated list of favorites
|
||||||
|
112
cura/Machines/Models/FirstStartMachineActionsModel.py
Normal file
112
cura/Machines/Models/FirstStartMachineActionsModel.py
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
# Copyright (c) 2019 Ultimaker B.V.
|
||||||
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
|
from typing import Optional, Dict, Any, TYPE_CHECKING
|
||||||
|
|
||||||
|
from PyQt5.QtCore import QObject, Qt, pyqtProperty, pyqtSignal, pyqtSlot
|
||||||
|
|
||||||
|
from UM.Qt.ListModel import ListModel
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from cura.CuraApplication import CuraApplication
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# This model holds all first-start machine actions for the currently active machine. It has 2 roles:
|
||||||
|
# - title : the title/name of the action
|
||||||
|
# - content : the QObject of the QML content of the action
|
||||||
|
# - action : the MachineAction object itself
|
||||||
|
#
|
||||||
|
class FirstStartMachineActionsModel(ListModel):
|
||||||
|
|
||||||
|
TitleRole = Qt.UserRole + 1
|
||||||
|
ContentRole = Qt.UserRole + 2
|
||||||
|
ActionRole = Qt.UserRole + 3
|
||||||
|
|
||||||
|
def __init__(self, application: "CuraApplication", parent: Optional[QObject] = None) -> None:
|
||||||
|
super().__init__(parent)
|
||||||
|
|
||||||
|
self.addRoleName(self.TitleRole, "title")
|
||||||
|
self.addRoleName(self.ContentRole, "content")
|
||||||
|
self.addRoleName(self.ActionRole, "action")
|
||||||
|
|
||||||
|
self._current_action_index = 0
|
||||||
|
|
||||||
|
self._application = application
|
||||||
|
self._application.initializationFinished.connect(self._initialize)
|
||||||
|
|
||||||
|
self._previous_global_stack = None
|
||||||
|
|
||||||
|
def _initialize(self) -> None:
|
||||||
|
self._application.getMachineManager().globalContainerChanged.connect(self._update)
|
||||||
|
self._update()
|
||||||
|
|
||||||
|
currentActionIndexChanged = pyqtSignal()
|
||||||
|
allFinished = pyqtSignal() # Emitted when all actions have been finished.
|
||||||
|
|
||||||
|
@pyqtProperty(int, notify = currentActionIndexChanged)
|
||||||
|
def currentActionIndex(self) -> int:
|
||||||
|
return self._current_action_index
|
||||||
|
|
||||||
|
@pyqtProperty("QVariantMap", notify = currentActionIndexChanged)
|
||||||
|
def currentItem(self) -> Optional[Dict[str, Any]]:
|
||||||
|
if self._current_action_index >= self.count:
|
||||||
|
return dict()
|
||||||
|
else:
|
||||||
|
return self.getItem(self._current_action_index)
|
||||||
|
|
||||||
|
@pyqtProperty(bool, notify = currentActionIndexChanged)
|
||||||
|
def hasMoreActions(self) -> bool:
|
||||||
|
return self._current_action_index < self.count - 1
|
||||||
|
|
||||||
|
@pyqtSlot()
|
||||||
|
def goToNextAction(self) -> None:
|
||||||
|
# finish the current item
|
||||||
|
if "action" in self.currentItem:
|
||||||
|
self.currentItem["action"].setFinished()
|
||||||
|
|
||||||
|
if not self.hasMoreActions:
|
||||||
|
self.allFinished.emit()
|
||||||
|
self.reset()
|
||||||
|
return
|
||||||
|
|
||||||
|
self._current_action_index += 1
|
||||||
|
self.currentActionIndexChanged.emit()
|
||||||
|
|
||||||
|
# Resets the current action index to 0 so the wizard panel can show actions from the beginning.
|
||||||
|
@pyqtSlot()
|
||||||
|
def reset(self) -> None:
|
||||||
|
self._current_action_index = 0
|
||||||
|
self.currentActionIndexChanged.emit()
|
||||||
|
|
||||||
|
if self.count == 0:
|
||||||
|
self.allFinished.emit()
|
||||||
|
|
||||||
|
def _update(self) -> None:
|
||||||
|
global_stack = self._application.getMachineManager().activeMachine
|
||||||
|
if global_stack is None:
|
||||||
|
self.setItems([])
|
||||||
|
return
|
||||||
|
|
||||||
|
# Do not update if the machine has not been switched. This can cause the SettingProviders on the Machine
|
||||||
|
# Setting page to do a force update, but they can use potential outdated cached values.
|
||||||
|
if self._previous_global_stack is not None and global_stack.getId() == self._previous_global_stack.getId():
|
||||||
|
return
|
||||||
|
self._previous_global_stack = global_stack
|
||||||
|
|
||||||
|
definition_id = global_stack.definition.getId()
|
||||||
|
first_start_actions = self._application.getMachineActionManager().getFirstStartActions(definition_id)
|
||||||
|
|
||||||
|
item_list = []
|
||||||
|
for item in first_start_actions:
|
||||||
|
item_list.append({"title": item.label,
|
||||||
|
"content": item.getDisplayItem(),
|
||||||
|
"action": item,
|
||||||
|
})
|
||||||
|
item.reset()
|
||||||
|
|
||||||
|
self.setItems(item_list)
|
||||||
|
self.reset()
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = ["FirstStartMachineActionsModel"]
|
@ -1,7 +1,6 @@
|
|||||||
# Copyright (c) 2018 Ultimaker B.V.
|
# Copyright (c) 2018 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 UM.Logger import Logger
|
|
||||||
from cura.Machines.Models.BaseMaterialsModel import BaseMaterialsModel
|
from cura.Machines.Models.BaseMaterialsModel import BaseMaterialsModel
|
||||||
|
|
||||||
class GenericMaterialsModel(BaseMaterialsModel):
|
class GenericMaterialsModel(BaseMaterialsModel):
|
||||||
@ -11,10 +10,7 @@ class GenericMaterialsModel(BaseMaterialsModel):
|
|||||||
self._update()
|
self._update()
|
||||||
|
|
||||||
def _update(self):
|
def _update(self):
|
||||||
|
|
||||||
# Perform standard check and reset if the check fails
|
|
||||||
if not self._canUpdate():
|
if not self._canUpdate():
|
||||||
self.setItems([])
|
|
||||||
return
|
return
|
||||||
|
|
||||||
# Get updated list of favorites
|
# Get updated list of favorites
|
||||||
|
77
cura/Machines/Models/GlobalStacksModel.py
Normal file
77
cura/Machines/Models/GlobalStacksModel.py
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
# Copyright (c) 2018 Ultimaker B.V.
|
||||||
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
|
from PyQt5.QtCore import Qt, QTimer
|
||||||
|
|
||||||
|
from UM.Qt.ListModel import ListModel
|
||||||
|
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 GlobalStacksModel(ListModel):
|
||||||
|
NameRole = Qt.UserRole + 1
|
||||||
|
IdRole = Qt.UserRole + 2
|
||||||
|
HasRemoteConnectionRole = Qt.UserRole + 3
|
||||||
|
ConnectionTypeRole = Qt.UserRole + 4
|
||||||
|
MetaDataRole = Qt.UserRole + 5
|
||||||
|
DiscoverySourceRole = Qt.UserRole + 6 # For separating local and remote printers in the machine management page
|
||||||
|
|
||||||
|
def __init__(self, parent = None) -> None:
|
||||||
|
super().__init__(parent)
|
||||||
|
|
||||||
|
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.DiscoverySourceRole, "discoverySource")
|
||||||
|
|
||||||
|
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()
|
||||||
|
|
||||||
|
## Handler for container added/removed events from registry
|
||||||
|
def _onContainerChanged(self, container) -> None:
|
||||||
|
# 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:
|
||||||
|
items = []
|
||||||
|
|
||||||
|
container_stacks = CuraContainerRegistry.getInstance().findContainerStacks(type = "machine")
|
||||||
|
for container_stack in container_stacks:
|
||||||
|
has_remote_connection = False
|
||||||
|
|
||||||
|
for connection_type in container_stack.configuredConnectionTypes:
|
||||||
|
has_remote_connection |= connection_type in [ConnectionType.NetworkConnection.value,
|
||||||
|
ConnectionType.CloudConnection.value]
|
||||||
|
|
||||||
|
if parseBool(container_stack.getMetaDataEntry("hidden", False)):
|
||||||
|
continue
|
||||||
|
|
||||||
|
section_name = "Network enabled printers" if has_remote_connection else "Local printers"
|
||||||
|
section_name = self._catalog.i18nc("@info:title", section_name)
|
||||||
|
|
||||||
|
items.append({"name": container_stack.getMetaDataEntry("group_name", container_stack.getName()),
|
||||||
|
"id": container_stack.getId(),
|
||||||
|
"hasRemoteConnection": has_remote_connection,
|
||||||
|
"metadata": container_stack.getMetaData().copy(),
|
||||||
|
"discoverySource": section_name})
|
||||||
|
items.sort(key = lambda i: (not i["hasRemoteConnection"], i["name"]))
|
||||||
|
self.setItems(items)
|
@ -1,82 +0,0 @@
|
|||||||
# Copyright (c) 2018 Ultimaker B.V.
|
|
||||||
# Cura is released under the terms of the LGPLv3 or higher.
|
|
||||||
|
|
||||||
from UM.Qt.ListModel import ListModel
|
|
||||||
|
|
||||||
from PyQt5.QtCore import Qt
|
|
||||||
|
|
||||||
from UM.Settings.ContainerRegistry import ContainerRegistry
|
|
||||||
from UM.Settings.ContainerStack import ContainerStack
|
|
||||||
|
|
||||||
from UM.i18n import i18nCatalog
|
|
||||||
catalog = i18nCatalog("cura")
|
|
||||||
|
|
||||||
|
|
||||||
#
|
|
||||||
# This the QML model for the quality management page.
|
|
||||||
#
|
|
||||||
class MachineManagementModel(ListModel):
|
|
||||||
NameRole = Qt.UserRole + 1
|
|
||||||
IdRole = Qt.UserRole + 2
|
|
||||||
MetaDataRole = Qt.UserRole + 3
|
|
||||||
GroupRole = Qt.UserRole + 4
|
|
||||||
|
|
||||||
def __init__(self, parent = None):
|
|
||||||
super().__init__(parent)
|
|
||||||
self.addRoleName(self.NameRole, "name")
|
|
||||||
self.addRoleName(self.IdRole, "id")
|
|
||||||
self.addRoleName(self.MetaDataRole, "metadata")
|
|
||||||
self.addRoleName(self.GroupRole, "group")
|
|
||||||
self._local_container_stacks = []
|
|
||||||
self._network_container_stacks = []
|
|
||||||
|
|
||||||
# Listen to changes
|
|
||||||
ContainerRegistry.getInstance().containerAdded.connect(self._onContainerChanged)
|
|
||||||
ContainerRegistry.getInstance().containerMetaDataChanged.connect(self._onContainerChanged)
|
|
||||||
ContainerRegistry.getInstance().containerRemoved.connect(self._onContainerChanged)
|
|
||||||
self._filter_dict = {}
|
|
||||||
self._update()
|
|
||||||
|
|
||||||
## Handler for container added/removed events from registry
|
|
||||||
def _onContainerChanged(self, container):
|
|
||||||
# We only need to update when the added / removed container is a stack.
|
|
||||||
if isinstance(container, ContainerStack) and container.getMetaDataEntry("type") == "machine":
|
|
||||||
self._update()
|
|
||||||
|
|
||||||
## Private convenience function to reset & repopulate the model.
|
|
||||||
def _update(self):
|
|
||||||
items = []
|
|
||||||
|
|
||||||
# Get first the network enabled printers
|
|
||||||
network_filter_printers = {"type": "machine",
|
|
||||||
"um_network_key": "*",
|
|
||||||
"hidden": "False"}
|
|
||||||
self._network_container_stacks = ContainerRegistry.getInstance().findContainerStacks(**network_filter_printers)
|
|
||||||
self._network_container_stacks.sort(key = lambda i: i.getMetaDataEntry("connect_group_name"))
|
|
||||||
|
|
||||||
for container in self._network_container_stacks:
|
|
||||||
metadata = container.getMetaData().copy()
|
|
||||||
if container.getBottom():
|
|
||||||
metadata["definition_name"] = container.getBottom().getName()
|
|
||||||
|
|
||||||
items.append({"name": metadata["connect_group_name"],
|
|
||||||
"id": container.getId(),
|
|
||||||
"metadata": metadata,
|
|
||||||
"group": catalog.i18nc("@info:title", "Network enabled printers")})
|
|
||||||
|
|
||||||
# Get now the local printers
|
|
||||||
local_filter_printers = {"type": "machine", "um_network_key": None}
|
|
||||||
self._local_container_stacks = ContainerRegistry.getInstance().findContainerStacks(**local_filter_printers)
|
|
||||||
self._local_container_stacks.sort(key = lambda i: i.getName())
|
|
||||||
|
|
||||||
for container in self._local_container_stacks:
|
|
||||||
metadata = container.getMetaData().copy()
|
|
||||||
if container.getBottom():
|
|
||||||
metadata["definition_name"] = container.getBottom().getName()
|
|
||||||
|
|
||||||
items.append({"name": container.getName(),
|
|
||||||
"id": container.getId(),
|
|
||||||
"metadata": metadata,
|
|
||||||
"group": catalog.i18nc("@info:title", "Local printers")})
|
|
||||||
|
|
||||||
self.setItems(items)
|
|
@ -1,9 +1,8 @@
|
|||||||
# Copyright (c) 2018 Ultimaker B.V.
|
# Copyright (c) 2018 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 PyQt5.QtCore import Qt, pyqtSignal, pyqtProperty
|
from PyQt5.QtCore import Qt, pyqtSignal
|
||||||
from UM.Qt.ListModel import ListModel
|
from UM.Qt.ListModel import ListModel
|
||||||
from UM.Logger import Logger
|
|
||||||
from cura.Machines.Models.BaseMaterialsModel import BaseMaterialsModel
|
from cura.Machines.Models.BaseMaterialsModel import BaseMaterialsModel
|
||||||
|
|
||||||
class MaterialTypesModel(ListModel):
|
class MaterialTypesModel(ListModel):
|
||||||
@ -28,12 +27,8 @@ class MaterialBrandsModel(BaseMaterialsModel):
|
|||||||
self._update()
|
self._update()
|
||||||
|
|
||||||
def _update(self):
|
def _update(self):
|
||||||
|
|
||||||
# Perform standard check and reset if the check fails
|
|
||||||
if not self._canUpdate():
|
if not self._canUpdate():
|
||||||
self.setItems([])
|
|
||||||
return
|
return
|
||||||
|
|
||||||
# Get updated list of favorites
|
# Get updated list of favorites
|
||||||
self._favorite_ids = self._material_manager.getFavorites()
|
self._favorite_ids = self._material_manager.getFavorites()
|
||||||
|
|
||||||
|
@ -4,6 +4,7 @@
|
|||||||
from PyQt5.QtCore import QTimer, pyqtSignal, pyqtProperty
|
from PyQt5.QtCore import QTimer, pyqtSignal, pyqtProperty
|
||||||
|
|
||||||
from UM.Application import Application
|
from UM.Application import Application
|
||||||
|
from UM.Scene.Camera import Camera
|
||||||
from UM.Scene.Selection import Selection
|
from UM.Scene.Selection import Selection
|
||||||
from UM.Qt.ListModel import ListModel
|
from UM.Qt.ListModel import ListModel
|
||||||
|
|
||||||
@ -34,6 +35,7 @@ class MultiBuildPlateModel(ListModel):
|
|||||||
self._active_build_plate = -1
|
self._active_build_plate = -1
|
||||||
|
|
||||||
def setMaxBuildPlate(self, max_build_plate):
|
def setMaxBuildPlate(self, max_build_plate):
|
||||||
|
if self._max_build_plate != max_build_plate:
|
||||||
self._max_build_plate = max_build_plate
|
self._max_build_plate = max_build_plate
|
||||||
self.maxBuildPlateChanged.emit()
|
self.maxBuildPlateChanged.emit()
|
||||||
|
|
||||||
@ -43,6 +45,7 @@ class MultiBuildPlateModel(ListModel):
|
|||||||
return self._max_build_plate
|
return self._max_build_plate
|
||||||
|
|
||||||
def setActiveBuildPlate(self, nr):
|
def setActiveBuildPlate(self, nr):
|
||||||
|
if self._active_build_plate != nr:
|
||||||
self._active_build_plate = nr
|
self._active_build_plate = nr
|
||||||
self.activeBuildPlateChanged.emit()
|
self.activeBuildPlateChanged.emit()
|
||||||
|
|
||||||
@ -51,6 +54,7 @@ class MultiBuildPlateModel(ListModel):
|
|||||||
return self._active_build_plate
|
return self._active_build_plate
|
||||||
|
|
||||||
def _updateSelectedObjectBuildPlateNumbersDelayed(self, *args):
|
def _updateSelectedObjectBuildPlateNumbersDelayed(self, *args):
|
||||||
|
if not isinstance(args[0], Camera):
|
||||||
self._update_timer.start()
|
self._update_timer.start()
|
||||||
|
|
||||||
def _updateSelectedObjectBuildPlateNumbers(self, *args):
|
def _updateSelectedObjectBuildPlateNumbers(self, *args):
|
||||||
|
@ -33,8 +33,6 @@ class NozzleModel(ListModel):
|
|||||||
def _update(self):
|
def _update(self):
|
||||||
Logger.log("d", "Updating {model_class_name}.".format(model_class_name = self.__class__.__name__))
|
Logger.log("d", "Updating {model_class_name}.".format(model_class_name = self.__class__.__name__))
|
||||||
|
|
||||||
self.items.clear()
|
|
||||||
|
|
||||||
global_stack = self._machine_manager.activeMachine
|
global_stack = self._machine_manager.activeMachine
|
||||||
if global_stack is None:
|
if global_stack is None:
|
||||||
self.setItems([])
|
self.setItems([])
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
# Copyright (c) 2018 Ultimaker B.V.
|
# Copyright (c) 2018 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 PyQt5.QtCore import Qt
|
from PyQt5.QtCore import Qt, QTimer
|
||||||
|
|
||||||
from UM.Application import Application
|
from UM.Application import Application
|
||||||
from UM.Logger import Logger
|
from UM.Logger import Logger
|
||||||
@ -10,6 +10,7 @@ from UM.Settings.SettingFunction import SettingFunction
|
|||||||
|
|
||||||
from cura.Machines.QualityManager import QualityGroup
|
from cura.Machines.QualityManager import QualityGroup
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# QML Model for all built-in quality profiles. This model is used for the drop-down quality menu.
|
# QML Model for all built-in quality profiles. This model is used for the drop-down quality menu.
|
||||||
#
|
#
|
||||||
@ -21,6 +22,7 @@ class QualityProfilesDropDownMenuModel(ListModel):
|
|||||||
AvailableRole = Qt.UserRole + 5
|
AvailableRole = Qt.UserRole + 5
|
||||||
QualityGroupRole = Qt.UserRole + 6
|
QualityGroupRole = Qt.UserRole + 6
|
||||||
QualityChangesGroupRole = Qt.UserRole + 7
|
QualityChangesGroupRole = Qt.UserRole + 7
|
||||||
|
IsExperimentalRole = Qt.UserRole + 8
|
||||||
|
|
||||||
def __init__(self, parent = None):
|
def __init__(self, parent = None):
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
@ -32,19 +34,28 @@ class QualityProfilesDropDownMenuModel(ListModel):
|
|||||||
self.addRoleName(self.AvailableRole, "available") #Whether the quality profile is available in our current nozzle + material.
|
self.addRoleName(self.AvailableRole, "available") #Whether the quality profile is available in our current nozzle + material.
|
||||||
self.addRoleName(self.QualityGroupRole, "quality_group")
|
self.addRoleName(self.QualityGroupRole, "quality_group")
|
||||||
self.addRoleName(self.QualityChangesGroupRole, "quality_changes_group")
|
self.addRoleName(self.QualityChangesGroupRole, "quality_changes_group")
|
||||||
|
self.addRoleName(self.IsExperimentalRole, "is_experimental")
|
||||||
|
|
||||||
self._application = Application.getInstance()
|
self._application = Application.getInstance()
|
||||||
self._machine_manager = self._application.getMachineManager()
|
self._machine_manager = self._application.getMachineManager()
|
||||||
self._quality_manager = Application.getInstance().getQualityManager()
|
self._quality_manager = Application.getInstance().getQualityManager()
|
||||||
|
|
||||||
self._application.globalContainerStackChanged.connect(self._update)
|
self._application.globalContainerStackChanged.connect(self._onChange)
|
||||||
self._machine_manager.activeQualityGroupChanged.connect(self._update)
|
self._machine_manager.activeQualityGroupChanged.connect(self._onChange)
|
||||||
self._machine_manager.extruderChanged.connect(self._update)
|
self._machine_manager.extruderChanged.connect(self._onChange)
|
||||||
self._quality_manager.qualitiesUpdated.connect(self._update)
|
self._quality_manager.qualitiesUpdated.connect(self._onChange)
|
||||||
|
|
||||||
self._layer_height_unit = "" # This is cached
|
self._layer_height_unit = "" # This is cached
|
||||||
|
|
||||||
self._update()
|
self._update_timer = QTimer() # type: QTimer
|
||||||
|
self._update_timer.setInterval(100)
|
||||||
|
self._update_timer.setSingleShot(True)
|
||||||
|
self._update_timer.timeout.connect(self._update)
|
||||||
|
|
||||||
|
self._onChange()
|
||||||
|
|
||||||
|
def _onChange(self) -> None:
|
||||||
|
self._update_timer.start()
|
||||||
|
|
||||||
def _update(self):
|
def _update(self):
|
||||||
Logger.log("d", "Updating {model_class_name}.".format(model_class_name = self.__class__.__name__))
|
Logger.log("d", "Updating {model_class_name}.".format(model_class_name = self.__class__.__name__))
|
||||||
@ -74,7 +85,8 @@ class QualityProfilesDropDownMenuModel(ListModel):
|
|||||||
"layer_height": layer_height,
|
"layer_height": layer_height,
|
||||||
"layer_height_unit": self._layer_height_unit,
|
"layer_height_unit": self._layer_height_unit,
|
||||||
"available": quality_group.is_available,
|
"available": quality_group.is_available,
|
||||||
"quality_group": quality_group}
|
"quality_group": quality_group,
|
||||||
|
"is_experimental": quality_group.is_experimental}
|
||||||
|
|
||||||
item_list.append(item)
|
item_list.append(item)
|
||||||
|
|
||||||
|
@ -1,135 +1,115 @@
|
|||||||
# Copyright (c) 2018 Ultimaker B.V.
|
# Copyright (c) 2018 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, List, Dict, Union
|
from typing import Optional, List
|
||||||
import os
|
|
||||||
import urllib.parse
|
|
||||||
from configparser import ConfigParser
|
|
||||||
|
|
||||||
from PyQt5.QtCore import pyqtProperty, Qt, pyqtSignal, pyqtSlot
|
from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject
|
||||||
|
|
||||||
from UM.Application import Application
|
|
||||||
from UM.Logger import Logger
|
from UM.Logger import Logger
|
||||||
from UM.Qt.ListModel import ListModel
|
from UM.Preferences import Preferences
|
||||||
from UM.Resources import Resources
|
from UM.Resources import Resources
|
||||||
from UM.MimeTypeDatabase import MimeTypeDatabase, MimeTypeNotFoundError
|
|
||||||
|
|
||||||
from UM.i18n import i18nCatalog
|
from UM.i18n import i18nCatalog
|
||||||
|
from cura.Settings.SettingVisibilityPreset import SettingVisibilityPreset
|
||||||
|
|
||||||
catalog = i18nCatalog("cura")
|
catalog = i18nCatalog("cura")
|
||||||
|
|
||||||
|
|
||||||
class SettingVisibilityPresetsModel(ListModel):
|
class SettingVisibilityPresetsModel(QObject):
|
||||||
IdRole = Qt.UserRole + 1
|
onItemsChanged = pyqtSignal()
|
||||||
NameRole = Qt.UserRole + 2
|
activePresetChanged = pyqtSignal()
|
||||||
SettingsRole = Qt.UserRole + 3
|
|
||||||
|
|
||||||
def __init__(self, parent = None):
|
def __init__(self, preferences: Preferences, parent = None) -> None:
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
self.addRoleName(self.IdRole, "id")
|
|
||||||
self.addRoleName(self.NameRole, "name")
|
self._items = [] # type: List[SettingVisibilityPreset]
|
||||||
self.addRoleName(self.SettingsRole, "settings")
|
self._custom_preset = SettingVisibilityPreset(preset_id = "custom", name = "Custom selection", weight = -100)
|
||||||
|
|
||||||
self._populate()
|
self._populate()
|
||||||
basic_item = self.items[1]
|
|
||||||
basic_visibile_settings = ";".join(basic_item["settings"])
|
|
||||||
|
|
||||||
self._preferences = Application.getInstance().getPreferences()
|
basic_item = self.getVisibilityPresetById("basic")
|
||||||
|
if basic_item is not None:
|
||||||
|
basic_visibile_settings = ";".join(basic_item.settings)
|
||||||
|
else:
|
||||||
|
Logger.log("w", "Unable to find the basic visiblity preset.")
|
||||||
|
basic_visibile_settings = ""
|
||||||
|
|
||||||
|
self._preferences = preferences
|
||||||
|
|
||||||
# Preference to store which preset is currently selected
|
# Preference to store which preset is currently selected
|
||||||
self._preferences.addPreference("cura/active_setting_visibility_preset", "basic")
|
self._preferences.addPreference("cura/active_setting_visibility_preset", "basic")
|
||||||
|
|
||||||
# Preference that stores the "custom" set so it can always be restored (even after a restart)
|
# Preference that stores the "custom" set so it can always be restored (even after a restart)
|
||||||
self._preferences.addPreference("cura/custom_visible_settings", basic_visibile_settings)
|
self._preferences.addPreference("cura/custom_visible_settings", basic_visibile_settings)
|
||||||
self._preferences.preferenceChanged.connect(self._onPreferencesChanged)
|
self._preferences.preferenceChanged.connect(self._onPreferencesChanged)
|
||||||
|
|
||||||
self._active_preset_item = self._getItem(self._preferences.getValue("cura/active_setting_visibility_preset"))
|
self._active_preset_item = self.getVisibilityPresetById(self._preferences.getValue("cura/active_setting_visibility_preset"))
|
||||||
|
|
||||||
# Initialize visible settings if it is not done yet
|
# Initialize visible settings if it is not done yet
|
||||||
visible_settings = self._preferences.getValue("general/visible_settings")
|
visible_settings = self._preferences.getValue("general/visible_settings")
|
||||||
|
|
||||||
if not visible_settings:
|
if not visible_settings:
|
||||||
self._preferences.setValue("general/visible_settings", ";".join(self._active_preset_item["settings"]))
|
new_visible_settings = self._active_preset_item.settings if self._active_preset_item is not None else []
|
||||||
|
self._preferences.setValue("general/visible_settings", ";".join(new_visible_settings))
|
||||||
else:
|
else:
|
||||||
self._onPreferencesChanged("general/visible_settings")
|
self._onPreferencesChanged("general/visible_settings")
|
||||||
|
|
||||||
self.activePresetChanged.emit()
|
self.activePresetChanged.emit()
|
||||||
|
|
||||||
def _getItem(self, item_id: str) -> Optional[dict]:
|
def getVisibilityPresetById(self, item_id: str) -> Optional[SettingVisibilityPreset]:
|
||||||
result = None
|
result = None
|
||||||
for item in self.items:
|
for item in self._items:
|
||||||
if item["id"] == item_id:
|
if item.presetId == item_id:
|
||||||
result = item
|
result = item
|
||||||
break
|
break
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def _populate(self) -> None:
|
def _populate(self) -> None:
|
||||||
from cura.CuraApplication import CuraApplication
|
from cura.CuraApplication import CuraApplication
|
||||||
items = [] # type: List[Dict[str, Union[str, int, List[str]]]]
|
items = [] # type: List[SettingVisibilityPreset]
|
||||||
|
items.append(self._custom_preset)
|
||||||
for file_path in Resources.getAllResourcesOfType(CuraApplication.ResourceTypes.SettingVisibilityPreset):
|
for file_path in Resources.getAllResourcesOfType(CuraApplication.ResourceTypes.SettingVisibilityPreset):
|
||||||
|
setting_visibility_preset = SettingVisibilityPreset()
|
||||||
try:
|
try:
|
||||||
mime_type = MimeTypeDatabase.getMimeTypeForFile(file_path)
|
setting_visibility_preset.loadFromFile(file_path)
|
||||||
except MimeTypeNotFoundError:
|
|
||||||
Logger.log("e", "Could not determine mime type of file %s", file_path)
|
|
||||||
continue
|
|
||||||
|
|
||||||
item_id = urllib.parse.unquote_plus(mime_type.stripExtension(os.path.basename(file_path)))
|
|
||||||
if not os.path.isfile(file_path):
|
|
||||||
Logger.log("e", "[%s] is not a file", file_path)
|
|
||||||
continue
|
|
||||||
|
|
||||||
parser = ConfigParser(allow_no_value = True) # accept options without any value,
|
|
||||||
try:
|
|
||||||
parser.read([file_path])
|
|
||||||
if not parser.has_option("general", "name") or not parser.has_option("general", "weight"):
|
|
||||||
continue
|
|
||||||
|
|
||||||
settings = [] # type: List[str]
|
|
||||||
for section in parser.sections():
|
|
||||||
if section == 'general':
|
|
||||||
continue
|
|
||||||
|
|
||||||
settings.append(section)
|
|
||||||
for option in parser[section].keys():
|
|
||||||
settings.append(option)
|
|
||||||
|
|
||||||
items.append({
|
|
||||||
"id": item_id,
|
|
||||||
"name": catalog.i18nc("@action:inmenu", parser["general"]["name"]),
|
|
||||||
"weight": parser["general"]["weight"],
|
|
||||||
"settings": settings,
|
|
||||||
})
|
|
||||||
|
|
||||||
except Exception:
|
except Exception:
|
||||||
Logger.logException("e", "Failed to load setting preset %s", file_path)
|
Logger.logException("e", "Failed to load setting preset %s", file_path)
|
||||||
|
|
||||||
items.sort(key = lambda k: (int(k["weight"]), k["id"])) # type: ignore
|
items.append(setting_visibility_preset)
|
||||||
# Put "custom" at the top
|
|
||||||
items.insert(0, {"id": "custom",
|
# Sort them on weight (and if that fails, use ID)
|
||||||
"name": "Custom selection",
|
items.sort(key = lambda k: (int(k.weight), k.presetId))
|
||||||
"weight": -100,
|
|
||||||
"settings": []})
|
|
||||||
|
|
||||||
self.setItems(items)
|
self.setItems(items)
|
||||||
|
|
||||||
|
@pyqtProperty("QVariantList", notify = onItemsChanged)
|
||||||
|
def items(self) -> List[SettingVisibilityPreset]:
|
||||||
|
return self._items
|
||||||
|
|
||||||
|
def setItems(self, items: List[SettingVisibilityPreset]) -> None:
|
||||||
|
if self._items != items:
|
||||||
|
self._items = items
|
||||||
|
self.onItemsChanged.emit()
|
||||||
|
|
||||||
@pyqtSlot(str)
|
@pyqtSlot(str)
|
||||||
def setActivePreset(self, preset_id: str):
|
def setActivePreset(self, preset_id: str) -> None:
|
||||||
if preset_id == self._active_preset_item["id"]:
|
if self._active_preset_item is not None and preset_id == self._active_preset_item.presetId:
|
||||||
Logger.log("d", "Same setting visibility preset [%s] selected, do nothing.", preset_id)
|
Logger.log("d", "Same setting visibility preset [%s] selected, do nothing.", preset_id)
|
||||||
return
|
return
|
||||||
|
|
||||||
preset_item = None
|
preset_item = self.getVisibilityPresetById(preset_id)
|
||||||
for item in self.items:
|
|
||||||
if item["id"] == preset_id:
|
|
||||||
preset_item = item
|
|
||||||
break
|
|
||||||
if preset_item is None:
|
if preset_item is None:
|
||||||
Logger.log("w", "Tried to set active preset to unknown id [%s]", preset_id)
|
Logger.log("w", "Tried to set active preset to unknown id [%s]", preset_id)
|
||||||
return
|
return
|
||||||
|
|
||||||
need_to_save_to_custom = self._active_preset_item["id"] == "custom" and preset_id != "custom"
|
need_to_save_to_custom = self._active_preset_item is None or (self._active_preset_item.presetId == "custom" and preset_id != "custom")
|
||||||
if need_to_save_to_custom:
|
if need_to_save_to_custom:
|
||||||
# Save the current visibility settings to custom
|
# Save the current visibility settings to custom
|
||||||
current_visibility_string = self._preferences.getValue("general/visible_settings")
|
current_visibility_string = self._preferences.getValue("general/visible_settings")
|
||||||
if current_visibility_string:
|
if current_visibility_string:
|
||||||
self._preferences.setValue("cura/custom_visible_settings", current_visibility_string)
|
self._preferences.setValue("cura/custom_visible_settings", current_visibility_string)
|
||||||
|
|
||||||
new_visibility_string = ";".join(preset_item["settings"])
|
new_visibility_string = ";".join(preset_item.settings)
|
||||||
if preset_id == "custom":
|
if preset_id == "custom":
|
||||||
# Get settings from the stored custom data
|
# Get settings from the stored custom data
|
||||||
new_visibility_string = self._preferences.getValue("cura/custom_visible_settings")
|
new_visibility_string = self._preferences.getValue("cura/custom_visible_settings")
|
||||||
@ -141,11 +121,11 @@ class SettingVisibilityPresetsModel(ListModel):
|
|||||||
self._active_preset_item = preset_item
|
self._active_preset_item = preset_item
|
||||||
self.activePresetChanged.emit()
|
self.activePresetChanged.emit()
|
||||||
|
|
||||||
activePresetChanged = pyqtSignal()
|
|
||||||
|
|
||||||
@pyqtProperty(str, notify = activePresetChanged)
|
@pyqtProperty(str, notify = activePresetChanged)
|
||||||
def activePreset(self) -> str:
|
def activePreset(self) -> str:
|
||||||
return self._active_preset_item["id"]
|
if self._active_preset_item is not None:
|
||||||
|
return self._active_preset_item.presetId
|
||||||
|
return ""
|
||||||
|
|
||||||
def _onPreferencesChanged(self, name: str) -> None:
|
def _onPreferencesChanged(self, name: str) -> None:
|
||||||
if name != "general/visible_settings":
|
if name != "general/visible_settings":
|
||||||
@ -158,25 +138,31 @@ class SettingVisibilityPresetsModel(ListModel):
|
|||||||
|
|
||||||
visibility_set = set(visibility_string.split(";"))
|
visibility_set = set(visibility_string.split(";"))
|
||||||
matching_preset_item = None
|
matching_preset_item = None
|
||||||
for item in self.items:
|
for item in self._items:
|
||||||
if item["id"] == "custom":
|
if item.presetId == "custom":
|
||||||
continue
|
continue
|
||||||
if set(item["settings"]) == visibility_set:
|
if set(item.settings) == visibility_set:
|
||||||
matching_preset_item = item
|
matching_preset_item = item
|
||||||
break
|
break
|
||||||
|
|
||||||
item_to_set = self._active_preset_item
|
item_to_set = self._active_preset_item
|
||||||
if matching_preset_item is None:
|
if matching_preset_item is None:
|
||||||
# The new visibility setup is "custom" should be custom
|
# The new visibility setup is "custom" should be custom
|
||||||
if self._active_preset_item["id"] == "custom":
|
if self._active_preset_item is None or self._active_preset_item.presetId == "custom":
|
||||||
# We are already in custom, just save the settings
|
# We are already in custom, just save the settings
|
||||||
self._preferences.setValue("cura/custom_visible_settings", visibility_string)
|
self._preferences.setValue("cura/custom_visible_settings", visibility_string)
|
||||||
else:
|
else:
|
||||||
item_to_set = self.items[0] # 0 is custom
|
# We need to move to custom preset.
|
||||||
|
item_to_set = self.getVisibilityPresetById("custom")
|
||||||
else:
|
else:
|
||||||
item_to_set = matching_preset_item
|
item_to_set = matching_preset_item
|
||||||
|
|
||||||
if self._active_preset_item is None or self._active_preset_item["id"] != item_to_set["id"]:
|
# If we didn't find a matching preset, fallback to custom.
|
||||||
|
if item_to_set is None:
|
||||||
|
item_to_set = self._custom_preset
|
||||||
|
|
||||||
|
if self._active_preset_item is None or self._active_preset_item.presetId != item_to_set.presetId:
|
||||||
self._active_preset_item = item_to_set
|
self._active_preset_item = item_to_set
|
||||||
self._preferences.setValue("cura/active_setting_visibility_preset", self._active_preset_item["id"])
|
if self._active_preset_item is not None:
|
||||||
|
self._preferences.setValue("cura/active_setting_visibility_preset", self._active_preset_item.presetId)
|
||||||
self.activePresetChanged.emit()
|
self.activePresetChanged.emit()
|
||||||
|
@ -10,7 +10,6 @@ from UM.Application import Application
|
|||||||
from UM.Settings.ContainerRegistry import ContainerRegistry
|
from UM.Settings.ContainerRegistry import ContainerRegistry
|
||||||
from UM.i18n import i18nCatalog
|
from UM.i18n import i18nCatalog
|
||||||
from UM.Settings.SettingFunction import SettingFunction
|
from UM.Settings.SettingFunction import SettingFunction
|
||||||
|
|
||||||
from UM.Qt.ListModel import ListModel
|
from UM.Qt.ListModel import ListModel
|
||||||
|
|
||||||
|
|
@ -4,6 +4,9 @@
|
|||||||
from typing import Dict, Optional, List, Set
|
from typing import Dict, Optional, List, Set
|
||||||
|
|
||||||
from PyQt5.QtCore import QObject, pyqtSlot
|
from PyQt5.QtCore import QObject, pyqtSlot
|
||||||
|
|
||||||
|
from UM.Util import parseBool
|
||||||
|
|
||||||
from cura.Machines.ContainerNode import ContainerNode
|
from cura.Machines.ContainerNode import ContainerNode
|
||||||
|
|
||||||
|
|
||||||
@ -29,6 +32,7 @@ class QualityGroup(QObject):
|
|||||||
self.nodes_for_extruders = {} # type: Dict[int, ContainerNode]
|
self.nodes_for_extruders = {} # type: Dict[int, ContainerNode]
|
||||||
self.quality_type = quality_type
|
self.quality_type = quality_type
|
||||||
self.is_available = False
|
self.is_available = False
|
||||||
|
self.is_experimental = False
|
||||||
|
|
||||||
@pyqtSlot(result = str)
|
@pyqtSlot(result = str)
|
||||||
def getName(self) -> str:
|
def getName(self) -> str:
|
||||||
@ -51,3 +55,17 @@ class QualityGroup(QObject):
|
|||||||
for extruder_node in self.nodes_for_extruders.values():
|
for extruder_node in self.nodes_for_extruders.values():
|
||||||
result.append(extruder_node)
|
result.append(extruder_node)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
def setGlobalNode(self, node: "ContainerNode") -> None:
|
||||||
|
self.node_for_global = node
|
||||||
|
|
||||||
|
# Update is_experimental flag
|
||||||
|
is_experimental = parseBool(node.getMetaDataEntry("is_experimental", False))
|
||||||
|
self.is_experimental |= is_experimental
|
||||||
|
|
||||||
|
def setExtruderNode(self, position: int, node: "ContainerNode") -> None:
|
||||||
|
self.nodes_for_extruders[position] = node
|
||||||
|
|
||||||
|
# Update is_experimental flag
|
||||||
|
is_experimental = parseBool(node.getMetaDataEntry("is_experimental", False))
|
||||||
|
self.is_experimental |= is_experimental
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
# Copyright (c) 2018 Ultimaker B.V.
|
# Copyright (c) 2018 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 TYPE_CHECKING, Optional, cast, Dict, List
|
from typing import TYPE_CHECKING, Optional, cast, Dict, List, Set
|
||||||
|
|
||||||
from PyQt5.QtCore import QObject, QTimer, pyqtSignal, pyqtSlot
|
from PyQt5.QtCore import QObject, QTimer, pyqtSignal, pyqtSlot
|
||||||
|
|
||||||
@ -16,7 +16,7 @@ from .QualityGroup import QualityGroup
|
|||||||
from .QualityNode import QualityNode
|
from .QualityNode import QualityNode
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from UM.Settings.DefinitionContainer import DefinitionContainer
|
from UM.Settings.Interfaces import DefinitionContainerInterface
|
||||||
from cura.Settings.GlobalStack import GlobalStack
|
from cura.Settings.GlobalStack import GlobalStack
|
||||||
from .QualityChangesGroup import QualityChangesGroup
|
from .QualityChangesGroup import QualityChangesGroup
|
||||||
from cura.CuraApplication import CuraApplication
|
from cura.CuraApplication import CuraApplication
|
||||||
@ -202,13 +202,11 @@ class QualityManager(QObject):
|
|||||||
def getQualityGroups(self, machine: "GlobalStack") -> Dict[str, QualityGroup]:
|
def getQualityGroups(self, machine: "GlobalStack") -> Dict[str, QualityGroup]:
|
||||||
machine_definition_id = getMachineDefinitionIDForQualitySearch(machine.definition)
|
machine_definition_id = getMachineDefinitionIDForQualitySearch(machine.definition)
|
||||||
|
|
||||||
# This determines if we should only get the global qualities for the global stack and skip the global qualities for the extruder stacks
|
|
||||||
has_machine_specific_qualities = machine.getHasMachineQuality()
|
|
||||||
|
|
||||||
# To find the quality container for the GlobalStack, check in the following fall-back manner:
|
# To find the quality container for the GlobalStack, check in the following fall-back manner:
|
||||||
# (1) the machine-specific node
|
# (1) the machine-specific node
|
||||||
# (2) the generic node
|
# (2) the generic node
|
||||||
machine_node = self._machine_nozzle_buildplate_material_quality_type_to_quality_dict.get(machine_definition_id)
|
machine_node = self._machine_nozzle_buildplate_material_quality_type_to_quality_dict.get(machine_definition_id)
|
||||||
|
|
||||||
# Check if this machine has specific quality profiles for its extruders, if so, when looking up extruder
|
# Check if this machine has specific quality profiles for its extruders, if so, when looking up extruder
|
||||||
# qualities, we should not fall back to use the global qualities.
|
# qualities, we should not fall back to use the global qualities.
|
||||||
has_extruder_specific_qualities = False
|
has_extruder_specific_qualities = False
|
||||||
@ -235,7 +233,7 @@ class QualityManager(QObject):
|
|||||||
|
|
||||||
for quality_type, quality_node in node.quality_type_map.items():
|
for quality_type, quality_node in node.quality_type_map.items():
|
||||||
quality_group = QualityGroup(quality_node.getMetaDataEntry("name", ""), quality_type)
|
quality_group = QualityGroup(quality_node.getMetaDataEntry("name", ""), quality_type)
|
||||||
quality_group.node_for_global = quality_node
|
quality_group.setGlobalNode(quality_node)
|
||||||
quality_group_dict[quality_type] = quality_group
|
quality_group_dict[quality_type] = quality_group
|
||||||
break
|
break
|
||||||
|
|
||||||
@ -259,11 +257,15 @@ class QualityManager(QObject):
|
|||||||
root_material_id = self._material_manager.getRootMaterialIDWithoutDiameter(root_material_id)
|
root_material_id = self._material_manager.getRootMaterialIDWithoutDiameter(root_material_id)
|
||||||
root_material_id_list.append(root_material_id)
|
root_material_id_list.append(root_material_id)
|
||||||
|
|
||||||
# Also try to get the fallback material
|
# Also try to get the fallback materials
|
||||||
material_type = extruder.material.getMetaDataEntry("material")
|
fallback_ids = self._material_manager.getFallBackMaterialIdsByMaterial(extruder.material)
|
||||||
fallback_root_material_id = self._material_manager.getFallbackMaterialIdByMaterialType(material_type)
|
|
||||||
if fallback_root_material_id:
|
if fallback_ids:
|
||||||
root_material_id_list.append(fallback_root_material_id)
|
root_material_id_list.extend(fallback_ids)
|
||||||
|
|
||||||
|
# Weed out duplicates while preserving the order.
|
||||||
|
seen = set() # type: Set[str]
|
||||||
|
root_material_id_list = [x for x in root_material_id_list if x not in seen and not seen.add(x)] # type: ignore
|
||||||
|
|
||||||
# Here we construct a list of nodes we want to look for qualities with the highest priority first.
|
# Here we construct a list of nodes we want to look for qualities with the highest priority first.
|
||||||
# The use case is that, when we look for qualities for a machine, we first want to search in the following
|
# The use case is that, when we look for qualities for a machine, we first want to search in the following
|
||||||
@ -333,7 +335,7 @@ class QualityManager(QObject):
|
|||||||
|
|
||||||
quality_group = quality_group_dict[quality_type]
|
quality_group = quality_group_dict[quality_type]
|
||||||
if position not in quality_group.nodes_for_extruders:
|
if position not in quality_group.nodes_for_extruders:
|
||||||
quality_group.nodes_for_extruders[position] = quality_node
|
quality_group.setExtruderNode(position, quality_node)
|
||||||
|
|
||||||
# If the machine has its own specific qualities, for extruders, it should skip the global qualities
|
# If the machine has its own specific qualities, for extruders, it should skip the global qualities
|
||||||
# and use the material/variant specific qualities.
|
# and use the material/variant specific qualities.
|
||||||
@ -363,7 +365,7 @@ class QualityManager(QObject):
|
|||||||
if node and node.quality_type_map:
|
if node and node.quality_type_map:
|
||||||
for quality_type, quality_node in node.quality_type_map.items():
|
for quality_type, quality_node in node.quality_type_map.items():
|
||||||
quality_group = QualityGroup(quality_node.getMetaDataEntry("name", ""), quality_type)
|
quality_group = QualityGroup(quality_node.getMetaDataEntry("name", ""), quality_type)
|
||||||
quality_group.node_for_global = quality_node
|
quality_group.setGlobalNode(quality_node)
|
||||||
quality_group_dict[quality_type] = quality_group
|
quality_group_dict[quality_type] = quality_group
|
||||||
break
|
break
|
||||||
|
|
||||||
@ -437,7 +439,8 @@ class QualityManager(QObject):
|
|||||||
quality_changes_group = quality_model_item["quality_changes_group"]
|
quality_changes_group = quality_model_item["quality_changes_group"]
|
||||||
if quality_changes_group is None:
|
if quality_changes_group is None:
|
||||||
# create global quality changes only
|
# create global quality changes only
|
||||||
new_quality_changes = self._createQualityChanges(quality_group.quality_type, quality_changes_name,
|
new_name = self._container_registry.uniqueName(quality_changes_name)
|
||||||
|
new_quality_changes = self._createQualityChanges(quality_group.quality_type, new_name,
|
||||||
global_stack, None)
|
global_stack, None)
|
||||||
self._container_registry.addContainer(new_quality_changes)
|
self._container_registry.addContainer(new_quality_changes)
|
||||||
else:
|
else:
|
||||||
@ -534,7 +537,7 @@ class QualityManager(QObject):
|
|||||||
# Example: for an Ultimaker 3 Extended, it has "quality_definition = ultimaker3". This means Ultimaker 3 Extended
|
# Example: for an Ultimaker 3 Extended, it has "quality_definition = ultimaker3". This means Ultimaker 3 Extended
|
||||||
# shares the same set of qualities profiles as Ultimaker 3.
|
# shares the same set of qualities profiles as Ultimaker 3.
|
||||||
#
|
#
|
||||||
def getMachineDefinitionIDForQualitySearch(machine_definition: "DefinitionContainer",
|
def getMachineDefinitionIDForQualitySearch(machine_definition: "DefinitionContainerInterface",
|
||||||
default_definition_id: str = "fdmprinter") -> str:
|
default_definition_id: str = "fdmprinter") -> str:
|
||||||
machine_definition_id = default_definition_id
|
machine_definition_id = default_definition_id
|
||||||
if parseBool(machine_definition.getMetaDataEntry("has_machine_quality", False)):
|
if parseBool(machine_definition.getMetaDataEntry("has_machine_quality", False)):
|
||||||
|
@ -107,7 +107,7 @@ class VariantManager:
|
|||||||
break
|
break
|
||||||
return variant_node
|
return variant_node
|
||||||
|
|
||||||
return self._machine_to_variant_dict_map[machine_definition_id].get(variant_type, {}).get(variant_name)
|
return self._machine_to_variant_dict_map.get(machine_definition_id, {}).get(variant_type, {}).get(variant_name)
|
||||||
|
|
||||||
def getVariantNodes(self, machine: "GlobalStack", variant_type: "VariantType") -> Dict[str, ContainerNode]:
|
def getVariantNodes(self, machine: "GlobalStack", variant_type: "VariantType") -> Dict[str, ContainerNode]:
|
||||||
machine_definition_id = machine.definition.getId()
|
machine_definition_id = machine.definition.getId()
|
||||||
|
@ -25,7 +25,7 @@ class MultiplyObjectsJob(Job):
|
|||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
status_message = Message(i18n_catalog.i18nc("@info:status", "Multiplying and placing objects"), lifetime=0,
|
status_message = Message(i18n_catalog.i18nc("@info:status", "Multiplying and placing objects"), lifetime=0,
|
||||||
dismissable=False, progress=0, title = i18n_catalog.i18nc("@info:title", "Placing Object"))
|
dismissable=False, progress=0, title = i18n_catalog.i18nc("@info:title", "Placing Objects"))
|
||||||
status_message.show()
|
status_message.show()
|
||||||
scene = Application.getInstance().getController().getScene()
|
scene = Application.getInstance().getController().getScene()
|
||||||
|
|
||||||
|
@ -1,33 +1,37 @@
|
|||||||
# Copyright (c) 2018 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 datetime import datetime
|
||||||
import json
|
import json
|
||||||
import random
|
import random
|
||||||
from hashlib import sha512
|
from hashlib import sha512
|
||||||
from base64 import b64encode
|
from base64 import b64encode
|
||||||
from typing import Dict, Optional
|
from typing import Optional
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
|
from UM.i18n import i18nCatalog
|
||||||
from UM.Logger import Logger
|
from UM.Logger import Logger
|
||||||
|
|
||||||
from cura.OAuth2.Models import AuthenticationResponse, UserProfile, OAuth2Settings
|
from cura.OAuth2.Models import AuthenticationResponse, UserProfile, OAuth2Settings
|
||||||
|
catalog = i18nCatalog("cura")
|
||||||
|
TOKEN_TIMESTAMP_FORMAT = "%Y-%m-%d %H:%M:%S"
|
||||||
|
|
||||||
|
## Class containing several helpers to deal with the authorization flow.
|
||||||
# Class containing several helpers to deal with the authorization flow.
|
|
||||||
class AuthorizationHelpers:
|
class AuthorizationHelpers:
|
||||||
def __init__(self, settings: "OAuth2Settings") -> None:
|
def __init__(self, settings: "OAuth2Settings") -> None:
|
||||||
self._settings = settings
|
self._settings = settings
|
||||||
self._token_url = "{}/token".format(self._settings.OAUTH_SERVER_URL)
|
self._token_url = "{}/token".format(self._settings.OAUTH_SERVER_URL)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
# The OAuth2 settings object.
|
## The OAuth2 settings object.
|
||||||
def settings(self) -> "OAuth2Settings":
|
def settings(self) -> "OAuth2Settings":
|
||||||
return self._settings
|
return self._settings
|
||||||
|
|
||||||
# Request the access token from the authorization server.
|
## Request the access token from the authorization server.
|
||||||
# \param authorization_code: The authorization code from the 1st step.
|
# \param authorization_code: The authorization code from the 1st step.
|
||||||
# \param verification_code: The verification code needed for the PKCE extension.
|
# \param verification_code: The verification code needed for the PKCE
|
||||||
# \return: An AuthenticationResponse object.
|
# extension.
|
||||||
|
# \return An AuthenticationResponse object.
|
||||||
def getAccessTokenUsingAuthorizationCode(self, authorization_code: str, verification_code: str) -> "AuthenticationResponse":
|
def getAccessTokenUsingAuthorizationCode(self, authorization_code: str, verification_code: str) -> "AuthenticationResponse":
|
||||||
data = {
|
data = {
|
||||||
"client_id": self._settings.CLIENT_ID if self._settings.CLIENT_ID is not None else "",
|
"client_id": self._settings.CLIENT_ID if self._settings.CLIENT_ID is not None else "",
|
||||||
@ -37,12 +41,16 @@ class AuthorizationHelpers:
|
|||||||
"code_verifier": verification_code,
|
"code_verifier": verification_code,
|
||||||
"scope": self._settings.CLIENT_SCOPES if self._settings.CLIENT_SCOPES is not None else "",
|
"scope": self._settings.CLIENT_SCOPES if self._settings.CLIENT_SCOPES is not None else "",
|
||||||
}
|
}
|
||||||
|
try:
|
||||||
return self.parseTokenResponse(requests.post(self._token_url, data = data)) # type: ignore
|
return self.parseTokenResponse(requests.post(self._token_url, data = data)) # type: ignore
|
||||||
|
except requests.exceptions.ConnectionError:
|
||||||
|
return AuthenticationResponse(success=False, err_message="Unable to connect to remote server")
|
||||||
|
|
||||||
# Request the access token from the authorization server using a refresh token.
|
## Request the access token from the authorization server using a refresh token.
|
||||||
# \param refresh_token:
|
# \param refresh_token:
|
||||||
# \return: An AuthenticationResponse object.
|
# \return An AuthenticationResponse object.
|
||||||
def getAccessTokenUsingRefreshToken(self, refresh_token: str) -> "AuthenticationResponse":
|
def getAccessTokenUsingRefreshToken(self, refresh_token: str) -> "AuthenticationResponse":
|
||||||
|
Logger.log("d", "Refreshing the access token.")
|
||||||
data = {
|
data = {
|
||||||
"client_id": self._settings.CLIENT_ID if self._settings.CLIENT_ID is not None else "",
|
"client_id": self._settings.CLIENT_ID if self._settings.CLIENT_ID is not None else "",
|
||||||
"redirect_uri": self._settings.CALLBACK_URL if self._settings.CALLBACK_URL is not None else "",
|
"redirect_uri": self._settings.CALLBACK_URL if self._settings.CALLBACK_URL is not None else "",
|
||||||
@ -50,12 +58,15 @@ class AuthorizationHelpers:
|
|||||||
"refresh_token": refresh_token,
|
"refresh_token": refresh_token,
|
||||||
"scope": self._settings.CLIENT_SCOPES if self._settings.CLIENT_SCOPES is not None else "",
|
"scope": self._settings.CLIENT_SCOPES if self._settings.CLIENT_SCOPES is not None else "",
|
||||||
}
|
}
|
||||||
|
try:
|
||||||
return self.parseTokenResponse(requests.post(self._token_url, data = data)) # type: ignore
|
return self.parseTokenResponse(requests.post(self._token_url, data = data)) # type: ignore
|
||||||
|
except requests.exceptions.ConnectionError:
|
||||||
|
return AuthenticationResponse(success=False, err_message="Unable to connect to remote server")
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
# Parse the token response from the authorization server into an AuthenticationResponse object.
|
## Parse the token response from the authorization server into an AuthenticationResponse object.
|
||||||
# \param token_response: The JSON string data response from the authorization server.
|
# \param token_response: The JSON string data response from the authorization server.
|
||||||
# \return: An AuthenticationResponse object.
|
# \return An AuthenticationResponse object.
|
||||||
def parseTokenResponse(token_response: requests.models.Response) -> "AuthenticationResponse":
|
def parseTokenResponse(token_response: requests.models.Response) -> "AuthenticationResponse":
|
||||||
token_data = None
|
token_data = None
|
||||||
|
|
||||||
@ -65,25 +76,31 @@ class AuthorizationHelpers:
|
|||||||
Logger.log("w", "Could not parse token response data: %s", token_response.text)
|
Logger.log("w", "Could not parse token response data: %s", token_response.text)
|
||||||
|
|
||||||
if not token_data:
|
if not token_data:
|
||||||
return AuthenticationResponse(success=False, err_message="Could not read response.")
|
return AuthenticationResponse(success = False, err_message = catalog.i18nc("@message", "Could not read response."))
|
||||||
|
|
||||||
if token_response.status_code not in (200, 201):
|
if token_response.status_code not in (200, 201):
|
||||||
return AuthenticationResponse(success=False, err_message=token_data["error_description"])
|
return AuthenticationResponse(success = False, err_message = token_data["error_description"])
|
||||||
|
|
||||||
return AuthenticationResponse(success=True,
|
return AuthenticationResponse(success=True,
|
||||||
token_type=token_data["token_type"],
|
token_type=token_data["token_type"],
|
||||||
access_token=token_data["access_token"],
|
access_token=token_data["access_token"],
|
||||||
refresh_token=token_data["refresh_token"],
|
refresh_token=token_data["refresh_token"],
|
||||||
expires_in=token_data["expires_in"],
|
expires_in=token_data["expires_in"],
|
||||||
scope=token_data["scope"])
|
scope=token_data["scope"],
|
||||||
|
received_at=datetime.now().strftime(TOKEN_TIMESTAMP_FORMAT))
|
||||||
|
|
||||||
# Calls the authentication API endpoint to get the token data.
|
## Calls the authentication API endpoint to get the token data.
|
||||||
# \param access_token: The encoded JWT token.
|
# \param access_token: The encoded JWT token.
|
||||||
# \return: Dict containing some profile data.
|
# \return Dict containing some profile data.
|
||||||
def parseJWT(self, access_token: str) -> Optional["UserProfile"]:
|
def parseJWT(self, access_token: str) -> Optional["UserProfile"]:
|
||||||
|
try:
|
||||||
token_request = requests.get("{}/check-token".format(self._settings.OAUTH_SERVER_URL), headers = {
|
token_request = requests.get("{}/check-token".format(self._settings.OAUTH_SERVER_URL), headers = {
|
||||||
"Authorization": "Bearer {}".format(access_token)
|
"Authorization": "Bearer {}".format(access_token)
|
||||||
})
|
})
|
||||||
|
except requests.exceptions.ConnectionError:
|
||||||
|
# Connection was suddenly dropped. Nothing we can do about that.
|
||||||
|
Logger.log("w", "Something failed while attempting to parse the JWT token")
|
||||||
|
return None
|
||||||
if token_request.status_code not in (200, 201):
|
if token_request.status_code not in (200, 201):
|
||||||
Logger.log("w", "Could not retrieve token data from auth server: %s", token_request.text)
|
Logger.log("w", "Could not retrieve token data from auth server: %s", token_request.text)
|
||||||
return None
|
return None
|
||||||
@ -98,15 +115,15 @@ class AuthorizationHelpers:
|
|||||||
)
|
)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
# Generate a 16-character verification code.
|
## Generate a 16-character verification code.
|
||||||
# \param code_length: How long should the code be?
|
# \param code_length: How long should the code be?
|
||||||
def generateVerificationCode(code_length: int = 16) -> str:
|
def generateVerificationCode(code_length: int = 16) -> str:
|
||||||
return "".join(random.choice("0123456789ABCDEF") for i in range(code_length))
|
return "".join(random.choice("0123456789ABCDEF") for i in range(code_length))
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
# Generates a base64 encoded sha512 encrypted version of a given string.
|
## Generates a base64 encoded sha512 encrypted version of a given string.
|
||||||
# \param verification_code:
|
# \param verification_code:
|
||||||
# \return: The encrypted code in base64 format.
|
# \return The encrypted code in base64 format.
|
||||||
def generateVerificationCodeChallenge(verification_code: str) -> str:
|
def generateVerificationCodeChallenge(verification_code: str) -> str:
|
||||||
encoded = sha512(verification_code.encode()).digest()
|
encoded = sha512(verification_code.encode()).digest()
|
||||||
return b64encode(encoded, altchars = b"_-").decode()
|
return b64encode(encoded, altchars = b"_-").decode()
|
||||||
|
@ -1,25 +1,27 @@
|
|||||||
# Copyright (c) 2018 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 typing import Optional, Callable, Tuple, Dict, Any, List, TYPE_CHECKING
|
|
||||||
|
|
||||||
from http.server import BaseHTTPRequestHandler
|
from http.server import BaseHTTPRequestHandler
|
||||||
|
from typing import Optional, Callable, Tuple, Dict, Any, List, TYPE_CHECKING
|
||||||
from urllib.parse import parse_qs, urlparse
|
from urllib.parse import parse_qs, urlparse
|
||||||
|
|
||||||
from cura.OAuth2.Models import AuthenticationResponse, ResponseData, HTTP_STATUS
|
from cura.OAuth2.Models import AuthenticationResponse, ResponseData, HTTP_STATUS
|
||||||
|
from UM.i18n import i18nCatalog
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from cura.OAuth2.Models import ResponseStatus
|
from cura.OAuth2.Models import ResponseStatus
|
||||||
from cura.OAuth2.AuthorizationHelpers import AuthorizationHelpers
|
from cura.OAuth2.AuthorizationHelpers import AuthorizationHelpers
|
||||||
|
|
||||||
|
catalog = i18nCatalog("cura")
|
||||||
|
|
||||||
# This handler handles all HTTP requests on the local web server.
|
## This handler handles all HTTP requests on the local web server.
|
||||||
# It also requests the access token for the 2nd stage of the OAuth flow.
|
# It also requests the access token for the 2nd stage of the OAuth flow.
|
||||||
class AuthorizationRequestHandler(BaseHTTPRequestHandler):
|
class AuthorizationRequestHandler(BaseHTTPRequestHandler):
|
||||||
def __init__(self, request, client_address, server) -> None:
|
def __init__(self, request, client_address, server) -> None:
|
||||||
super().__init__(request, client_address, server)
|
super().__init__(request, client_address, server)
|
||||||
|
|
||||||
# These values will be injected by the HTTPServer that this handler belongs to.
|
# These values will be injected by the HTTPServer that this handler belongs to.
|
||||||
self.authorization_helpers = None # type: Optional["AuthorizationHelpers"]
|
self.authorization_helpers = None # type: Optional[AuthorizationHelpers]
|
||||||
self.authorization_callback = None # type: Optional[Callable[[AuthenticationResponse], None]]
|
self.authorization_callback = None # type: Optional[Callable[[AuthenticationResponse], None]]
|
||||||
self.verification_code = None # type: Optional[str]
|
self.verification_code = None # type: Optional[str]
|
||||||
|
|
||||||
@ -47,9 +49,9 @@ class AuthorizationRequestHandler(BaseHTTPRequestHandler):
|
|||||||
# This will cause the server to shut down, so we do it at the very end of the request handling.
|
# This will cause the server to shut down, so we do it at the very end of the request handling.
|
||||||
self.authorization_callback(token_response)
|
self.authorization_callback(token_response)
|
||||||
|
|
||||||
# Handler for the callback URL redirect.
|
## Handler for the callback URL redirect.
|
||||||
# \param query: Dict containing the HTTP query parameters.
|
# \param query Dict containing the HTTP query parameters.
|
||||||
# \return: HTTP ResponseData containing a success page to show to the user.
|
# \return HTTP ResponseData containing a success page to show to the user.
|
||||||
def _handleCallback(self, query: Dict[Any, List]) -> Tuple[ResponseData, Optional[AuthenticationResponse]]:
|
def _handleCallback(self, query: Dict[Any, List]) -> Tuple[ResponseData, Optional[AuthenticationResponse]]:
|
||||||
code = self._queryGet(query, "code")
|
code = self._queryGet(query, "code")
|
||||||
if code and self.authorization_helpers is not None and self.verification_code is not None:
|
if code and self.authorization_helpers is not None and self.verification_code is not None:
|
||||||
@ -60,30 +62,30 @@ class AuthorizationRequestHandler(BaseHTTPRequestHandler):
|
|||||||
elif self._queryGet(query, "error_code") == "user_denied":
|
elif self._queryGet(query, "error_code") == "user_denied":
|
||||||
# Otherwise we show an error message (probably the user clicked "Deny" in the auth dialog).
|
# Otherwise we show an error message (probably the user clicked "Deny" in the auth dialog).
|
||||||
token_response = AuthenticationResponse(
|
token_response = AuthenticationResponse(
|
||||||
success=False,
|
success = False,
|
||||||
err_message="Please give the required permissions when authorizing this application."
|
err_message = catalog.i18nc("@message", "Please give the required permissions when authorizing this application.")
|
||||||
)
|
)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# We don't know what went wrong here, so instruct the user to check the logs.
|
# We don't know what went wrong here, so instruct the user to check the logs.
|
||||||
token_response = AuthenticationResponse(
|
token_response = AuthenticationResponse(
|
||||||
success=False,
|
success = False,
|
||||||
error_message="Something unexpected happened when trying to log in, please try again."
|
error_message = catalog.i18nc("@message", "Something unexpected happened when trying to log in, please try again.")
|
||||||
)
|
)
|
||||||
if self.authorization_helpers is None:
|
if self.authorization_helpers is None:
|
||||||
return ResponseData(), token_response
|
return ResponseData(), token_response
|
||||||
|
|
||||||
return ResponseData(
|
return ResponseData(
|
||||||
status=HTTP_STATUS["REDIRECT"],
|
status = HTTP_STATUS["REDIRECT"],
|
||||||
data_stream=b"Redirecting...",
|
data_stream = b"Redirecting...",
|
||||||
redirect_uri=self.authorization_helpers.settings.AUTH_SUCCESS_REDIRECT if token_response.success else
|
redirect_uri = self.authorization_helpers.settings.AUTH_SUCCESS_REDIRECT if token_response.success else
|
||||||
self.authorization_helpers.settings.AUTH_FAILED_REDIRECT
|
self.authorization_helpers.settings.AUTH_FAILED_REDIRECT
|
||||||
), token_response
|
), token_response
|
||||||
|
|
||||||
|
## Handle all other non-existing server calls.
|
||||||
@staticmethod
|
@staticmethod
|
||||||
# Handle all other non-existing server calls.
|
|
||||||
def _handleNotFound() -> ResponseData:
|
def _handleNotFound() -> ResponseData:
|
||||||
return ResponseData(status=HTTP_STATUS["NOT_FOUND"], content_type="text/html", data_stream=b"Not found.")
|
return ResponseData(status = HTTP_STATUS["NOT_FOUND"], content_type = "text/html", data_stream = b"Not found.")
|
||||||
|
|
||||||
def _sendHeaders(self, status: "ResponseStatus", content_type: str, redirect_uri: str = None) -> None:
|
def _sendHeaders(self, status: "ResponseStatus", content_type: str, redirect_uri: str = None) -> None:
|
||||||
self.send_response(status.code, status.message)
|
self.send_response(status.code, status.message)
|
||||||
@ -95,7 +97,7 @@ class AuthorizationRequestHandler(BaseHTTPRequestHandler):
|
|||||||
def _sendData(self, data: bytes) -> None:
|
def _sendData(self, data: bytes) -> None:
|
||||||
self.wfile.write(data)
|
self.wfile.write(data)
|
||||||
|
|
||||||
|
## Convenience helper for getting values from a pre-parsed query string
|
||||||
@staticmethod
|
@staticmethod
|
||||||
# Convenience Helper for getting values from a pre-parsed query string
|
def _queryGet(query_data: Dict[Any, List], key: str, default: Optional[str] = None) -> Optional[str]:
|
||||||
def _queryGet(query_data: Dict[Any, List], key: str, default: Optional[str]=None) -> Optional[str]:
|
|
||||||
return query_data.get(key, [default])[0]
|
return query_data.get(key, [default])[0]
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
# Copyright (c) 2018 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 http.server import HTTPServer
|
from http.server import HTTPServer
|
||||||
from typing import Callable, Any, TYPE_CHECKING
|
from typing import Callable, Any, TYPE_CHECKING
|
||||||
|
|
||||||
@ -8,19 +9,19 @@ if TYPE_CHECKING:
|
|||||||
from cura.OAuth2.AuthorizationHelpers import AuthorizationHelpers
|
from cura.OAuth2.AuthorizationHelpers import AuthorizationHelpers
|
||||||
|
|
||||||
|
|
||||||
# The authorization request callback handler server.
|
## The authorization request callback handler server.
|
||||||
# This subclass is needed to be able to pass some data to the request handler.
|
# This subclass is needed to be able to pass some data to the request handler.
|
||||||
# This cannot be done on the request handler directly as the HTTPServer creates an instance of the handler after
|
# This cannot be done on the request handler directly as the HTTPServer
|
||||||
# init.
|
# creates an instance of the handler after init.
|
||||||
class AuthorizationRequestServer(HTTPServer):
|
class AuthorizationRequestServer(HTTPServer):
|
||||||
# Set the authorization helpers instance on the request handler.
|
## Set the authorization helpers instance on the request handler.
|
||||||
def setAuthorizationHelpers(self, authorization_helpers: "AuthorizationHelpers") -> None:
|
def setAuthorizationHelpers(self, authorization_helpers: "AuthorizationHelpers") -> None:
|
||||||
self.RequestHandlerClass.authorization_helpers = authorization_helpers # type: ignore
|
self.RequestHandlerClass.authorization_helpers = authorization_helpers # type: ignore
|
||||||
|
|
||||||
# Set the authorization callback on the request handler.
|
## Set the authorization callback on the request handler.
|
||||||
def setAuthorizationCallback(self, authorization_callback: Callable[["AuthenticationResponse"], Any]) -> None:
|
def setAuthorizationCallback(self, authorization_callback: Callable[["AuthenticationResponse"], Any]) -> None:
|
||||||
self.RequestHandlerClass.authorization_callback = authorization_callback # type: ignore
|
self.RequestHandlerClass.authorization_callback = authorization_callback # type: ignore
|
||||||
|
|
||||||
# Set the verification code on the request handler.
|
## Set the verification code on the request handler.
|
||||||
def setVerificationCode(self, verification_code: str) -> None:
|
def setVerificationCode(self, verification_code: str) -> None:
|
||||||
self.RequestHandlerClass.verification_code = verification_code # type: ignore
|
self.RequestHandlerClass.verification_code = verification_code # type: ignore
|
||||||
|
@ -1,34 +1,41 @@
|
|||||||
# Copyright (c) 2018 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 webbrowser
|
import webbrowser
|
||||||
|
from datetime import datetime, timedelta
|
||||||
from typing import Optional, TYPE_CHECKING
|
from typing import Optional, TYPE_CHECKING
|
||||||
from urllib.parse import urlencode
|
from urllib.parse import urlencode
|
||||||
|
import requests.exceptions
|
||||||
|
|
||||||
|
|
||||||
from UM.Logger import Logger
|
from UM.Logger import Logger
|
||||||
|
from UM.Message import Message
|
||||||
from UM.Signal import Signal
|
from UM.Signal import Signal
|
||||||
|
|
||||||
from cura.OAuth2.LocalAuthorizationServer import LocalAuthorizationServer
|
from cura.OAuth2.LocalAuthorizationServer import LocalAuthorizationServer
|
||||||
from cura.OAuth2.AuthorizationHelpers import AuthorizationHelpers
|
from cura.OAuth2.AuthorizationHelpers import AuthorizationHelpers, TOKEN_TIMESTAMP_FORMAT
|
||||||
from cura.OAuth2.Models import AuthenticationResponse
|
from cura.OAuth2.Models import AuthenticationResponse
|
||||||
|
|
||||||
|
from UM.i18n import i18nCatalog
|
||||||
|
i18n_catalog = i18nCatalog("cura")
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from cura.OAuth2.Models import UserProfile, OAuth2Settings
|
from cura.OAuth2.Models import UserProfile, OAuth2Settings
|
||||||
from UM.Preferences import Preferences
|
from UM.Preferences import Preferences
|
||||||
|
|
||||||
|
|
||||||
|
## The authorization service is responsible for handling the login flow,
|
||||||
|
# storing user credentials and providing account information.
|
||||||
class AuthorizationService:
|
class AuthorizationService:
|
||||||
"""
|
|
||||||
The authorization service is responsible for handling the login flow,
|
|
||||||
storing user credentials and providing account information.
|
|
||||||
"""
|
|
||||||
|
|
||||||
# Emit signal when authentication is completed.
|
# Emit signal when authentication is completed.
|
||||||
onAuthStateChanged = Signal()
|
onAuthStateChanged = Signal()
|
||||||
|
|
||||||
# Emit signal when authentication failed.
|
# Emit signal when authentication failed.
|
||||||
onAuthenticationError = Signal()
|
onAuthenticationError = Signal()
|
||||||
|
|
||||||
|
accessTokenChanged = Signal()
|
||||||
|
|
||||||
def __init__(self, settings: "OAuth2Settings", preferences: Optional["Preferences"] = None) -> None:
|
def __init__(self, settings: "OAuth2Settings", preferences: Optional["Preferences"] = None) -> None:
|
||||||
self._settings = settings
|
self._settings = settings
|
||||||
self._auth_helpers = AuthorizationHelpers(settings)
|
self._auth_helpers = AuthorizationHelpers(settings)
|
||||||
@ -38,31 +45,48 @@ class AuthorizationService:
|
|||||||
self._preferences = preferences
|
self._preferences = preferences
|
||||||
self._server = LocalAuthorizationServer(self._auth_helpers, self._onAuthStateChanged, daemon=True)
|
self._server = LocalAuthorizationServer(self._auth_helpers, self._onAuthStateChanged, daemon=True)
|
||||||
|
|
||||||
|
self._unable_to_get_data_message = None # type: Optional[Message]
|
||||||
|
|
||||||
|
self.onAuthStateChanged.connect(self._authChanged)
|
||||||
|
|
||||||
|
def _authChanged(self, logged_in):
|
||||||
|
if logged_in and self._unable_to_get_data_message is not None:
|
||||||
|
self._unable_to_get_data_message.hide()
|
||||||
|
|
||||||
def initialize(self, preferences: Optional["Preferences"] = None) -> None:
|
def initialize(self, preferences: Optional["Preferences"] = None) -> None:
|
||||||
if preferences is not None:
|
if preferences is not None:
|
||||||
self._preferences = preferences
|
self._preferences = preferences
|
||||||
if self._preferences:
|
if self._preferences:
|
||||||
self._preferences.addPreference(self._settings.AUTH_DATA_PREFERENCE_KEY, "{}")
|
self._preferences.addPreference(self._settings.AUTH_DATA_PREFERENCE_KEY, "{}")
|
||||||
|
|
||||||
# Get the user profile as obtained from the JWT (JSON Web Token).
|
## Get the user profile as obtained from the JWT (JSON Web Token).
|
||||||
# If the JWT is not yet parsed, calling this will take care of that.
|
# If the JWT is not yet parsed, calling this will take care of that.
|
||||||
# \return UserProfile if a user is logged in, None otherwise.
|
# \return UserProfile if a user is logged in, None otherwise.
|
||||||
# \sa _parseJWT
|
# \sa _parseJWT
|
||||||
def getUserProfile(self) -> Optional["UserProfile"]:
|
def getUserProfile(self) -> Optional["UserProfile"]:
|
||||||
if not self._user_profile:
|
if not self._user_profile:
|
||||||
# If no user profile was stored locally, we try to get it from JWT.
|
# If no user profile was stored locally, we try to get it from JWT.
|
||||||
|
try:
|
||||||
self._user_profile = self._parseJWT()
|
self._user_profile = self._parseJWT()
|
||||||
if not self._user_profile:
|
except requests.exceptions.ConnectionError:
|
||||||
|
# Unable to get connection, can't login.
|
||||||
|
Logger.logException("w", "Unable to validate user data with the remote server.")
|
||||||
|
return None
|
||||||
|
|
||||||
|
if not self._user_profile and self._auth_data:
|
||||||
# If there is still no user profile from the JWT, we have to log in again.
|
# If there is still no user profile from the JWT, we have to log in again.
|
||||||
|
Logger.log("w", "The user profile could not be loaded. The user must log in again!")
|
||||||
|
self.deleteAuthData()
|
||||||
return None
|
return None
|
||||||
|
|
||||||
return self._user_profile
|
return self._user_profile
|
||||||
|
|
||||||
# Tries to parse the JWT (JSON Web Token) data, which it does if all the needed data is there.
|
## Tries to parse the JWT (JSON Web Token) data, which it does if all the needed data is there.
|
||||||
# \return UserProfile if it was able to parse, None otherwise.
|
# \return UserProfile if it was able to parse, None otherwise.
|
||||||
def _parseJWT(self) -> Optional["UserProfile"]:
|
def _parseJWT(self) -> Optional["UserProfile"]:
|
||||||
if not self._auth_data or self._auth_data.access_token is None:
|
if not self._auth_data or self._auth_data.access_token is None:
|
||||||
# If no auth data exists, we should always log in again.
|
# If no auth data exists, we should always log in again.
|
||||||
|
Logger.log("d", "There was no auth data or access token")
|
||||||
return None
|
return None
|
||||||
user_data = self._auth_helpers.parseJWT(self._auth_data.access_token)
|
user_data = self._auth_helpers.parseJWT(self._auth_data.access_token)
|
||||||
if user_data:
|
if user_data:
|
||||||
@ -70,41 +94,54 @@ class AuthorizationService:
|
|||||||
return user_data
|
return user_data
|
||||||
# The JWT was expired or invalid and we should request a new one.
|
# The JWT was expired or invalid and we should request a new one.
|
||||||
if self._auth_data.refresh_token is None:
|
if self._auth_data.refresh_token is None:
|
||||||
|
Logger.log("w", "There was no refresh token in the auth data.")
|
||||||
return None
|
return None
|
||||||
self._auth_data = self._auth_helpers.getAccessTokenUsingRefreshToken(self._auth_data.refresh_token)
|
self._auth_data = self._auth_helpers.getAccessTokenUsingRefreshToken(self._auth_data.refresh_token)
|
||||||
if not self._auth_data or self._auth_data.access_token is None:
|
if not self._auth_data or self._auth_data.access_token is None:
|
||||||
|
Logger.log("w", "Unable to use the refresh token to get a new access token.")
|
||||||
# The token could not be refreshed using the refresh token. We should login again.
|
# The token could not be refreshed using the refresh token. We should login again.
|
||||||
return None
|
return None
|
||||||
|
# Ensure it gets stored as otherwise we only have it in memory. The stored refresh token has been deleted
|
||||||
|
# from the server already.
|
||||||
|
self._storeAuthData(self._auth_data)
|
||||||
return self._auth_helpers.parseJWT(self._auth_data.access_token)
|
return self._auth_helpers.parseJWT(self._auth_data.access_token)
|
||||||
|
|
||||||
# Get the access token as provided by the repsonse data.
|
## Get the access token as provided by the repsonse data.
|
||||||
def getAccessToken(self) -> Optional[str]:
|
def getAccessToken(self) -> Optional[str]:
|
||||||
if not self.getUserProfile():
|
|
||||||
# We check if we can get the user profile.
|
|
||||||
# If we can't get it, that means the access token (JWT) was invalid or expired.
|
|
||||||
return None
|
|
||||||
|
|
||||||
if self._auth_data is None:
|
if self._auth_data is None:
|
||||||
|
Logger.log("d", "No auth data to retrieve the access_token from")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
return self._auth_data.access_token
|
# Check if the current access token is expired and refresh it if that is the case.
|
||||||
|
# We have a fallback on a date far in the past for currently stored auth data in cura.cfg.
|
||||||
|
received_at = datetime.strptime(self._auth_data.received_at, TOKEN_TIMESTAMP_FORMAT) \
|
||||||
|
if self._auth_data.received_at else datetime(2000, 1, 1)
|
||||||
|
expiry_date = received_at + timedelta(seconds = float(self._auth_data.expires_in or 0) - 60)
|
||||||
|
if datetime.now() > expiry_date:
|
||||||
|
self.refreshAccessToken()
|
||||||
|
|
||||||
# Try to refresh the access token. This should be used when it has expired.
|
return self._auth_data.access_token if self._auth_data else None
|
||||||
|
|
||||||
|
## Try to refresh the access token. This should be used when it has expired.
|
||||||
def refreshAccessToken(self) -> None:
|
def refreshAccessToken(self) -> None:
|
||||||
if self._auth_data is None or self._auth_data.refresh_token is None:
|
if self._auth_data is None or self._auth_data.refresh_token is None:
|
||||||
Logger.log("w", "Unable to refresh access token, since there is no refresh token.")
|
Logger.log("w", "Unable to refresh access token, since there is no refresh token.")
|
||||||
return
|
return
|
||||||
self._storeAuthData(self._auth_helpers.getAccessTokenUsingRefreshToken(self._auth_data.refresh_token))
|
response = self._auth_helpers.getAccessTokenUsingRefreshToken(self._auth_data.refresh_token)
|
||||||
self.onAuthStateChanged.emit(logged_in=True)
|
if response.success:
|
||||||
|
self._storeAuthData(response)
|
||||||
|
self.onAuthStateChanged.emit(logged_in = True)
|
||||||
|
else:
|
||||||
|
Logger.log("w", "Failed to get a new access token from the server.")
|
||||||
|
self.onAuthStateChanged.emit(logged_in = False)
|
||||||
|
|
||||||
# Delete the authentication data that we have stored locally (eg; logout)
|
## Delete the authentication data that we have stored locally (eg; logout)
|
||||||
def deleteAuthData(self) -> None:
|
def deleteAuthData(self) -> None:
|
||||||
if self._auth_data is not None:
|
if self._auth_data is not None:
|
||||||
self._storeAuthData()
|
self._storeAuthData()
|
||||||
self.onAuthStateChanged.emit(logged_in=False)
|
self.onAuthStateChanged.emit(logged_in = False)
|
||||||
|
|
||||||
# Start the flow to become authenticated. This will start a new webbrowser tap, prompting the user to login.
|
## Start the flow to become authenticated. This will start a new webbrowser tap, prompting the user to login.
|
||||||
def startAuthorizationFlow(self) -> None:
|
def startAuthorizationFlow(self) -> None:
|
||||||
Logger.log("d", "Starting new OAuth2 flow...")
|
Logger.log("d", "Starting new OAuth2 flow...")
|
||||||
|
|
||||||
@ -120,7 +157,7 @@ class AuthorizationService:
|
|||||||
"redirect_uri": self._settings.CALLBACK_URL,
|
"redirect_uri": self._settings.CALLBACK_URL,
|
||||||
"scope": self._settings.CLIENT_SCOPES,
|
"scope": self._settings.CLIENT_SCOPES,
|
||||||
"response_type": "code",
|
"response_type": "code",
|
||||||
"state": "CuraDriveIsAwesome",
|
"state": "(.Y.)",
|
||||||
"code_challenge": challenge_code,
|
"code_challenge": challenge_code,
|
||||||
"code_challenge_method": "S512"
|
"code_challenge_method": "S512"
|
||||||
})
|
})
|
||||||
@ -131,16 +168,16 @@ class AuthorizationService:
|
|||||||
# Start a local web server to receive the callback URL on.
|
# Start a local web server to receive the callback URL on.
|
||||||
self._server.start(verification_code)
|
self._server.start(verification_code)
|
||||||
|
|
||||||
# Callback method for the authentication flow.
|
## Callback method for the authentication flow.
|
||||||
def _onAuthStateChanged(self, auth_response: AuthenticationResponse) -> None:
|
def _onAuthStateChanged(self, auth_response: AuthenticationResponse) -> None:
|
||||||
if auth_response.success:
|
if auth_response.success:
|
||||||
self._storeAuthData(auth_response)
|
self._storeAuthData(auth_response)
|
||||||
self.onAuthStateChanged.emit(logged_in=True)
|
self.onAuthStateChanged.emit(logged_in = True)
|
||||||
else:
|
else:
|
||||||
self.onAuthenticationError.emit(logged_in=False, error_message=auth_response.err_message)
|
self.onAuthenticationError.emit(logged_in = False, error_message = auth_response.err_message)
|
||||||
self._server.stop() # Stop the web server at all times.
|
self._server.stop() # Stop the web server at all times.
|
||||||
|
|
||||||
# Load authentication data from preferences.
|
## Load authentication data from preferences.
|
||||||
def loadAuthDataFromPreferences(self) -> None:
|
def loadAuthDataFromPreferences(self) -> None:
|
||||||
if self._preferences is None:
|
if self._preferences is None:
|
||||||
Logger.log("e", "Unable to load authentication data, since no preference has been set!")
|
Logger.log("e", "Unable to load authentication data, since no preference has been set!")
|
||||||
@ -149,12 +186,24 @@ class AuthorizationService:
|
|||||||
preferences_data = json.loads(self._preferences.getValue(self._settings.AUTH_DATA_PREFERENCE_KEY))
|
preferences_data = json.loads(self._preferences.getValue(self._settings.AUTH_DATA_PREFERENCE_KEY))
|
||||||
if preferences_data:
|
if preferences_data:
|
||||||
self._auth_data = AuthenticationResponse(**preferences_data)
|
self._auth_data = AuthenticationResponse(**preferences_data)
|
||||||
self.onAuthStateChanged.emit(logged_in=True)
|
# Also check if we can actually get the user profile information.
|
||||||
|
user_profile = self.getUserProfile()
|
||||||
|
if user_profile is not None:
|
||||||
|
self.onAuthStateChanged.emit(logged_in = True)
|
||||||
|
else:
|
||||||
|
if self._unable_to_get_data_message is not None:
|
||||||
|
self._unable_to_get_data_message.hide()
|
||||||
|
|
||||||
|
self._unable_to_get_data_message = Message(i18n_catalog.i18nc("@info", "Unable to reach the Ultimaker account server."), title = i18n_catalog.i18nc("@info:title", "Warning"))
|
||||||
|
self._unable_to_get_data_message.addAction("retry", i18n_catalog.i18nc("@action:button", "Retry"), "[no_icon]", "[no_description]")
|
||||||
|
self._unable_to_get_data_message.actionTriggered.connect(self._onMessageActionTriggered)
|
||||||
|
self._unable_to_get_data_message.show()
|
||||||
except ValueError:
|
except ValueError:
|
||||||
Logger.logException("w", "Could not load auth data from preferences")
|
Logger.logException("w", "Could not load auth data from preferences")
|
||||||
|
|
||||||
# Store authentication data in preferences.
|
## Store authentication data in preferences.
|
||||||
def _storeAuthData(self, auth_data: Optional[AuthenticationResponse] = None) -> None:
|
def _storeAuthData(self, auth_data: Optional[AuthenticationResponse] = None) -> None:
|
||||||
|
Logger.log("d", "Attempting to store the auth data")
|
||||||
if self._preferences is None:
|
if self._preferences is None:
|
||||||
Logger.log("e", "Unable to save authentication data, since no preference has been set!")
|
Logger.log("e", "Unable to save authentication data, since no preference has been set!")
|
||||||
return
|
return
|
||||||
@ -166,3 +215,9 @@ class AuthorizationService:
|
|||||||
else:
|
else:
|
||||||
self._user_profile = None
|
self._user_profile = None
|
||||||
self._preferences.resetPreference(self._settings.AUTH_DATA_PREFERENCE_KEY)
|
self._preferences.resetPreference(self._settings.AUTH_DATA_PREFERENCE_KEY)
|
||||||
|
|
||||||
|
self.accessTokenChanged.emit()
|
||||||
|
|
||||||
|
def _onMessageActionTriggered(self, _, action):
|
||||||
|
if action == "retry":
|
||||||
|
self.loadAuthDataFromPreferences()
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
# Copyright (c) 2018 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 threading
|
import threading
|
||||||
from typing import Optional, Callable, Any, TYPE_CHECKING
|
from typing import Optional, Callable, Any, TYPE_CHECKING
|
||||||
|
|
||||||
@ -14,12 +15,15 @@ if TYPE_CHECKING:
|
|||||||
|
|
||||||
|
|
||||||
class LocalAuthorizationServer:
|
class LocalAuthorizationServer:
|
||||||
# The local LocalAuthorizationServer takes care of the oauth2 callbacks.
|
## The local LocalAuthorizationServer takes care of the oauth2 callbacks.
|
||||||
# Once the flow is completed, this server should be closed down again by calling stop()
|
# Once the flow is completed, this server should be closed down again by
|
||||||
# \param auth_helpers: An instance of the authorization helpers class.
|
# calling stop()
|
||||||
# \param auth_state_changed_callback: A callback function to be called when the authorization state changes.
|
# \param auth_helpers An instance of the authorization helpers class.
|
||||||
# \param daemon: Whether the server thread should be run in daemon mode. Note: Daemon threads are abruptly stopped
|
# \param auth_state_changed_callback A callback function to be called when
|
||||||
# at shutdown. Their resources (e.g. open files) may never be released.
|
# the authorization state changes.
|
||||||
|
# \param daemon Whether the server thread should be run in daemon mode.
|
||||||
|
# Note: Daemon threads are abruptly stopped at shutdown. Their resources
|
||||||
|
# (e.g. open files) may never be released.
|
||||||
def __init__(self, auth_helpers: "AuthorizationHelpers",
|
def __init__(self, auth_helpers: "AuthorizationHelpers",
|
||||||
auth_state_changed_callback: Callable[["AuthenticationResponse"], Any],
|
auth_state_changed_callback: Callable[["AuthenticationResponse"], Any],
|
||||||
daemon: bool) -> None:
|
daemon: bool) -> None:
|
||||||
@ -30,8 +34,8 @@ class LocalAuthorizationServer:
|
|||||||
self._auth_state_changed_callback = auth_state_changed_callback
|
self._auth_state_changed_callback = auth_state_changed_callback
|
||||||
self._daemon = daemon
|
self._daemon = daemon
|
||||||
|
|
||||||
# Starts the local web server to handle the authorization callback.
|
## Starts the local web server to handle the authorization callback.
|
||||||
# \param verification_code: The verification code part of the OAuth2 client identification.
|
# \param verification_code The verification code part of the OAuth2 client identification.
|
||||||
def start(self, verification_code: str) -> None:
|
def start(self, verification_code: str) -> None:
|
||||||
if self._web_server:
|
if self._web_server:
|
||||||
# If the server is already running (because of a previously aborted auth flow), we don't have to start it.
|
# If the server is already running (because of a previously aborted auth flow), we don't have to start it.
|
||||||
@ -54,7 +58,7 @@ class LocalAuthorizationServer:
|
|||||||
self._web_server_thread = threading.Thread(None, self._web_server.serve_forever, daemon = self._daemon)
|
self._web_server_thread = threading.Thread(None, self._web_server.serve_forever, daemon = self._daemon)
|
||||||
self._web_server_thread.start()
|
self._web_server_thread.start()
|
||||||
|
|
||||||
# Stops the web server if it was running. It also does some cleanup.
|
## Stops the web server if it was running. It also does some cleanup.
|
||||||
def stop(self) -> None:
|
def stop(self) -> None:
|
||||||
Logger.log("d", "Stopping local oauth2 web server...")
|
Logger.log("d", "Stopping local oauth2 web server...")
|
||||||
|
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
# Copyright (c) 2018 Ultimaker B.V.
|
# Copyright (c) 2019 Ultimaker B.V.
|
||||||
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
|
|
||||||
@ -7,7 +8,7 @@ class BaseModel:
|
|||||||
self.__dict__.update(kwargs)
|
self.__dict__.update(kwargs)
|
||||||
|
|
||||||
|
|
||||||
# OAuth OAuth2Settings data template.
|
## OAuth OAuth2Settings data template.
|
||||||
class OAuth2Settings(BaseModel):
|
class OAuth2Settings(BaseModel):
|
||||||
CALLBACK_PORT = None # type: Optional[int]
|
CALLBACK_PORT = None # type: Optional[int]
|
||||||
OAUTH_SERVER_URL = None # type: Optional[str]
|
OAUTH_SERVER_URL = None # type: Optional[str]
|
||||||
@ -19,14 +20,14 @@ class OAuth2Settings(BaseModel):
|
|||||||
AUTH_FAILED_REDIRECT = "https://ultimaker.com" # type: str
|
AUTH_FAILED_REDIRECT = "https://ultimaker.com" # type: str
|
||||||
|
|
||||||
|
|
||||||
# User profile data template.
|
## User profile data template.
|
||||||
class UserProfile(BaseModel):
|
class UserProfile(BaseModel):
|
||||||
user_id = None # type: Optional[str]
|
user_id = None # type: Optional[str]
|
||||||
username = None # type: Optional[str]
|
username = None # type: Optional[str]
|
||||||
profile_image_url = None # type: Optional[str]
|
profile_image_url = None # type: Optional[str]
|
||||||
|
|
||||||
|
|
||||||
# Authentication data template.
|
## Authentication data template.
|
||||||
class AuthenticationResponse(BaseModel):
|
class AuthenticationResponse(BaseModel):
|
||||||
"""Data comes from the token response with success flag and error message added."""
|
"""Data comes from the token response with success flag and error message added."""
|
||||||
success = True # type: bool
|
success = True # type: bool
|
||||||
@ -36,25 +37,25 @@ class AuthenticationResponse(BaseModel):
|
|||||||
expires_in = None # type: Optional[str]
|
expires_in = None # type: Optional[str]
|
||||||
scope = None # type: Optional[str]
|
scope = None # type: Optional[str]
|
||||||
err_message = None # type: Optional[str]
|
err_message = None # type: Optional[str]
|
||||||
|
received_at = None # type: Optional[str]
|
||||||
|
|
||||||
|
|
||||||
# Response status template.
|
## Response status template.
|
||||||
class ResponseStatus(BaseModel):
|
class ResponseStatus(BaseModel):
|
||||||
code = 200 # type: int
|
code = 200 # type: int
|
||||||
message = "" # type str
|
message = "" # type: str
|
||||||
|
|
||||||
|
|
||||||
# Response data template.
|
## Response data template.
|
||||||
class ResponseData(BaseModel):
|
class ResponseData(BaseModel):
|
||||||
status = None # type: ResponseStatus
|
status = None # type: ResponseStatus
|
||||||
data_stream = None # type: Optional[bytes]
|
data_stream = None # type: Optional[bytes]
|
||||||
redirect_uri = None # type: Optional[str]
|
redirect_uri = None # type: Optional[str]
|
||||||
content_type = "text/html" # type: str
|
content_type = "text/html" # type: str
|
||||||
|
|
||||||
|
## Possible HTTP responses.
|
||||||
# Possible HTTP responses.
|
|
||||||
HTTP_STATUS = {
|
HTTP_STATUS = {
|
||||||
"OK": ResponseStatus(code=200, message="OK"),
|
"OK": ResponseStatus(code = 200, message = "OK"),
|
||||||
"NOT_FOUND": ResponseStatus(code=404, message="NOT FOUND"),
|
"NOT_FOUND": ResponseStatus(code = 404, message = "NOT FOUND"),
|
||||||
"REDIRECT": ResponseStatus(code=302, message="REDIRECT")
|
"REDIRECT": ResponseStatus(code = 302, message = "REDIRECT")
|
||||||
}
|
}
|
||||||
|
@ -1,2 +1,2 @@
|
|||||||
# Copyright (c) 2018 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.
|
||||||
|
@ -1,94 +0,0 @@
|
|||||||
# Copyright (c) 2018 Ultimaker B.V.
|
|
||||||
# Cura is released under the terms of the LGPLv3 or higher.
|
|
||||||
|
|
||||||
from PyQt5.QtCore import QTimer
|
|
||||||
|
|
||||||
from UM.Application import Application
|
|
||||||
from UM.Qt.ListModel import ListModel
|
|
||||||
from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
|
|
||||||
from UM.Scene.SceneNode import SceneNode
|
|
||||||
from UM.Scene.Selection import Selection
|
|
||||||
from UM.i18n import i18nCatalog
|
|
||||||
from collections import defaultdict
|
|
||||||
|
|
||||||
catalog = i18nCatalog("cura")
|
|
||||||
|
|
||||||
|
|
||||||
## Keep track of all objects in the project
|
|
||||||
class ObjectsModel(ListModel):
|
|
||||||
def __init__(self):
|
|
||||||
super().__init__()
|
|
||||||
|
|
||||||
Application.getInstance().getController().getScene().sceneChanged.connect(self._updateDelayed)
|
|
||||||
Application.getInstance().getPreferences().preferenceChanged.connect(self._updateDelayed)
|
|
||||||
|
|
||||||
self._update_timer = QTimer()
|
|
||||||
self._update_timer.setInterval(100)
|
|
||||||
self._update_timer.setSingleShot(True)
|
|
||||||
self._update_timer.timeout.connect(self._update)
|
|
||||||
|
|
||||||
self._build_plate_number = -1
|
|
||||||
|
|
||||||
def setActiveBuildPlate(self, nr):
|
|
||||||
self._build_plate_number = nr
|
|
||||||
self._update()
|
|
||||||
|
|
||||||
def _updateDelayed(self, *args):
|
|
||||||
self._update_timer.start()
|
|
||||||
|
|
||||||
def _update(self, *args):
|
|
||||||
nodes = []
|
|
||||||
filter_current_build_plate = Application.getInstance().getPreferences().getValue("view/filter_current_build_plate")
|
|
||||||
active_build_plate_number = self._build_plate_number
|
|
||||||
group_nr = 1
|
|
||||||
name_count_dict = defaultdict(int)
|
|
||||||
|
|
||||||
for node in DepthFirstIterator(Application.getInstance().getController().getScene().getRoot()):
|
|
||||||
if not isinstance(node, SceneNode):
|
|
||||||
continue
|
|
||||||
if (not node.getMeshData() and not node.callDecoration("getLayerData")) and not node.callDecoration("isGroup"):
|
|
||||||
continue
|
|
||||||
if node.getParent() and node.getParent().callDecoration("isGroup"):
|
|
||||||
continue # Grouped nodes don't need resetting as their parent (the group) is resetted)
|
|
||||||
if not node.callDecoration("isSliceable") and not node.callDecoration("isGroup"):
|
|
||||||
continue
|
|
||||||
node_build_plate_number = node.callDecoration("getBuildPlateNumber")
|
|
||||||
if filter_current_build_plate and node_build_plate_number != active_build_plate_number:
|
|
||||||
continue
|
|
||||||
|
|
||||||
if not node.callDecoration("isGroup"):
|
|
||||||
name = node.getName()
|
|
||||||
|
|
||||||
else:
|
|
||||||
name = catalog.i18nc("@label", "Group #{group_nr}").format(group_nr = str(group_nr))
|
|
||||||
group_nr += 1
|
|
||||||
|
|
||||||
if hasattr(node, "isOutsideBuildArea"):
|
|
||||||
is_outside_build_area = node.isOutsideBuildArea()
|
|
||||||
else:
|
|
||||||
is_outside_build_area = False
|
|
||||||
|
|
||||||
#check if we already have an instance of the object based on name
|
|
||||||
name_count_dict[name] += 1
|
|
||||||
name_count = name_count_dict[name]
|
|
||||||
|
|
||||||
if name_count > 1:
|
|
||||||
name = "{0}({1})".format(name, name_count-1)
|
|
||||||
node.setName(name)
|
|
||||||
|
|
||||||
nodes.append({
|
|
||||||
"name": name,
|
|
||||||
"isSelected": Selection.isSelected(node),
|
|
||||||
"isOutsideBuildArea": is_outside_build_area,
|
|
||||||
"buildPlateNumber": node_build_plate_number,
|
|
||||||
"node": node
|
|
||||||
})
|
|
||||||
|
|
||||||
nodes = sorted(nodes, key=lambda n: n["name"])
|
|
||||||
self.setItems(nodes)
|
|
||||||
|
|
||||||
self.itemsChanged.emit()
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def createObjectsModel():
|
|
||||||
return ObjectsModel()
|
|
@ -29,4 +29,4 @@ class PlatformPhysicsOperation(Operation):
|
|||||||
return group
|
return group
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return "PlatformPhysicsOperation(translation = {0})".format(self._translation)
|
return "PlatformPhysicsOp.(trans.={0})".format(self._translation)
|
||||||
|
@ -17,7 +17,6 @@ from cura.Scene import ZOffsetDecorator
|
|||||||
|
|
||||||
import random # used for list shuffling
|
import random # used for list shuffling
|
||||||
|
|
||||||
|
|
||||||
class PlatformPhysics:
|
class PlatformPhysics:
|
||||||
def __init__(self, controller, volume):
|
def __init__(self, controller, volume):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
@ -40,8 +39,9 @@ class PlatformPhysics:
|
|||||||
Application.getInstance().getPreferences().addPreference("physics/automatic_drop_down", True)
|
Application.getInstance().getPreferences().addPreference("physics/automatic_drop_down", True)
|
||||||
|
|
||||||
def _onSceneChanged(self, source):
|
def _onSceneChanged(self, source):
|
||||||
if not source.getMeshData():
|
if not source.callDecoration("isSliceable"):
|
||||||
return
|
return
|
||||||
|
|
||||||
self._change_timer.start()
|
self._change_timer.start()
|
||||||
|
|
||||||
def _onChangeTimerFinished(self):
|
def _onChangeTimerFinished(self):
|
||||||
@ -76,7 +76,7 @@ class PlatformPhysics:
|
|||||||
move_vector = move_vector.set(y = -bbox.bottom + z_offset)
|
move_vector = move_vector.set(y = -bbox.bottom + z_offset)
|
||||||
|
|
||||||
# If there is no convex hull for the node, start calculating it and continue.
|
# If there is no convex hull for the node, start calculating it and continue.
|
||||||
if not node.getDecorator(ConvexHullDecorator):
|
if not node.getDecorator(ConvexHullDecorator) and not node.callDecoration("isNonPrintingMesh"):
|
||||||
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
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
# Copyright (c) 2018 Ultimaker B.V.
|
# Copyright (c) 2018 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, TYPE_CHECKING
|
from typing import Optional, TYPE_CHECKING, cast
|
||||||
|
|
||||||
|
|
||||||
from UM.Application import Application
|
from UM.Application import Application
|
||||||
from UM.Resources import Resources
|
from UM.Resources import Resources
|
||||||
@ -12,6 +13,7 @@ from UM.View.RenderBatch import RenderBatch
|
|||||||
|
|
||||||
|
|
||||||
from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
|
from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
|
||||||
|
from cura.Scene.CuraSceneNode import CuraSceneNode
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from UM.View.GL.ShaderProgram import ShaderProgram
|
from UM.View.GL.ShaderProgram import ShaderProgram
|
||||||
@ -44,9 +46,9 @@ class PreviewPass(RenderPass):
|
|||||||
|
|
||||||
self._renderer = Application.getInstance().getRenderer()
|
self._renderer = Application.getInstance().getRenderer()
|
||||||
|
|
||||||
self._shader = None #type: Optional[ShaderProgram]
|
self._shader = None # type: Optional[ShaderProgram]
|
||||||
self._non_printing_shader = None #type: Optional[ShaderProgram]
|
self._non_printing_shader = None # type: Optional[ShaderProgram]
|
||||||
self._support_mesh_shader = None #type: Optional[ShaderProgram]
|
self._support_mesh_shader = None # type: Optional[ShaderProgram]
|
||||||
self._scene = Application.getInstance().getController().getScene()
|
self._scene = Application.getInstance().getController().getScene()
|
||||||
|
|
||||||
# Set the camera to be used by this render pass
|
# Set the camera to be used by this render pass
|
||||||
@ -83,7 +85,8 @@ class PreviewPass(RenderPass):
|
|||||||
batch_support_mesh = RenderBatch(self._support_mesh_shader)
|
batch_support_mesh = RenderBatch(self._support_mesh_shader)
|
||||||
|
|
||||||
# Fill up the batch with objects that can be sliced.
|
# Fill up the batch with objects that can be sliced.
|
||||||
for node in DepthFirstIterator(self._scene.getRoot()): #type: ignore #Ignore type error because iter() should get called automatically by Python syntax.
|
for node in DepthFirstIterator(self._scene.getRoot()):
|
||||||
|
if hasattr(node, "_outside_buildarea") and not getattr(node, "_outside_buildarea"):
|
||||||
if node.callDecoration("isSliceable") and node.getMeshData() and node.isVisible():
|
if node.callDecoration("isSliceable") and node.getMeshData() and node.isVisible():
|
||||||
per_mesh_stack = node.callDecoration("getStack")
|
per_mesh_stack = node.callDecoration("getStack")
|
||||||
if node.callDecoration("isNonThumbnailVisibleMesh"):
|
if node.callDecoration("isNonThumbnailVisibleMesh"):
|
||||||
@ -93,7 +96,7 @@ class PreviewPass(RenderPass):
|
|||||||
# Support mesh
|
# Support mesh
|
||||||
uniforms = {}
|
uniforms = {}
|
||||||
shade_factor = 0.6
|
shade_factor = 0.6
|
||||||
diffuse_color = node.getDiffuseColor()
|
diffuse_color = cast(CuraSceneNode, node).getDiffuseColor()
|
||||||
diffuse_color2 = [
|
diffuse_color2 = [
|
||||||
diffuse_color[0] * shade_factor,
|
diffuse_color[0] * shade_factor,
|
||||||
diffuse_color[1] * shade_factor,
|
diffuse_color[1] * shade_factor,
|
||||||
@ -105,7 +108,7 @@ class PreviewPass(RenderPass):
|
|||||||
else:
|
else:
|
||||||
# Normal scene node
|
# Normal scene node
|
||||||
uniforms = {}
|
uniforms = {}
|
||||||
uniforms["diffuse_color"] = prettier_color(node.getDiffuseColor())
|
uniforms["diffuse_color"] = prettier_color(cast(CuraSceneNode, node).getDiffuseColor())
|
||||||
batch.addItem(node.getWorldTransformation(), node.getMeshData(), uniforms = uniforms)
|
batch.addItem(node.getWorldTransformation(), node.getMeshData(), uniforms = uniforms)
|
||||||
|
|
||||||
self.bind()
|
self.bind()
|
||||||
|
78
cura/PrinterOutput/FirmwareUpdater.py
Normal file
78
cura/PrinterOutput/FirmwareUpdater.py
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
# Copyright (c) 2018 Ultimaker B.V.
|
||||||
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
|
from PyQt5.QtCore import QObject, QUrl, pyqtSignal, pyqtProperty
|
||||||
|
|
||||||
|
from enum import IntEnum
|
||||||
|
from threading import Thread
|
||||||
|
from typing import Union
|
||||||
|
|
||||||
|
MYPY = False
|
||||||
|
if MYPY:
|
||||||
|
from cura.PrinterOutput.PrinterOutputDevice import PrinterOutputDevice
|
||||||
|
|
||||||
|
class FirmwareUpdater(QObject):
|
||||||
|
firmwareProgressChanged = pyqtSignal()
|
||||||
|
firmwareUpdateStateChanged = pyqtSignal()
|
||||||
|
|
||||||
|
def __init__(self, output_device: "PrinterOutputDevice") -> None:
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
self._output_device = output_device
|
||||||
|
|
||||||
|
self._update_firmware_thread = Thread(target=self._updateFirmware, daemon=True)
|
||||||
|
|
||||||
|
self._firmware_file = ""
|
||||||
|
self._firmware_progress = 0
|
||||||
|
self._firmware_update_state = FirmwareUpdateState.idle
|
||||||
|
|
||||||
|
def updateFirmware(self, firmware_file: Union[str, QUrl]) -> None:
|
||||||
|
# the file path could be url-encoded.
|
||||||
|
if firmware_file.startswith("file://"):
|
||||||
|
self._firmware_file = QUrl(firmware_file).toLocalFile()
|
||||||
|
else:
|
||||||
|
self._firmware_file = firmware_file
|
||||||
|
|
||||||
|
self._setFirmwareUpdateState(FirmwareUpdateState.updating)
|
||||||
|
|
||||||
|
self._update_firmware_thread.start()
|
||||||
|
|
||||||
|
def _updateFirmware(self) -> None:
|
||||||
|
raise NotImplementedError("_updateFirmware needs to be implemented")
|
||||||
|
|
||||||
|
## Cleanup after a succesful update
|
||||||
|
def _cleanupAfterUpdate(self) -> None:
|
||||||
|
# Clean up for next attempt.
|
||||||
|
self._update_firmware_thread = Thread(target=self._updateFirmware, daemon=True)
|
||||||
|
self._firmware_file = ""
|
||||||
|
self._onFirmwareProgress(100)
|
||||||
|
self._setFirmwareUpdateState(FirmwareUpdateState.completed)
|
||||||
|
|
||||||
|
@pyqtProperty(int, notify = firmwareProgressChanged)
|
||||||
|
def firmwareProgress(self) -> int:
|
||||||
|
return self._firmware_progress
|
||||||
|
|
||||||
|
@pyqtProperty(int, notify=firmwareUpdateStateChanged)
|
||||||
|
def firmwareUpdateState(self) -> "FirmwareUpdateState":
|
||||||
|
return self._firmware_update_state
|
||||||
|
|
||||||
|
def _setFirmwareUpdateState(self, state: "FirmwareUpdateState") -> None:
|
||||||
|
if self._firmware_update_state != state:
|
||||||
|
self._firmware_update_state = state
|
||||||
|
self.firmwareUpdateStateChanged.emit()
|
||||||
|
|
||||||
|
# Callback function for firmware update progress.
|
||||||
|
def _onFirmwareProgress(self, progress: int, max_progress: int = 100) -> None:
|
||||||
|
self._firmware_progress = int(progress * 100 / max_progress) # Convert to scale of 0-100
|
||||||
|
self.firmwareProgressChanged.emit()
|
||||||
|
|
||||||
|
|
||||||
|
class FirmwareUpdateState(IntEnum):
|
||||||
|
idle = 0
|
||||||
|
updating = 1
|
||||||
|
completed = 2
|
||||||
|
unknown_error = 3
|
||||||
|
communication_error = 4
|
||||||
|
io_error = 5
|
||||||
|
firmware_not_found_error = 6
|
||||||
|
|
@ -1,35 +1,37 @@
|
|||||||
# Copyright (c) 2018 Ultimaker B.V.
|
# Copyright (c) 2018 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 TYPE_CHECKING
|
from typing import TYPE_CHECKING, Set, Union, Optional
|
||||||
|
|
||||||
from cura.PrinterOutput.PrinterOutputController import PrinterOutputController
|
|
||||||
from PyQt5.QtCore import QTimer
|
from PyQt5.QtCore import QTimer
|
||||||
|
|
||||||
|
from .PrinterOutputController import PrinterOutputController
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from cura.PrinterOutput.PrintJobOutputModel import PrintJobOutputModel
|
from .Models.PrintJobOutputModel import PrintJobOutputModel
|
||||||
from cura.PrinterOutput.PrinterOutputModel import PrinterOutputModel
|
from .Models.PrinterOutputModel import PrinterOutputModel
|
||||||
from cura.PrinterOutput.ExtruderOutputModel import ExtruderOutputModel
|
from .PrinterOutputDevice import PrinterOutputDevice
|
||||||
|
from .Models.ExtruderOutputModel import ExtruderOutputModel
|
||||||
|
|
||||||
|
|
||||||
class GenericOutputController(PrinterOutputController):
|
class GenericOutputController(PrinterOutputController):
|
||||||
def __init__(self, output_device):
|
def __init__(self, output_device: "PrinterOutputDevice") -> None:
|
||||||
super().__init__(output_device)
|
super().__init__(output_device)
|
||||||
|
|
||||||
self._preheat_bed_timer = QTimer()
|
self._preheat_bed_timer = QTimer()
|
||||||
self._preheat_bed_timer.setSingleShot(True)
|
self._preheat_bed_timer.setSingleShot(True)
|
||||||
self._preheat_bed_timer.timeout.connect(self._onPreheatBedTimerFinished)
|
self._preheat_bed_timer.timeout.connect(self._onPreheatBedTimerFinished)
|
||||||
self._preheat_printer = None
|
self._preheat_printer = None # type: Optional[PrinterOutputModel]
|
||||||
|
|
||||||
self._preheat_hotends_timer = QTimer()
|
self._preheat_hotends_timer = QTimer()
|
||||||
self._preheat_hotends_timer.setSingleShot(True)
|
self._preheat_hotends_timer.setSingleShot(True)
|
||||||
self._preheat_hotends_timer.timeout.connect(self._onPreheatHotendsTimerFinished)
|
self._preheat_hotends_timer.timeout.connect(self._onPreheatHotendsTimerFinished)
|
||||||
self._preheat_hotends = set()
|
self._preheat_hotends = set() # type: Set[ExtruderOutputModel]
|
||||||
|
|
||||||
self._output_device.printersChanged.connect(self._onPrintersChanged)
|
self._output_device.printersChanged.connect(self._onPrintersChanged)
|
||||||
self._active_printer = None
|
self._active_printer = None # type: Optional[PrinterOutputModel]
|
||||||
|
|
||||||
def _onPrintersChanged(self):
|
def _onPrintersChanged(self) -> None:
|
||||||
if self._active_printer:
|
if self._active_printer:
|
||||||
self._active_printer.stateChanged.disconnect(self._onPrinterStateChanged)
|
self._active_printer.stateChanged.disconnect(self._onPrinterStateChanged)
|
||||||
self._active_printer.targetBedTemperatureChanged.disconnect(self._onTargetBedTemperatureChanged)
|
self._active_printer.targetBedTemperatureChanged.disconnect(self._onTargetBedTemperatureChanged)
|
||||||
@ -43,10 +45,11 @@ class GenericOutputController(PrinterOutputController):
|
|||||||
for extruder in self._active_printer.extruders:
|
for extruder in self._active_printer.extruders:
|
||||||
extruder.targetHotendTemperatureChanged.connect(self._onTargetHotendTemperatureChanged)
|
extruder.targetHotendTemperatureChanged.connect(self._onTargetHotendTemperatureChanged)
|
||||||
|
|
||||||
def _onPrinterStateChanged(self):
|
def _onPrinterStateChanged(self) -> None:
|
||||||
if self._active_printer.state != "idle":
|
if self._active_printer and self._active_printer.state != "idle":
|
||||||
if self._preheat_bed_timer.isActive():
|
if self._preheat_bed_timer.isActive():
|
||||||
self._preheat_bed_timer.stop()
|
self._preheat_bed_timer.stop()
|
||||||
|
if self._preheat_printer:
|
||||||
self._preheat_printer.updateIsPreheating(False)
|
self._preheat_printer.updateIsPreheating(False)
|
||||||
if self._preheat_hotends_timer.isActive():
|
if self._preheat_hotends_timer.isActive():
|
||||||
self._preheat_hotends_timer.stop()
|
self._preheat_hotends_timer.stop()
|
||||||
@ -54,21 +57,21 @@ class GenericOutputController(PrinterOutputController):
|
|||||||
extruder.updateIsPreheating(False)
|
extruder.updateIsPreheating(False)
|
||||||
self._preheat_hotends = set()
|
self._preheat_hotends = set()
|
||||||
|
|
||||||
def moveHead(self, printer: "PrinterOutputModel", x, y, z, speed):
|
def moveHead(self, printer: "PrinterOutputModel", x, y, z, speed) -> None:
|
||||||
self._output_device.sendCommand("G91")
|
self._output_device.sendCommand("G91")
|
||||||
self._output_device.sendCommand("G0 X%s Y%s Z%s F%s" % (x, y, z, speed))
|
self._output_device.sendCommand("G0 X%s Y%s Z%s F%s" % (x, y, z, speed))
|
||||||
self._output_device.sendCommand("G90")
|
self._output_device.sendCommand("G90")
|
||||||
|
|
||||||
def homeHead(self, printer):
|
def homeHead(self, printer: "PrinterOutputModel") -> None:
|
||||||
self._output_device.sendCommand("G28 X Y")
|
self._output_device.sendCommand("G28 X Y")
|
||||||
|
|
||||||
def homeBed(self, printer):
|
def homeBed(self, printer: "PrinterOutputModel") -> None:
|
||||||
self._output_device.sendCommand("G28 Z")
|
self._output_device.sendCommand("G28 Z")
|
||||||
|
|
||||||
def sendRawCommand(self, printer: "PrinterOutputModel", command: str):
|
def sendRawCommand(self, printer: "PrinterOutputModel", command: str) -> None:
|
||||||
self._output_device.sendCommand(command.upper()) #Most printers only understand uppercase g-code commands.
|
self._output_device.sendCommand(command.upper()) #Most printers only understand uppercase g-code commands.
|
||||||
|
|
||||||
def setJobState(self, job: "PrintJobOutputModel", state: str):
|
def setJobState(self, job: "PrintJobOutputModel", state: str) -> None:
|
||||||
if state == "pause":
|
if state == "pause":
|
||||||
self._output_device.pausePrint()
|
self._output_device.pausePrint()
|
||||||
job.updateState("paused")
|
job.updateState("paused")
|
||||||
@ -79,42 +82,46 @@ class GenericOutputController(PrinterOutputController):
|
|||||||
self._output_device.cancelPrint()
|
self._output_device.cancelPrint()
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def setTargetBedTemperature(self, printer: "PrinterOutputModel", temperature: int):
|
def setTargetBedTemperature(self, printer: "PrinterOutputModel", temperature: float) -> None:
|
||||||
self._output_device.sendCommand("M140 S%s" % temperature)
|
self._output_device.sendCommand("M140 S%s" % round(temperature)) # The API doesn't allow floating point.
|
||||||
|
|
||||||
def _onTargetBedTemperatureChanged(self):
|
def _onTargetBedTemperatureChanged(self) -> None:
|
||||||
if self._preheat_bed_timer.isActive() and self._preheat_printer.targetBedTemperature == 0:
|
if self._preheat_bed_timer.isActive() and self._preheat_printer and self._preheat_printer.targetBedTemperature == 0:
|
||||||
self._preheat_bed_timer.stop()
|
self._preheat_bed_timer.stop()
|
||||||
self._preheat_printer.updateIsPreheating(False)
|
self._preheat_printer.updateIsPreheating(False)
|
||||||
|
|
||||||
def preheatBed(self, printer: "PrinterOutputModel", temperature, duration):
|
def preheatBed(self, printer: "PrinterOutputModel", temperature, duration) -> None:
|
||||||
try:
|
try:
|
||||||
temperature = round(temperature) # The API doesn't allow floating point.
|
temperature = round(temperature) # The API doesn't allow floating point.
|
||||||
duration = round(duration)
|
duration = round(duration)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
return # Got invalid values, can't pre-heat.
|
return # Got invalid values, can't pre-heat.
|
||||||
|
|
||||||
self.setTargetBedTemperature(printer, temperature=temperature)
|
self.setTargetBedTemperature(printer, temperature = temperature)
|
||||||
self._preheat_bed_timer.setInterval(duration * 1000)
|
self._preheat_bed_timer.setInterval(duration * 1000)
|
||||||
self._preheat_bed_timer.start()
|
self._preheat_bed_timer.start()
|
||||||
self._preheat_printer = printer
|
self._preheat_printer = printer
|
||||||
printer.updateIsPreheating(True)
|
printer.updateIsPreheating(True)
|
||||||
|
|
||||||
def cancelPreheatBed(self, printer: "PrinterOutputModel"):
|
def cancelPreheatBed(self, printer: "PrinterOutputModel") -> None:
|
||||||
self.setTargetBedTemperature(printer, temperature=0)
|
self.setTargetBedTemperature(printer, temperature = 0)
|
||||||
self._preheat_bed_timer.stop()
|
self._preheat_bed_timer.stop()
|
||||||
printer.updateIsPreheating(False)
|
printer.updateIsPreheating(False)
|
||||||
|
|
||||||
def _onPreheatBedTimerFinished(self):
|
def _onPreheatBedTimerFinished(self) -> None:
|
||||||
|
if not self._preheat_printer:
|
||||||
|
return
|
||||||
self.setTargetBedTemperature(self._preheat_printer, 0)
|
self.setTargetBedTemperature(self._preheat_printer, 0)
|
||||||
self._preheat_printer.updateIsPreheating(False)
|
self._preheat_printer.updateIsPreheating(False)
|
||||||
|
|
||||||
def setTargetHotendTemperature(self, printer: "PrinterOutputModel", position: int, temperature: int):
|
def setTargetHotendTemperature(self, printer: "PrinterOutputModel", position: int, temperature: Union[int, float]) -> None:
|
||||||
self._output_device.sendCommand("M104 S%s T%s" % (temperature, position))
|
self._output_device.sendCommand("M104 S%s T%s" % (temperature, position))
|
||||||
|
|
||||||
def _onTargetHotendTemperatureChanged(self):
|
def _onTargetHotendTemperatureChanged(self) -> None:
|
||||||
if not self._preheat_hotends_timer.isActive():
|
if not self._preheat_hotends_timer.isActive():
|
||||||
return
|
return
|
||||||
|
if not self._active_printer:
|
||||||
|
return
|
||||||
|
|
||||||
for extruder in self._active_printer.extruders:
|
for extruder in self._active_printer.extruders:
|
||||||
if extruder in self._preheat_hotends and extruder.targetHotendTemperature == 0:
|
if extruder in self._preheat_hotends and extruder.targetHotendTemperature == 0:
|
||||||
@ -123,7 +130,7 @@ class GenericOutputController(PrinterOutputController):
|
|||||||
if not self._preheat_hotends:
|
if not self._preheat_hotends:
|
||||||
self._preheat_hotends_timer.stop()
|
self._preheat_hotends_timer.stop()
|
||||||
|
|
||||||
def preheatHotend(self, extruder: "ExtruderOutputModel", temperature, duration):
|
def preheatHotend(self, extruder: "ExtruderOutputModel", temperature, duration) -> None:
|
||||||
position = extruder.getPosition()
|
position = extruder.getPosition()
|
||||||
number_of_extruders = len(extruder.getPrinter().extruders)
|
number_of_extruders = len(extruder.getPrinter().extruders)
|
||||||
if position >= number_of_extruders:
|
if position >= number_of_extruders:
|
||||||
@ -141,7 +148,7 @@ class GenericOutputController(PrinterOutputController):
|
|||||||
self._preheat_hotends.add(extruder)
|
self._preheat_hotends.add(extruder)
|
||||||
extruder.updateIsPreheating(True)
|
extruder.updateIsPreheating(True)
|
||||||
|
|
||||||
def cancelPreheatHotend(self, extruder: "ExtruderOutputModel"):
|
def cancelPreheatHotend(self, extruder: "ExtruderOutputModel") -> None:
|
||||||
self.setTargetHotendTemperature(extruder.getPrinter(), extruder.getPosition(), temperature=0)
|
self.setTargetHotendTemperature(extruder.getPrinter(), extruder.getPosition(), temperature=0)
|
||||||
if extruder in self._preheat_hotends:
|
if extruder in self._preheat_hotends:
|
||||||
extruder.updateIsPreheating(False)
|
extruder.updateIsPreheating(False)
|
||||||
@ -149,14 +156,14 @@ class GenericOutputController(PrinterOutputController):
|
|||||||
if not self._preheat_hotends and self._preheat_hotends_timer.isActive():
|
if not self._preheat_hotends and self._preheat_hotends_timer.isActive():
|
||||||
self._preheat_hotends_timer.stop()
|
self._preheat_hotends_timer.stop()
|
||||||
|
|
||||||
def _onPreheatHotendsTimerFinished(self):
|
def _onPreheatHotendsTimerFinished(self) -> None:
|
||||||
for extruder in self._preheat_hotends:
|
for extruder in self._preheat_hotends:
|
||||||
self.setTargetHotendTemperature(extruder.getPrinter(), extruder.getPosition(), 0)
|
self.setTargetHotendTemperature(extruder.getPrinter(), extruder.getPosition(), 0)
|
||||||
self._preheat_hotends = set()
|
self._preheat_hotends = set()
|
||||||
|
|
||||||
# Cancel any ongoing preheating timers, without setting back the temperature to 0
|
# Cancel any ongoing preheating timers, without setting back the temperature to 0
|
||||||
# This can be used eg at the start of a print
|
# This can be used eg at the start of a print
|
||||||
def stopPreheatTimers(self):
|
def stopPreheatTimers(self) -> None:
|
||||||
if self._preheat_hotends_timer.isActive():
|
if self._preheat_hotends_timer.isActive():
|
||||||
for extruder in self._preheat_hotends:
|
for extruder in self._preheat_hotends:
|
||||||
extruder.updateIsPreheating(False)
|
extruder.updateIsPreheating(False)
|
||||||
@ -165,5 +172,6 @@ class GenericOutputController(PrinterOutputController):
|
|||||||
self._preheat_hotends_timer.stop()
|
self._preheat_hotends_timer.stop()
|
||||||
|
|
||||||
if self._preheat_bed_timer.isActive():
|
if self._preheat_bed_timer.isActive():
|
||||||
|
if self._preheat_printer:
|
||||||
self._preheat_printer.updateIsPreheating(False)
|
self._preheat_printer.updateIsPreheating(False)
|
||||||
self._preheat_bed_timer.stop()
|
self._preheat_bed_timer.stop()
|
||||||
|
@ -1,34 +0,0 @@
|
|||||||
# Copyright (c) 2017 Ultimaker B.V.
|
|
||||||
# Cura is released under the terms of the LGPLv3 or higher.
|
|
||||||
|
|
||||||
from PyQt5.QtCore import pyqtSignal, pyqtProperty, QObject, QVariant, pyqtSlot
|
|
||||||
|
|
||||||
|
|
||||||
class MaterialOutputModel(QObject):
|
|
||||||
def __init__(self, guid, type, color, brand, name, parent = None):
|
|
||||||
super().__init__(parent)
|
|
||||||
self._guid = guid
|
|
||||||
self._type = type
|
|
||||||
self._color = color
|
|
||||||
self._brand = brand
|
|
||||||
self._name = name
|
|
||||||
|
|
||||||
@pyqtProperty(str, constant = True)
|
|
||||||
def guid(self):
|
|
||||||
return self._guid
|
|
||||||
|
|
||||||
@pyqtProperty(str, constant=True)
|
|
||||||
def type(self):
|
|
||||||
return self._type
|
|
||||||
|
|
||||||
@pyqtProperty(str, constant=True)
|
|
||||||
def brand(self):
|
|
||||||
return self._brand
|
|
||||||
|
|
||||||
@pyqtProperty(str, constant=True)
|
|
||||||
def color(self):
|
|
||||||
return self._color
|
|
||||||
|
|
||||||
@pyqtProperty(str, constant=True)
|
|
||||||
def name(self):
|
|
||||||
return self._name
|
|
@ -4,7 +4,7 @@ from typing import Optional
|
|||||||
|
|
||||||
from PyQt5.QtCore import pyqtProperty, QObject, pyqtSignal
|
from PyQt5.QtCore import pyqtProperty, QObject, pyqtSignal
|
||||||
|
|
||||||
from cura.PrinterOutput.MaterialOutputModel import MaterialOutputModel
|
from .MaterialOutputModel import MaterialOutputModel
|
||||||
|
|
||||||
|
|
||||||
class ExtruderConfigurationModel(QObject):
|
class ExtruderConfigurationModel(QObject):
|
||||||
@ -62,7 +62,24 @@ class ExtruderConfigurationModel(QObject):
|
|||||||
return " ".join(message_chunks)
|
return " ".join(message_chunks)
|
||||||
|
|
||||||
def __eq__(self, other) -> bool:
|
def __eq__(self, other) -> bool:
|
||||||
return hash(self) == hash(other)
|
if not isinstance(other, ExtruderConfigurationModel):
|
||||||
|
return False
|
||||||
|
|
||||||
|
if self._position != other.position:
|
||||||
|
return False
|
||||||
|
# Empty materials should be ignored for comparison
|
||||||
|
if self.activeMaterial is not None and other.activeMaterial is not None:
|
||||||
|
if self.activeMaterial.guid != other.activeMaterial.guid:
|
||||||
|
if self.activeMaterial.guid != "" and other.activeMaterial.guid != "":
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
# At this point there is no material, so it doesn't matter what the hotend is.
|
||||||
|
return True
|
||||||
|
|
||||||
|
if self.hotendID != other.hotendID:
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
# Calculating a hash function using the position of the extruder, the material GUID and the hotend id to check if is
|
# Calculating a hash function using the position of the extruder, the material GUID and the hotend id to check if is
|
||||||
# unique within a set
|
# unique within a set
|
@ -1,14 +1,15 @@
|
|||||||
# Copyright (c) 2018 Ultimaker B.V.
|
# Copyright (c) 2018 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 PyQt5.QtCore import pyqtSignal, pyqtProperty, QObject, pyqtSlot
|
|
||||||
from cura.PrinterOutput.ExtruderConfigurationModel import ExtruderConfigurationModel
|
|
||||||
|
|
||||||
from typing import Optional, TYPE_CHECKING
|
from typing import Optional, TYPE_CHECKING
|
||||||
|
|
||||||
|
from PyQt5.QtCore import pyqtSignal, pyqtProperty, QObject, pyqtSlot
|
||||||
|
|
||||||
|
from .ExtruderConfigurationModel import ExtruderConfigurationModel
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from cura.PrinterOutput.PrinterOutputModel import PrinterOutputModel
|
from .MaterialOutputModel import MaterialOutputModel
|
||||||
from cura.PrinterOutput.MaterialOutputModel import MaterialOutputModel
|
from .PrinterOutputModel import PrinterOutputModel
|
||||||
|
|
||||||
|
|
||||||
class ExtruderOutputModel(QObject):
|
class ExtruderOutputModel(QObject):
|
36
cura/PrinterOutput/Models/MaterialOutputModel.py
Normal file
36
cura/PrinterOutput/Models/MaterialOutputModel.py
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
# Copyright (c) 2017 Ultimaker B.V.
|
||||||
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from PyQt5.QtCore import pyqtProperty, QObject
|
||||||
|
|
||||||
|
|
||||||
|
class MaterialOutputModel(QObject):
|
||||||
|
def __init__(self, guid: Optional[str], type: str, color: str, brand: str, name: str, parent = None) -> None:
|
||||||
|
super().__init__(parent)
|
||||||
|
self._guid = guid
|
||||||
|
self._type = type
|
||||||
|
self._color = color
|
||||||
|
self._brand = brand
|
||||||
|
self._name = name
|
||||||
|
|
||||||
|
@pyqtProperty(str, constant = True)
|
||||||
|
def guid(self) -> str:
|
||||||
|
return self._guid if self._guid else ""
|
||||||
|
|
||||||
|
@pyqtProperty(str, constant = True)
|
||||||
|
def type(self) -> str:
|
||||||
|
return self._type
|
||||||
|
|
||||||
|
@pyqtProperty(str, constant = True)
|
||||||
|
def brand(self) -> str:
|
||||||
|
return self._brand
|
||||||
|
|
||||||
|
@pyqtProperty(str, constant = True)
|
||||||
|
def color(self) -> str:
|
||||||
|
return self._color
|
||||||
|
|
||||||
|
@pyqtProperty(str, constant = True)
|
||||||
|
def name(self) -> str:
|
||||||
|
return self._name
|
171
cura/PrinterOutput/Models/PrintJobOutputModel.py
Normal file
171
cura/PrinterOutput/Models/PrintJobOutputModel.py
Normal file
@ -0,0 +1,171 @@
|
|||||||
|
# Copyright (c) 2018 Ultimaker B.V.
|
||||||
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
|
from typing import Optional, TYPE_CHECKING, List
|
||||||
|
|
||||||
|
from PyQt5.QtCore import pyqtSignal, pyqtProperty, QObject, pyqtSlot, QUrl
|
||||||
|
from PyQt5.QtGui import QImage
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from cura.PrinterOutput.PrinterOutputController import PrinterOutputController
|
||||||
|
from cura.PrinterOutput.Models.PrinterOutputModel import PrinterOutputModel
|
||||||
|
from cura.PrinterOutput.Models.PrinterConfigurationModel import PrinterConfigurationModel
|
||||||
|
|
||||||
|
|
||||||
|
class PrintJobOutputModel(QObject):
|
||||||
|
stateChanged = pyqtSignal()
|
||||||
|
timeTotalChanged = pyqtSignal()
|
||||||
|
timeElapsedChanged = pyqtSignal()
|
||||||
|
nameChanged = pyqtSignal()
|
||||||
|
keyChanged = pyqtSignal()
|
||||||
|
assignedPrinterChanged = pyqtSignal()
|
||||||
|
ownerChanged = pyqtSignal()
|
||||||
|
configurationChanged = pyqtSignal()
|
||||||
|
previewImageChanged = pyqtSignal()
|
||||||
|
compatibleMachineFamiliesChanged = pyqtSignal()
|
||||||
|
|
||||||
|
def __init__(self, output_controller: "PrinterOutputController", key: str = "", name: str = "", parent = None) -> None:
|
||||||
|
super().__init__(parent)
|
||||||
|
self._output_controller = output_controller
|
||||||
|
self._state = ""
|
||||||
|
self._time_total = 0
|
||||||
|
self._time_elapsed = 0
|
||||||
|
self._name = name # Human readable name
|
||||||
|
self._key = key # Unique identifier
|
||||||
|
self._assigned_printer = None # type: Optional[PrinterOutputModel]
|
||||||
|
self._owner = "" # Who started/owns the print job?
|
||||||
|
|
||||||
|
self._configuration = None # type: Optional[PrinterConfigurationModel]
|
||||||
|
self._compatible_machine_families = [] # type: List[str]
|
||||||
|
self._preview_image_id = 0
|
||||||
|
|
||||||
|
self._preview_image = None # type: Optional[QImage]
|
||||||
|
|
||||||
|
@pyqtProperty("QStringList", notify=compatibleMachineFamiliesChanged)
|
||||||
|
def compatibleMachineFamilies(self):
|
||||||
|
# Hack; Some versions of cluster will return a family more than once...
|
||||||
|
return list(set(self._compatible_machine_families))
|
||||||
|
|
||||||
|
def setCompatibleMachineFamilies(self, compatible_machine_families: List[str]) -> None:
|
||||||
|
if self._compatible_machine_families != compatible_machine_families:
|
||||||
|
self._compatible_machine_families = compatible_machine_families
|
||||||
|
self.compatibleMachineFamiliesChanged.emit()
|
||||||
|
|
||||||
|
@pyqtProperty(QUrl, notify=previewImageChanged)
|
||||||
|
def previewImageUrl(self):
|
||||||
|
self._preview_image_id += 1
|
||||||
|
# There is an image provider that is called "print_job_preview". In order to ensure that the image qml object, that
|
||||||
|
# requires a QUrl to function, updates correctly we add an increasing number. This causes to see the QUrl
|
||||||
|
# as new (instead of relying on cached version and thus forces an update.
|
||||||
|
temp = "image://print_job_preview/" + str(self._preview_image_id) + "/" + self._key
|
||||||
|
return QUrl(temp, QUrl.TolerantMode)
|
||||||
|
|
||||||
|
def getPreviewImage(self) -> Optional[QImage]:
|
||||||
|
return self._preview_image
|
||||||
|
|
||||||
|
def updatePreviewImage(self, preview_image: Optional[QImage]) -> None:
|
||||||
|
if self._preview_image != preview_image:
|
||||||
|
self._preview_image = preview_image
|
||||||
|
self.previewImageChanged.emit()
|
||||||
|
|
||||||
|
@pyqtProperty(QObject, notify=configurationChanged)
|
||||||
|
def configuration(self) -> Optional["PrinterConfigurationModel"]:
|
||||||
|
return self._configuration
|
||||||
|
|
||||||
|
def updateConfiguration(self, configuration: Optional["PrinterConfigurationModel"]) -> None:
|
||||||
|
if self._configuration != configuration:
|
||||||
|
self._configuration = configuration
|
||||||
|
self.configurationChanged.emit()
|
||||||
|
|
||||||
|
@pyqtProperty(str, notify=ownerChanged)
|
||||||
|
def owner(self):
|
||||||
|
return self._owner
|
||||||
|
|
||||||
|
def updateOwner(self, owner):
|
||||||
|
if self._owner != owner:
|
||||||
|
self._owner = owner
|
||||||
|
self.ownerChanged.emit()
|
||||||
|
|
||||||
|
@pyqtProperty(QObject, notify=assignedPrinterChanged)
|
||||||
|
def assignedPrinter(self):
|
||||||
|
return self._assigned_printer
|
||||||
|
|
||||||
|
def updateAssignedPrinter(self, assigned_printer: Optional["PrinterOutputModel"]) -> None:
|
||||||
|
if self._assigned_printer != assigned_printer:
|
||||||
|
old_printer = self._assigned_printer
|
||||||
|
self._assigned_printer = assigned_printer
|
||||||
|
if old_printer is not None:
|
||||||
|
# If the previously assigned printer is set, this job is moved away from it.
|
||||||
|
old_printer.updateActivePrintJob(None)
|
||||||
|
self.assignedPrinterChanged.emit()
|
||||||
|
|
||||||
|
@pyqtProperty(str, notify=keyChanged)
|
||||||
|
def key(self):
|
||||||
|
return self._key
|
||||||
|
|
||||||
|
def updateKey(self, key: str):
|
||||||
|
if self._key != key:
|
||||||
|
self._key = key
|
||||||
|
self.keyChanged.emit()
|
||||||
|
|
||||||
|
@pyqtProperty(str, notify = nameChanged)
|
||||||
|
def name(self):
|
||||||
|
return self._name
|
||||||
|
|
||||||
|
def updateName(self, name: str):
|
||||||
|
if self._name != name:
|
||||||
|
self._name = name
|
||||||
|
self.nameChanged.emit()
|
||||||
|
|
||||||
|
@pyqtProperty(int, notify = timeTotalChanged)
|
||||||
|
def timeTotal(self) -> int:
|
||||||
|
return self._time_total
|
||||||
|
|
||||||
|
@pyqtProperty(int, notify = timeElapsedChanged)
|
||||||
|
def timeElapsed(self) -> int:
|
||||||
|
return self._time_elapsed
|
||||||
|
|
||||||
|
@pyqtProperty(int, notify = timeElapsedChanged)
|
||||||
|
def timeRemaining(self) -> int:
|
||||||
|
# Never get a negative time remaining
|
||||||
|
return max(self.timeTotal - self.timeElapsed, 0)
|
||||||
|
|
||||||
|
@pyqtProperty(float, notify = timeElapsedChanged)
|
||||||
|
def progress(self) -> float:
|
||||||
|
result = float(self.timeElapsed) / max(self.timeTotal, 1.0) # Prevent a division by zero exception.
|
||||||
|
return min(result, 1.0) # Never get a progress past 1.0
|
||||||
|
|
||||||
|
@pyqtProperty(str, notify=stateChanged)
|
||||||
|
def state(self) -> str:
|
||||||
|
return self._state
|
||||||
|
|
||||||
|
@pyqtProperty(bool, notify=stateChanged)
|
||||||
|
def isActive(self) -> bool:
|
||||||
|
inactive_states = [
|
||||||
|
"pausing",
|
||||||
|
"paused",
|
||||||
|
"resuming",
|
||||||
|
"wait_cleanup"
|
||||||
|
]
|
||||||
|
if self.state in inactive_states and self.timeRemaining > 0:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
def updateTimeTotal(self, new_time_total):
|
||||||
|
if self._time_total != new_time_total:
|
||||||
|
self._time_total = new_time_total
|
||||||
|
self.timeTotalChanged.emit()
|
||||||
|
|
||||||
|
def updateTimeElapsed(self, new_time_elapsed):
|
||||||
|
if self._time_elapsed != new_time_elapsed:
|
||||||
|
self._time_elapsed = new_time_elapsed
|
||||||
|
self.timeElapsedChanged.emit()
|
||||||
|
|
||||||
|
def updateState(self, new_state):
|
||||||
|
if self._state != new_state:
|
||||||
|
self._state = new_state
|
||||||
|
self.stateChanged.emit()
|
||||||
|
|
||||||
|
@pyqtSlot(str)
|
||||||
|
def setState(self, state):
|
||||||
|
self._output_controller.setJobState(self, state)
|
@ -6,10 +6,10 @@ from typing import List
|
|||||||
|
|
||||||
MYPY = False
|
MYPY = False
|
||||||
if MYPY:
|
if MYPY:
|
||||||
from cura.PrinterOutput.ExtruderConfigurationModel import ExtruderConfigurationModel
|
from cura.PrinterOutput.Models.ExtruderConfigurationModel import ExtruderConfigurationModel
|
||||||
|
|
||||||
|
|
||||||
class ConfigurationModel(QObject):
|
class PrinterConfigurationModel(QObject):
|
||||||
|
|
||||||
configurationChanged = pyqtSignal()
|
configurationChanged = pyqtSignal()
|
||||||
|
|
||||||
@ -19,14 +19,14 @@ class ConfigurationModel(QObject):
|
|||||||
self._extruder_configurations = [] # type: List[ExtruderConfigurationModel]
|
self._extruder_configurations = [] # type: List[ExtruderConfigurationModel]
|
||||||
self._buildplate_configuration = ""
|
self._buildplate_configuration = ""
|
||||||
|
|
||||||
def setPrinterType(self, printer_type):
|
def setPrinterType(self, printer_type: str) -> None:
|
||||||
self._printer_type = printer_type
|
self._printer_type = printer_type
|
||||||
|
|
||||||
@pyqtProperty(str, fset = setPrinterType, notify = configurationChanged)
|
@pyqtProperty(str, fset = setPrinterType, notify = configurationChanged)
|
||||||
def printerType(self) -> str:
|
def printerType(self) -> str:
|
||||||
return self._printer_type
|
return self._printer_type
|
||||||
|
|
||||||
def setExtruderConfigurations(self, extruder_configurations: List["ExtruderConfigurationModel"]):
|
def setExtruderConfigurations(self, extruder_configurations: List["ExtruderConfigurationModel"]) -> None:
|
||||||
if self._extruder_configurations != extruder_configurations:
|
if self._extruder_configurations != extruder_configurations:
|
||||||
self._extruder_configurations = extruder_configurations
|
self._extruder_configurations = extruder_configurations
|
||||||
|
|
||||||
@ -40,7 +40,9 @@ class ConfigurationModel(QObject):
|
|||||||
return self._extruder_configurations
|
return self._extruder_configurations
|
||||||
|
|
||||||
def setBuildplateConfiguration(self, buildplate_configuration: str) -> None:
|
def setBuildplateConfiguration(self, buildplate_configuration: str) -> None:
|
||||||
|
if self._buildplate_configuration != buildplate_configuration:
|
||||||
self._buildplate_configuration = buildplate_configuration
|
self._buildplate_configuration = buildplate_configuration
|
||||||
|
self.configurationChanged.emit()
|
||||||
|
|
||||||
@pyqtProperty(str, fset = setBuildplateConfiguration, notify = configurationChanged)
|
@pyqtProperty(str, fset = setBuildplateConfiguration, notify = configurationChanged)
|
||||||
def buildplateConfiguration(self) -> str:
|
def buildplateConfiguration(self) -> str:
|
||||||
@ -54,7 +56,7 @@ class ConfigurationModel(QObject):
|
|||||||
for configuration in self._extruder_configurations:
|
for configuration in self._extruder_configurations:
|
||||||
if configuration is None:
|
if configuration is None:
|
||||||
return False
|
return False
|
||||||
return self._printer_type is not None
|
return self._printer_type != ""
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
message_chunks = []
|
message_chunks = []
|
||||||
@ -69,7 +71,23 @@ class ConfigurationModel(QObject):
|
|||||||
return "\n".join(message_chunks)
|
return "\n".join(message_chunks)
|
||||||
|
|
||||||
def __eq__(self, other):
|
def __eq__(self, other):
|
||||||
return hash(self) == hash(other)
|
if not isinstance(other, PrinterConfigurationModel):
|
||||||
|
return False
|
||||||
|
|
||||||
|
if self.printerType != other.printerType:
|
||||||
|
return False
|
||||||
|
|
||||||
|
if self.buildplateConfiguration != other.buildplateConfiguration:
|
||||||
|
return False
|
||||||
|
|
||||||
|
if len(self.extruderConfigurations) != len(other.extruderConfigurations):
|
||||||
|
return False
|
||||||
|
|
||||||
|
for self_extruder, other_extruder in zip(sorted(self._extruder_configurations, key=lambda x: x.position), sorted(other.extruderConfigurations, key=lambda x: x.position)):
|
||||||
|
if self_extruder != other_extruder:
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
## The hash function is used to compare and create unique sets. The configuration is unique if the configuration
|
## The hash function is used to compare and create unique sets. The configuration is unique if the configuration
|
||||||
# of the extruders is unique (the order of the extruders matters), and the type and buildplate is the same.
|
# of the extruders is unique (the order of the extruders matters), and the type and buildplate is the same.
|
312
cura/PrinterOutput/Models/PrinterOutputModel.py
Normal file
312
cura/PrinterOutput/Models/PrinterOutputModel.py
Normal file
@ -0,0 +1,312 @@
|
|||||||
|
# Copyright (c) 2019 Ultimaker B.V.
|
||||||
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
|
from PyQt5.QtCore import pyqtSignal, pyqtProperty, QObject, QVariant, pyqtSlot, QUrl
|
||||||
|
from typing import List, Dict, Optional, TYPE_CHECKING
|
||||||
|
from UM.Math.Vector import Vector
|
||||||
|
from cura.PrinterOutput.Peripheral import Peripheral
|
||||||
|
from cura.PrinterOutput.Models.PrinterConfigurationModel import PrinterConfigurationModel
|
||||||
|
from cura.PrinterOutput.Models.ExtruderOutputModel import ExtruderOutputModel
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from cura.PrinterOutput.Models.PrintJobOutputModel import PrintJobOutputModel
|
||||||
|
from cura.PrinterOutput.PrinterOutputController import PrinterOutputController
|
||||||
|
|
||||||
|
|
||||||
|
class PrinterOutputModel(QObject):
|
||||||
|
bedTemperatureChanged = pyqtSignal()
|
||||||
|
targetBedTemperatureChanged = pyqtSignal()
|
||||||
|
isPreheatingChanged = pyqtSignal()
|
||||||
|
stateChanged = pyqtSignal()
|
||||||
|
activePrintJobChanged = pyqtSignal()
|
||||||
|
nameChanged = pyqtSignal()
|
||||||
|
headPositionChanged = pyqtSignal()
|
||||||
|
keyChanged = pyqtSignal()
|
||||||
|
typeChanged = pyqtSignal()
|
||||||
|
buildplateChanged = pyqtSignal()
|
||||||
|
cameraUrlChanged = pyqtSignal()
|
||||||
|
configurationChanged = pyqtSignal()
|
||||||
|
canUpdateFirmwareChanged = pyqtSignal()
|
||||||
|
|
||||||
|
def __init__(self, output_controller: "PrinterOutputController", number_of_extruders: int = 1, parent=None, firmware_version = "") -> None:
|
||||||
|
super().__init__(parent)
|
||||||
|
self._bed_temperature = -1 # type: float # Use -1 for no heated bed.
|
||||||
|
self._target_bed_temperature = 0 # type: float
|
||||||
|
self._name = ""
|
||||||
|
self._key = "" # Unique identifier
|
||||||
|
self._controller = output_controller
|
||||||
|
self._controller.canUpdateFirmwareChanged.connect(self._onControllerCanUpdateFirmwareChanged)
|
||||||
|
self._extruders = [ExtruderOutputModel(printer = self, position = i) for i in range(number_of_extruders)]
|
||||||
|
self._printer_configuration = PrinterConfigurationModel() # Indicates the current configuration setup in this printer
|
||||||
|
self._head_position = Vector(0, 0, 0)
|
||||||
|
self._active_print_job = None # type: Optional[PrintJobOutputModel]
|
||||||
|
self._firmware_version = firmware_version
|
||||||
|
self._printer_state = "unknown"
|
||||||
|
self._is_preheating = False
|
||||||
|
self._printer_type = ""
|
||||||
|
self._buildplate = ""
|
||||||
|
self._peripherals = [] # type: List[Peripheral]
|
||||||
|
|
||||||
|
self._printer_configuration.extruderConfigurations = [extruder.extruderConfiguration for extruder in
|
||||||
|
self._extruders]
|
||||||
|
|
||||||
|
self._camera_url = QUrl() # type: QUrl
|
||||||
|
|
||||||
|
@pyqtProperty(str, constant = True)
|
||||||
|
def firmwareVersion(self) -> str:
|
||||||
|
return self._firmware_version
|
||||||
|
|
||||||
|
def setCameraUrl(self, camera_url: "QUrl") -> None:
|
||||||
|
if self._camera_url != camera_url:
|
||||||
|
self._camera_url = camera_url
|
||||||
|
self.cameraUrlChanged.emit()
|
||||||
|
|
||||||
|
@pyqtProperty(QUrl, fset = setCameraUrl, notify = cameraUrlChanged)
|
||||||
|
def cameraUrl(self) -> "QUrl":
|
||||||
|
return self._camera_url
|
||||||
|
|
||||||
|
def updateIsPreheating(self, pre_heating: bool) -> None:
|
||||||
|
if self._is_preheating != pre_heating:
|
||||||
|
self._is_preheating = pre_heating
|
||||||
|
self.isPreheatingChanged.emit()
|
||||||
|
|
||||||
|
@pyqtProperty(bool, notify=isPreheatingChanged)
|
||||||
|
def isPreheating(self) -> bool:
|
||||||
|
return self._is_preheating
|
||||||
|
|
||||||
|
@pyqtProperty(str, notify = typeChanged)
|
||||||
|
def type(self) -> str:
|
||||||
|
return self._printer_type
|
||||||
|
|
||||||
|
def updateType(self, printer_type: str) -> None:
|
||||||
|
if self._printer_type != printer_type:
|
||||||
|
self._printer_type = printer_type
|
||||||
|
self._printer_configuration.printerType = self._printer_type
|
||||||
|
self.typeChanged.emit()
|
||||||
|
self.configurationChanged.emit()
|
||||||
|
|
||||||
|
@pyqtProperty(str, notify = buildplateChanged)
|
||||||
|
def buildplate(self) -> str:
|
||||||
|
return self._buildplate
|
||||||
|
|
||||||
|
def updateBuildplate(self, buildplate: str) -> None:
|
||||||
|
if self._buildplate != buildplate:
|
||||||
|
self._buildplate = buildplate
|
||||||
|
self._printer_configuration.buildplateConfiguration = self._buildplate
|
||||||
|
self.buildplateChanged.emit()
|
||||||
|
self.configurationChanged.emit()
|
||||||
|
|
||||||
|
@pyqtProperty(str, notify=keyChanged)
|
||||||
|
def key(self) -> str:
|
||||||
|
return self._key
|
||||||
|
|
||||||
|
def updateKey(self, key: str) -> None:
|
||||||
|
if self._key != key:
|
||||||
|
self._key = key
|
||||||
|
self.keyChanged.emit()
|
||||||
|
|
||||||
|
@pyqtSlot()
|
||||||
|
def homeHead(self) -> None:
|
||||||
|
self._controller.homeHead(self)
|
||||||
|
|
||||||
|
@pyqtSlot()
|
||||||
|
def homeBed(self) -> None:
|
||||||
|
self._controller.homeBed(self)
|
||||||
|
|
||||||
|
@pyqtSlot(str)
|
||||||
|
def sendRawCommand(self, command: str) -> None:
|
||||||
|
self._controller.sendRawCommand(self, command)
|
||||||
|
|
||||||
|
@pyqtProperty("QVariantList", constant = True)
|
||||||
|
def extruders(self) -> List["ExtruderOutputModel"]:
|
||||||
|
return self._extruders
|
||||||
|
|
||||||
|
@pyqtProperty(QVariant, notify = headPositionChanged)
|
||||||
|
def headPosition(self) -> Dict[str, float]:
|
||||||
|
return {"x": self._head_position.x, "y": self._head_position.y, "z": self.head_position.z}
|
||||||
|
|
||||||
|
def updateHeadPosition(self, x: float, y: float, z: float) -> None:
|
||||||
|
if self._head_position.x != x or self._head_position.y != y or self._head_position.z != z:
|
||||||
|
self._head_position = Vector(x, y, z)
|
||||||
|
self.headPositionChanged.emit()
|
||||||
|
|
||||||
|
@pyqtProperty(float, float, float)
|
||||||
|
@pyqtProperty(float, float, float, float)
|
||||||
|
def setHeadPosition(self, x: float, y: float, z: float, speed: float = 3000) -> None:
|
||||||
|
self.updateHeadPosition(x, y, z)
|
||||||
|
self._controller.setHeadPosition(self, x, y, z, speed)
|
||||||
|
|
||||||
|
@pyqtProperty(float)
|
||||||
|
@pyqtProperty(float, float)
|
||||||
|
def setHeadX(self, x: float, speed: float = 3000) -> None:
|
||||||
|
self.updateHeadPosition(x, self._head_position.y, self._head_position.z)
|
||||||
|
self._controller.setHeadPosition(self, x, self._head_position.y, self._head_position.z, speed)
|
||||||
|
|
||||||
|
@pyqtProperty(float)
|
||||||
|
@pyqtProperty(float, float)
|
||||||
|
def setHeadY(self, y: float, speed: float = 3000) -> None:
|
||||||
|
self.updateHeadPosition(self._head_position.x, y, self._head_position.z)
|
||||||
|
self._controller.setHeadPosition(self, self._head_position.x, y, self._head_position.z, speed)
|
||||||
|
|
||||||
|
@pyqtProperty(float)
|
||||||
|
@pyqtProperty(float, float)
|
||||||
|
def setHeadZ(self, z: float, speed:float = 3000) -> None:
|
||||||
|
self.updateHeadPosition(self._head_position.x, self._head_position.y, z)
|
||||||
|
self._controller.setHeadPosition(self, self._head_position.x, self._head_position.y, z, speed)
|
||||||
|
|
||||||
|
@pyqtSlot(float, float, float)
|
||||||
|
@pyqtSlot(float, float, float, float)
|
||||||
|
def moveHead(self, x: float = 0, y: float = 0, z: float = 0, speed: float = 3000) -> None:
|
||||||
|
self._controller.moveHead(self, x, y, z, speed)
|
||||||
|
|
||||||
|
## Pre-heats the heated bed of the printer.
|
||||||
|
#
|
||||||
|
# \param temperature The temperature to heat the bed to, in degrees
|
||||||
|
# Celsius.
|
||||||
|
# \param duration How long the bed should stay warm, in seconds.
|
||||||
|
@pyqtSlot(float, float)
|
||||||
|
def preheatBed(self, temperature: float, duration: float) -> None:
|
||||||
|
self._controller.preheatBed(self, temperature, duration)
|
||||||
|
|
||||||
|
@pyqtSlot()
|
||||||
|
def cancelPreheatBed(self) -> None:
|
||||||
|
self._controller.cancelPreheatBed(self)
|
||||||
|
|
||||||
|
def getController(self) -> "PrinterOutputController":
|
||||||
|
return self._controller
|
||||||
|
|
||||||
|
@pyqtProperty(str, notify = nameChanged)
|
||||||
|
def name(self) -> str:
|
||||||
|
return self._name
|
||||||
|
|
||||||
|
def setName(self, name: str) -> None:
|
||||||
|
self.updateName(name)
|
||||||
|
|
||||||
|
def updateName(self, name: str) -> None:
|
||||||
|
if self._name != name:
|
||||||
|
self._name = name
|
||||||
|
self.nameChanged.emit()
|
||||||
|
|
||||||
|
## Update the bed temperature. This only changes it locally.
|
||||||
|
def updateBedTemperature(self, temperature: float) -> None:
|
||||||
|
if self._bed_temperature != temperature:
|
||||||
|
self._bed_temperature = temperature
|
||||||
|
self.bedTemperatureChanged.emit()
|
||||||
|
|
||||||
|
def updateTargetBedTemperature(self, temperature: float) -> None:
|
||||||
|
if self._target_bed_temperature != temperature:
|
||||||
|
self._target_bed_temperature = temperature
|
||||||
|
self.targetBedTemperatureChanged.emit()
|
||||||
|
|
||||||
|
## Set the target bed temperature. This ensures that it's actually sent to the remote.
|
||||||
|
@pyqtSlot(float)
|
||||||
|
def setTargetBedTemperature(self, temperature: float) -> None:
|
||||||
|
self._controller.setTargetBedTemperature(self, temperature)
|
||||||
|
self.updateTargetBedTemperature(temperature)
|
||||||
|
|
||||||
|
def updateActivePrintJob(self, print_job: Optional["PrintJobOutputModel"]) -> None:
|
||||||
|
if self._active_print_job != print_job:
|
||||||
|
old_print_job = self._active_print_job
|
||||||
|
|
||||||
|
if print_job is not None:
|
||||||
|
print_job.updateAssignedPrinter(self)
|
||||||
|
self._active_print_job = print_job
|
||||||
|
|
||||||
|
if old_print_job is not None:
|
||||||
|
old_print_job.updateAssignedPrinter(None)
|
||||||
|
self.activePrintJobChanged.emit()
|
||||||
|
|
||||||
|
def updateState(self, printer_state: str) -> None:
|
||||||
|
if self._printer_state != printer_state:
|
||||||
|
self._printer_state = printer_state
|
||||||
|
self.stateChanged.emit()
|
||||||
|
|
||||||
|
@pyqtProperty(QObject, notify = activePrintJobChanged)
|
||||||
|
def activePrintJob(self) -> Optional["PrintJobOutputModel"]:
|
||||||
|
return self._active_print_job
|
||||||
|
|
||||||
|
@pyqtProperty(str, notify = stateChanged)
|
||||||
|
def state(self) -> str:
|
||||||
|
return self._printer_state
|
||||||
|
|
||||||
|
@pyqtProperty(float, notify = bedTemperatureChanged)
|
||||||
|
def bedTemperature(self) -> float:
|
||||||
|
return self._bed_temperature
|
||||||
|
|
||||||
|
@pyqtProperty(float, notify = targetBedTemperatureChanged)
|
||||||
|
def targetBedTemperature(self) -> float:
|
||||||
|
return self._target_bed_temperature
|
||||||
|
|
||||||
|
# Does the printer support pre-heating the bed at all
|
||||||
|
@pyqtProperty(bool, constant = True)
|
||||||
|
def canPreHeatBed(self) -> bool:
|
||||||
|
if self._controller:
|
||||||
|
return self._controller.can_pre_heat_bed
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Does the printer support pre-heating the bed at all
|
||||||
|
@pyqtProperty(bool, constant = True)
|
||||||
|
def canPreHeatHotends(self) -> bool:
|
||||||
|
if self._controller:
|
||||||
|
return self._controller.can_pre_heat_hotends
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Does the printer support sending raw G-code at all
|
||||||
|
@pyqtProperty(bool, constant = True)
|
||||||
|
def canSendRawGcode(self) -> bool:
|
||||||
|
if self._controller:
|
||||||
|
return self._controller.can_send_raw_gcode
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Does the printer support pause at all
|
||||||
|
@pyqtProperty(bool, constant = True)
|
||||||
|
def canPause(self) -> bool:
|
||||||
|
if self._controller:
|
||||||
|
return self._controller.can_pause
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Does the printer support abort at all
|
||||||
|
@pyqtProperty(bool, constant = True)
|
||||||
|
def canAbort(self) -> bool:
|
||||||
|
if self._controller:
|
||||||
|
return self._controller.can_abort
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Does the printer support manual control at all
|
||||||
|
@pyqtProperty(bool, constant = True)
|
||||||
|
def canControlManually(self) -> bool:
|
||||||
|
if self._controller:
|
||||||
|
return self._controller.can_control_manually
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Does the printer support upgrading firmware
|
||||||
|
@pyqtProperty(bool, notify = canUpdateFirmwareChanged)
|
||||||
|
def canUpdateFirmware(self) -> bool:
|
||||||
|
if self._controller:
|
||||||
|
return self._controller.can_update_firmware
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Stub to connect UM.Signal to pyqtSignal
|
||||||
|
def _onControllerCanUpdateFirmwareChanged(self) -> None:
|
||||||
|
self.canUpdateFirmwareChanged.emit()
|
||||||
|
|
||||||
|
# Returns the configuration (material, variant and buildplate) of the current printer
|
||||||
|
@pyqtProperty(QObject, notify = configurationChanged)
|
||||||
|
def printerConfiguration(self) -> Optional[PrinterConfigurationModel]:
|
||||||
|
if self._printer_configuration.isValid():
|
||||||
|
return self._printer_configuration
|
||||||
|
return None
|
||||||
|
|
||||||
|
peripheralsChanged = pyqtSignal()
|
||||||
|
|
||||||
|
@pyqtProperty(str, notify = peripheralsChanged)
|
||||||
|
def peripherals(self) -> str:
|
||||||
|
return ", ".join(*[peripheral.name for peripheral in self._peripherals])
|
||||||
|
|
||||||
|
def addPeripheral(self, peripheral: Peripheral) -> None:
|
||||||
|
self._peripherals.append(peripheral)
|
||||||
|
self.peripheralsChanged.emit()
|
||||||
|
|
||||||
|
def removePeripheral(self, peripheral: Peripheral) -> None:
|
||||||
|
self._peripherals.remove(peripheral)
|
||||||
|
self.peripheralsChanged.emit()
|
0
cura/PrinterOutput/Models/__init__.py
Normal file
0
cura/PrinterOutput/Models/__init__.py
Normal file
@ -1,119 +0,0 @@
|
|||||||
from UM.Logger import Logger
|
|
||||||
|
|
||||||
from PyQt5.QtCore import QUrl, pyqtProperty, pyqtSignal, QObject, pyqtSlot
|
|
||||||
from PyQt5.QtGui import QImage
|
|
||||||
from PyQt5.QtNetwork import QNetworkRequest, QNetworkReply, QNetworkAccessManager
|
|
||||||
|
|
||||||
|
|
||||||
class NetworkCamera(QObject):
|
|
||||||
newImage = pyqtSignal()
|
|
||||||
|
|
||||||
def __init__(self, target = None, parent = None):
|
|
||||||
super().__init__(parent)
|
|
||||||
self._stream_buffer = b""
|
|
||||||
self._stream_buffer_start_index = -1
|
|
||||||
self._manager = None
|
|
||||||
self._image_request = None
|
|
||||||
self._image_reply = None
|
|
||||||
self._image = QImage()
|
|
||||||
self._image_id = 0
|
|
||||||
|
|
||||||
self._target = target
|
|
||||||
self._started = False
|
|
||||||
|
|
||||||
@pyqtSlot(str)
|
|
||||||
def setTarget(self, target):
|
|
||||||
restart_required = False
|
|
||||||
if self._started:
|
|
||||||
self.stop()
|
|
||||||
restart_required = True
|
|
||||||
|
|
||||||
self._target = target
|
|
||||||
|
|
||||||
if restart_required:
|
|
||||||
self.start()
|
|
||||||
|
|
||||||
@pyqtProperty(QUrl, notify=newImage)
|
|
||||||
def latestImage(self):
|
|
||||||
self._image_id += 1
|
|
||||||
# There is an image provider that is called "camera". In order to ensure that the image qml object, that
|
|
||||||
# requires a QUrl to function, updates correctly we add an increasing number. This causes to see the QUrl
|
|
||||||
# as new (instead of relying on cached version and thus forces an update.
|
|
||||||
temp = "image://camera/" + str(self._image_id)
|
|
||||||
|
|
||||||
return QUrl(temp, QUrl.TolerantMode)
|
|
||||||
|
|
||||||
@pyqtSlot()
|
|
||||||
def start(self):
|
|
||||||
# Ensure that previous requests (if any) are stopped.
|
|
||||||
self.stop()
|
|
||||||
if self._target is None:
|
|
||||||
Logger.log("w", "Unable to start camera stream without target!")
|
|
||||||
return
|
|
||||||
self._started = True
|
|
||||||
url = QUrl(self._target)
|
|
||||||
self._image_request = QNetworkRequest(url)
|
|
||||||
if self._manager is None:
|
|
||||||
self._manager = QNetworkAccessManager()
|
|
||||||
|
|
||||||
self._image_reply = self._manager.get(self._image_request)
|
|
||||||
self._image_reply.downloadProgress.connect(self._onStreamDownloadProgress)
|
|
||||||
|
|
||||||
@pyqtSlot()
|
|
||||||
def stop(self):
|
|
||||||
self._stream_buffer = b""
|
|
||||||
self._stream_buffer_start_index = -1
|
|
||||||
|
|
||||||
if self._image_reply:
|
|
||||||
try:
|
|
||||||
# disconnect the signal
|
|
||||||
try:
|
|
||||||
self._image_reply.downloadProgress.disconnect(self._onStreamDownloadProgress)
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
# abort the request if it's not finished
|
|
||||||
if not self._image_reply.isFinished():
|
|
||||||
self._image_reply.close()
|
|
||||||
except Exception as e: # RuntimeError
|
|
||||||
pass # It can happen that the wrapped c++ object is already deleted.
|
|
||||||
|
|
||||||
self._image_reply = None
|
|
||||||
self._image_request = None
|
|
||||||
|
|
||||||
self._manager = None
|
|
||||||
|
|
||||||
self._started = False
|
|
||||||
|
|
||||||
def getImage(self):
|
|
||||||
return self._image
|
|
||||||
|
|
||||||
## Ensure that close gets called when object is destroyed
|
|
||||||
def __del__(self):
|
|
||||||
self.stop()
|
|
||||||
|
|
||||||
def _onStreamDownloadProgress(self, bytes_received, bytes_total):
|
|
||||||
# An MJPG stream is (for our purpose) a stream of concatenated JPG images.
|
|
||||||
# JPG images start with the marker 0xFFD8, and end with 0xFFD9
|
|
||||||
if self._image_reply is None:
|
|
||||||
return
|
|
||||||
self._stream_buffer += self._image_reply.readAll()
|
|
||||||
|
|
||||||
if len(self._stream_buffer) > 2000000: # No single camera frame should be 2 Mb or larger
|
|
||||||
Logger.log("w", "MJPEG buffer exceeds reasonable size. Restarting stream...")
|
|
||||||
self.stop() # resets stream buffer and start index
|
|
||||||
self.start()
|
|
||||||
return
|
|
||||||
|
|
||||||
if self._stream_buffer_start_index == -1:
|
|
||||||
self._stream_buffer_start_index = self._stream_buffer.indexOf(b'\xff\xd8')
|
|
||||||
stream_buffer_end_index = self._stream_buffer.lastIndexOf(b'\xff\xd9')
|
|
||||||
# If this happens to be more than a single frame, then so be it; the JPG decoder will
|
|
||||||
# ignore the extra data. We do it like this in order not to get a buildup of frames
|
|
||||||
|
|
||||||
if self._stream_buffer_start_index != -1 and stream_buffer_end_index != -1:
|
|
||||||
jpg_data = self._stream_buffer[self._stream_buffer_start_index:stream_buffer_end_index + 2]
|
|
||||||
self._stream_buffer = self._stream_buffer[stream_buffer_end_index + 2:]
|
|
||||||
self._stream_buffer_start_index = -1
|
|
||||||
self._image.loadFromData(jpg_data)
|
|
||||||
|
|
||||||
self.newImage.emit()
|
|
153
cura/PrinterOutput/NetworkMJPGImage.py
Normal file
153
cura/PrinterOutput/NetworkMJPGImage.py
Normal file
@ -0,0 +1,153 @@
|
|||||||
|
# Copyright (c) 2018 Aldo Hoeben / fieldOfView
|
||||||
|
# NetworkMJPGImage is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
|
from PyQt5.QtCore import QUrl, pyqtProperty, pyqtSignal, pyqtSlot, QRect, QByteArray
|
||||||
|
from PyQt5.QtGui import QImage, QPainter
|
||||||
|
from PyQt5.QtQuick import QQuickPaintedItem
|
||||||
|
from PyQt5.QtNetwork import QNetworkRequest, QNetworkReply, QNetworkAccessManager
|
||||||
|
|
||||||
|
from UM.Logger import Logger
|
||||||
|
|
||||||
|
#
|
||||||
|
# A QQuickPaintedItem that progressively downloads a network mjpeg stream,
|
||||||
|
# picks it apart in individual jpeg frames, and paints it.
|
||||||
|
#
|
||||||
|
class NetworkMJPGImage(QQuickPaintedItem):
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs) -> None:
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
self._stream_buffer = QByteArray()
|
||||||
|
self._stream_buffer_start_index = -1
|
||||||
|
self._network_manager = None # type: QNetworkAccessManager
|
||||||
|
self._image_request = None # type: QNetworkRequest
|
||||||
|
self._image_reply = None # type: QNetworkReply
|
||||||
|
self._image = QImage()
|
||||||
|
self._image_rect = QRect()
|
||||||
|
|
||||||
|
self._source_url = QUrl()
|
||||||
|
self._started = False
|
||||||
|
|
||||||
|
self._mirror = False
|
||||||
|
|
||||||
|
self.setAntialiasing(True)
|
||||||
|
|
||||||
|
## Ensure that close gets called when object is destroyed
|
||||||
|
def __del__(self) -> None:
|
||||||
|
self.stop()
|
||||||
|
|
||||||
|
|
||||||
|
def paint(self, painter: "QPainter") -> None:
|
||||||
|
if self._mirror:
|
||||||
|
painter.drawImage(self.contentsBoundingRect(), self._image.mirrored())
|
||||||
|
return
|
||||||
|
|
||||||
|
painter.drawImage(self.contentsBoundingRect(), self._image)
|
||||||
|
|
||||||
|
|
||||||
|
def setSourceURL(self, source_url: "QUrl") -> None:
|
||||||
|
self._source_url = source_url
|
||||||
|
self.sourceURLChanged.emit()
|
||||||
|
if self._started:
|
||||||
|
self.start()
|
||||||
|
|
||||||
|
def getSourceURL(self) -> "QUrl":
|
||||||
|
return self._source_url
|
||||||
|
|
||||||
|
sourceURLChanged = pyqtSignal()
|
||||||
|
source = pyqtProperty(QUrl, fget = getSourceURL, fset = setSourceURL, notify = sourceURLChanged)
|
||||||
|
|
||||||
|
def setMirror(self, mirror: bool) -> None:
|
||||||
|
if mirror == self._mirror:
|
||||||
|
return
|
||||||
|
self._mirror = mirror
|
||||||
|
self.mirrorChanged.emit()
|
||||||
|
self.update()
|
||||||
|
|
||||||
|
def getMirror(self) -> bool:
|
||||||
|
return self._mirror
|
||||||
|
|
||||||
|
mirrorChanged = pyqtSignal()
|
||||||
|
mirror = pyqtProperty(bool, fget = getMirror, fset = setMirror, notify = mirrorChanged)
|
||||||
|
|
||||||
|
imageSizeChanged = pyqtSignal()
|
||||||
|
|
||||||
|
@pyqtProperty(int, notify = imageSizeChanged)
|
||||||
|
def imageWidth(self) -> int:
|
||||||
|
return self._image.width()
|
||||||
|
|
||||||
|
@pyqtProperty(int, notify = imageSizeChanged)
|
||||||
|
def imageHeight(self) -> int:
|
||||||
|
return self._image.height()
|
||||||
|
|
||||||
|
|
||||||
|
@pyqtSlot()
|
||||||
|
def start(self) -> None:
|
||||||
|
self.stop() # Ensure that previous requests (if any) are stopped.
|
||||||
|
|
||||||
|
if not self._source_url:
|
||||||
|
Logger.log("w", "Unable to start camera stream without target!")
|
||||||
|
return
|
||||||
|
self._started = True
|
||||||
|
|
||||||
|
self._image_request = QNetworkRequest(self._source_url)
|
||||||
|
if self._network_manager is None:
|
||||||
|
self._network_manager = QNetworkAccessManager()
|
||||||
|
|
||||||
|
self._image_reply = self._network_manager.get(self._image_request)
|
||||||
|
self._image_reply.downloadProgress.connect(self._onStreamDownloadProgress)
|
||||||
|
|
||||||
|
@pyqtSlot()
|
||||||
|
def stop(self) -> None:
|
||||||
|
self._stream_buffer = QByteArray()
|
||||||
|
self._stream_buffer_start_index = -1
|
||||||
|
|
||||||
|
if self._image_reply:
|
||||||
|
try:
|
||||||
|
try:
|
||||||
|
self._image_reply.downloadProgress.disconnect(self._onStreamDownloadProgress)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if not self._image_reply.isFinished():
|
||||||
|
self._image_reply.close()
|
||||||
|
except Exception as e: # RuntimeError
|
||||||
|
pass # It can happen that the wrapped c++ object is already deleted.
|
||||||
|
|
||||||
|
self._image_reply = None
|
||||||
|
self._image_request = None
|
||||||
|
|
||||||
|
self._network_manager = None
|
||||||
|
|
||||||
|
self._started = False
|
||||||
|
|
||||||
|
|
||||||
|
def _onStreamDownloadProgress(self, bytes_received: int, bytes_total: int) -> None:
|
||||||
|
# An MJPG stream is (for our purpose) a stream of concatenated JPG images.
|
||||||
|
# JPG images start with the marker 0xFFD8, and end with 0xFFD9
|
||||||
|
if self._image_reply is None:
|
||||||
|
return
|
||||||
|
self._stream_buffer += self._image_reply.readAll()
|
||||||
|
|
||||||
|
if len(self._stream_buffer) > 2000000: # No single camera frame should be 2 Mb or larger
|
||||||
|
Logger.log("w", "MJPEG buffer exceeds reasonable size. Restarting stream...")
|
||||||
|
self.stop() # resets stream buffer and start index
|
||||||
|
self.start()
|
||||||
|
return
|
||||||
|
|
||||||
|
if self._stream_buffer_start_index == -1:
|
||||||
|
self._stream_buffer_start_index = self._stream_buffer.indexOf(b'\xff\xd8')
|
||||||
|
stream_buffer_end_index = self._stream_buffer.lastIndexOf(b'\xff\xd9')
|
||||||
|
# If this happens to be more than a single frame, then so be it; the JPG decoder will
|
||||||
|
# ignore the extra data. We do it like this in order not to get a buildup of frames
|
||||||
|
|
||||||
|
if self._stream_buffer_start_index != -1 and stream_buffer_end_index != -1:
|
||||||
|
jpg_data = self._stream_buffer[self._stream_buffer_start_index:stream_buffer_end_index + 2]
|
||||||
|
self._stream_buffer = self._stream_buffer[stream_buffer_end_index + 2:]
|
||||||
|
self._stream_buffer_start_index = -1
|
||||||
|
self._image.loadFromData(jpg_data)
|
||||||
|
|
||||||
|
if self._image.rect() != self._image_rect:
|
||||||
|
self.imageSizeChanged.emit()
|
||||||
|
|
||||||
|
self.update()
|
@ -4,19 +4,23 @@
|
|||||||
from UM.FileHandler.FileHandler import FileHandler #For typing.
|
from UM.FileHandler.FileHandler import FileHandler #For typing.
|
||||||
from UM.Logger import Logger
|
from UM.Logger import Logger
|
||||||
from UM.Scene.SceneNode import SceneNode #For typing.
|
from UM.Scene.SceneNode import SceneNode #For typing.
|
||||||
|
from cura.API import Account
|
||||||
from cura.CuraApplication import CuraApplication
|
from cura.CuraApplication import CuraApplication
|
||||||
|
|
||||||
from cura.PrinterOutputDevice import PrinterOutputDevice, ConnectionState
|
from cura.PrinterOutput.PrinterOutputDevice import PrinterOutputDevice, ConnectionState, ConnectionType
|
||||||
|
|
||||||
from PyQt5.QtNetwork import QHttpMultiPart, QHttpPart, QNetworkRequest, QNetworkAccessManager, QNetworkReply, QAuthenticator
|
from PyQt5.QtNetwork import QHttpMultiPart, QHttpPart, QNetworkRequest, QNetworkAccessManager, QNetworkReply, QAuthenticator
|
||||||
from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject, QUrl, QCoreApplication
|
from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject, QUrl, QCoreApplication
|
||||||
from time import time
|
from time import time
|
||||||
from typing import Any, Callable, Dict, List, Optional
|
from typing import Callable, Dict, List, Optional, Union
|
||||||
from enum import IntEnum
|
from enum import IntEnum
|
||||||
|
|
||||||
import os # To get the username
|
import os # To get the username
|
||||||
import gzip
|
import gzip
|
||||||
|
|
||||||
|
from cura.Settings.CuraContainerRegistry import CuraContainerRegistry
|
||||||
|
|
||||||
|
|
||||||
class AuthState(IntEnum):
|
class AuthState(IntEnum):
|
||||||
NotAuthenticated = 1
|
NotAuthenticated = 1
|
||||||
AuthenticationRequested = 2
|
AuthenticationRequested = 2
|
||||||
@ -28,8 +32,8 @@ class AuthState(IntEnum):
|
|||||||
class NetworkedPrinterOutputDevice(PrinterOutputDevice):
|
class NetworkedPrinterOutputDevice(PrinterOutputDevice):
|
||||||
authenticationStateChanged = pyqtSignal()
|
authenticationStateChanged = pyqtSignal()
|
||||||
|
|
||||||
def __init__(self, device_id, address: str, properties: Dict[bytes, bytes], parent: QObject = None) -> None:
|
def __init__(self, device_id, address: str, properties: Dict[bytes, bytes], connection_type: ConnectionType = ConnectionType.NetworkConnection, parent: QObject = None) -> None:
|
||||||
super().__init__(device_id = device_id, parent = parent)
|
super().__init__(device_id = device_id, connection_type = connection_type, parent = parent)
|
||||||
self._manager = None # type: Optional[QNetworkAccessManager]
|
self._manager = None # type: Optional[QNetworkAccessManager]
|
||||||
self._last_manager_create_time = None # type: Optional[float]
|
self._last_manager_create_time = None # type: Optional[float]
|
||||||
self._recreate_network_manager_time = 30
|
self._recreate_network_manager_time = 30
|
||||||
@ -41,7 +45,8 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice):
|
|||||||
self._api_prefix = ""
|
self._api_prefix = ""
|
||||||
self._address = address
|
self._address = address
|
||||||
self._properties = properties
|
self._properties = properties
|
||||||
self._user_agent = "%s/%s " % (CuraApplication.getInstance().getApplicationName(), CuraApplication.getInstance().getVersion())
|
self._user_agent = "%s/%s " % (CuraApplication.getInstance().getApplicationName(),
|
||||||
|
CuraApplication.getInstance().getVersion())
|
||||||
|
|
||||||
self._onFinishedCallbacks = {} # type: Dict[str, Callable[[QNetworkReply], None]]
|
self._onFinishedCallbacks = {} # type: Dict[str, Callable[[QNetworkReply], None]]
|
||||||
self._authentication_state = AuthState.NotAuthenticated
|
self._authentication_state = AuthState.NotAuthenticated
|
||||||
@ -55,7 +60,8 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice):
|
|||||||
self._gcode = [] # type: List[str]
|
self._gcode = [] # type: List[str]
|
||||||
self._connection_state_before_timeout = None # type: Optional[ConnectionState]
|
self._connection_state_before_timeout = None # type: Optional[ConnectionState]
|
||||||
|
|
||||||
def requestWrite(self, nodes: List[SceneNode], file_name: Optional[str] = None, limit_mimetypes: bool = False, file_handler: Optional[FileHandler] = None, **kwargs: str) -> None:
|
def requestWrite(self, nodes: List["SceneNode"], file_name: Optional[str] = None, limit_mimetypes: bool = False,
|
||||||
|
file_handler: Optional["FileHandler"] = None, filter_by_machine: bool = False, **kwargs) -> None:
|
||||||
raise NotImplementedError("requestWrite needs to be implemented")
|
raise NotImplementedError("requestWrite needs to be implemented")
|
||||||
|
|
||||||
def setAuthenticationState(self, authentication_state: AuthState) -> None:
|
def setAuthenticationState(self, authentication_state: AuthState) -> None:
|
||||||
@ -125,17 +131,15 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice):
|
|||||||
if self._connection_state_before_timeout is None:
|
if self._connection_state_before_timeout is None:
|
||||||
self._connection_state_before_timeout = self._connection_state
|
self._connection_state_before_timeout = self._connection_state
|
||||||
|
|
||||||
self.setConnectionState(ConnectionState.closed)
|
self.setConnectionState(ConnectionState.Closed)
|
||||||
|
|
||||||
# We need to check if the manager needs to be re-created. If we don't, we get some issues when OSX goes to
|
# We need to check if the manager needs to be re-created. If we don't, we get some issues when OSX goes to
|
||||||
# sleep.
|
# sleep.
|
||||||
if time_since_last_response > self._recreate_network_manager_time:
|
if time_since_last_response > self._recreate_network_manager_time:
|
||||||
if self._last_manager_create_time is None:
|
if self._last_manager_create_time is None or time() - self._last_manager_create_time > self._recreate_network_manager_time:
|
||||||
self._createNetworkManager()
|
|
||||||
elif time() - self._last_manager_create_time > self._recreate_network_manager_time:
|
|
||||||
self._createNetworkManager()
|
self._createNetworkManager()
|
||||||
assert(self._manager is not None)
|
assert(self._manager is not None)
|
||||||
elif self._connection_state == ConnectionState.closed:
|
elif self._connection_state == ConnectionState.Closed:
|
||||||
# Go out of timeout.
|
# Go out of timeout.
|
||||||
if self._connection_state_before_timeout is not None: # sanity check, but it should never be None here
|
if self._connection_state_before_timeout is not None: # sanity check, but it should never be None here
|
||||||
self.setConnectionState(self._connection_state_before_timeout)
|
self.setConnectionState(self._connection_state_before_timeout)
|
||||||
@ -145,10 +149,15 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice):
|
|||||||
url = QUrl("http://" + self._address + self._api_prefix + target)
|
url = QUrl("http://" + self._address + self._api_prefix + target)
|
||||||
request = QNetworkRequest(url)
|
request = QNetworkRequest(url)
|
||||||
if content_type is not None:
|
if content_type is not None:
|
||||||
request.setHeader(QNetworkRequest.ContentTypeHeader, "application/json")
|
request.setHeader(QNetworkRequest.ContentTypeHeader, content_type)
|
||||||
request.setHeader(QNetworkRequest.UserAgentHeader, self._user_agent)
|
request.setHeader(QNetworkRequest.UserAgentHeader, self._user_agent)
|
||||||
return request
|
return request
|
||||||
|
|
||||||
|
## This method was only available privately before, but it was actually called from SendMaterialJob.py.
|
||||||
|
# We now have a public equivalent as well. We did not remove the private one as plugins might be using that.
|
||||||
|
def createFormPart(self, content_header: str, data: bytes, content_type: Optional[str] = None) -> QHttpPart:
|
||||||
|
return self._createFormPart(content_header, data, content_type)
|
||||||
|
|
||||||
def _createFormPart(self, content_header: str, data: bytes, content_type: Optional[str] = None) -> QHttpPart:
|
def _createFormPart(self, content_header: str, data: bytes, content_type: Optional[str] = None) -> QHttpPart:
|
||||||
part = QHttpPart()
|
part = QHttpPart()
|
||||||
|
|
||||||
@ -162,9 +171,15 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice):
|
|||||||
part.setBody(data)
|
part.setBody(data)
|
||||||
return part
|
return part
|
||||||
|
|
||||||
## Convenience function to get the username from the OS.
|
## Convenience function to get the username, either from the cloud or from the OS.
|
||||||
# The code was copied from the getpass module, as we try to use as little dependencies as possible.
|
|
||||||
def _getUserName(self) -> str:
|
def _getUserName(self) -> str:
|
||||||
|
# check first if we are logged in with the Ultimaker Account
|
||||||
|
account = CuraApplication.getInstance().getCuraAPI().account # type: Account
|
||||||
|
if account and account.isLoggedIn:
|
||||||
|
return account.userName
|
||||||
|
|
||||||
|
# Otherwise get the username from the US
|
||||||
|
# The code below was copied from the getpass module, as we try to use as little dependencies as possible.
|
||||||
for name in ("LOGNAME", "USER", "LNAME", "USERNAME"):
|
for name in ("LOGNAME", "USER", "LNAME", "USERNAME"):
|
||||||
user = os.environ.get(name)
|
user = os.environ.get(name)
|
||||||
if user:
|
if user:
|
||||||
@ -180,49 +195,89 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice):
|
|||||||
self._createNetworkManager()
|
self._createNetworkManager()
|
||||||
assert (self._manager is not None)
|
assert (self._manager is not None)
|
||||||
|
|
||||||
def put(self, target: str, data: str, on_finished: Optional[Callable[[QNetworkReply], None]]) -> None:
|
## Sends a put request to the given path.
|
||||||
|
# \param url: The path after the API prefix.
|
||||||
|
# \param data: The data to be sent in the body
|
||||||
|
# \param content_type: The content type of the body data.
|
||||||
|
# \param on_finished: The function to call when the response is received.
|
||||||
|
# \param on_progress: The function to call when the progress changes. Parameters are bytes_sent / bytes_total.
|
||||||
|
def put(self, url: str, data: Union[str, bytes], content_type: Optional[str] = "application/json",
|
||||||
|
on_finished: Optional[Callable[[QNetworkReply], None]] = None,
|
||||||
|
on_progress: Optional[Callable[[int, int], None]] = None) -> None:
|
||||||
self._validateManager()
|
self._validateManager()
|
||||||
request = self._createEmptyRequest(target)
|
|
||||||
self._last_request_time = time()
|
|
||||||
if self._manager is not None:
|
|
||||||
reply = self._manager.put(request, data.encode())
|
|
||||||
self._registerOnFinishedCallback(reply, on_finished)
|
|
||||||
else:
|
|
||||||
Logger.log("e", "Could not find manager.")
|
|
||||||
|
|
||||||
def delete(self, target: str, on_finished: Optional[Callable[[QNetworkReply], None]]) -> None:
|
request = self._createEmptyRequest(url, content_type = content_type)
|
||||||
self._validateManager()
|
|
||||||
request = self._createEmptyRequest(target)
|
|
||||||
self._last_request_time = time()
|
self._last_request_time = time()
|
||||||
if self._manager is not None:
|
|
||||||
|
if not self._manager:
|
||||||
|
Logger.log("e", "No network manager was created to execute the PUT call with.")
|
||||||
|
return
|
||||||
|
|
||||||
|
body = data if isinstance(data, bytes) else data.encode() # type: bytes
|
||||||
|
reply = self._manager.put(request, body)
|
||||||
|
self._registerOnFinishedCallback(reply, on_finished)
|
||||||
|
|
||||||
|
if on_progress is not None:
|
||||||
|
reply.uploadProgress.connect(on_progress)
|
||||||
|
|
||||||
|
## Sends a delete request to the given path.
|
||||||
|
# \param url: The path after the API prefix.
|
||||||
|
# \param on_finished: The function to be call when the response is received.
|
||||||
|
def delete(self, url: str, on_finished: Optional[Callable[[QNetworkReply], None]]) -> None:
|
||||||
|
self._validateManager()
|
||||||
|
|
||||||
|
request = self._createEmptyRequest(url)
|
||||||
|
self._last_request_time = time()
|
||||||
|
|
||||||
|
if not self._manager:
|
||||||
|
Logger.log("e", "No network manager was created to execute the DELETE call with.")
|
||||||
|
return
|
||||||
|
|
||||||
reply = self._manager.deleteResource(request)
|
reply = self._manager.deleteResource(request)
|
||||||
self._registerOnFinishedCallback(reply, on_finished)
|
self._registerOnFinishedCallback(reply, on_finished)
|
||||||
else:
|
|
||||||
Logger.log("e", "Could not find manager.")
|
|
||||||
|
|
||||||
def get(self, target: str, on_finished: Optional[Callable[[QNetworkReply], None]]) -> None:
|
## Sends a get request to the given path.
|
||||||
|
# \param url: The path after the API prefix.
|
||||||
|
# \param on_finished: The function to be call when the response is received.
|
||||||
|
def get(self, url: str, on_finished: Optional[Callable[[QNetworkReply], None]]) -> None:
|
||||||
self._validateManager()
|
self._validateManager()
|
||||||
request = self._createEmptyRequest(target)
|
|
||||||
|
request = self._createEmptyRequest(url)
|
||||||
self._last_request_time = time()
|
self._last_request_time = time()
|
||||||
if self._manager is not None:
|
|
||||||
|
if not self._manager:
|
||||||
|
Logger.log("e", "No network manager was created to execute the GET call with.")
|
||||||
|
return
|
||||||
|
|
||||||
reply = self._manager.get(request)
|
reply = self._manager.get(request)
|
||||||
self._registerOnFinishedCallback(reply, on_finished)
|
self._registerOnFinishedCallback(reply, on_finished)
|
||||||
else:
|
|
||||||
Logger.log("e", "Could not find manager.")
|
|
||||||
|
|
||||||
def post(self, target: str, data: str, on_finished: Optional[Callable[[QNetworkReply], None]], on_progress: Callable = None) -> None:
|
## Sends a post request to the given path.
|
||||||
|
# \param url: The path after the API prefix.
|
||||||
|
# \param data: The data to be sent in the body
|
||||||
|
# \param on_finished: The function to call when the response is received.
|
||||||
|
# \param on_progress: The function to call when the progress changes. Parameters are bytes_sent / bytes_total.
|
||||||
|
def post(self, url: str, data: Union[str, bytes],
|
||||||
|
on_finished: Optional[Callable[[QNetworkReply], None]],
|
||||||
|
on_progress: Optional[Callable[[int, int], None]] = None) -> None:
|
||||||
self._validateManager()
|
self._validateManager()
|
||||||
request = self._createEmptyRequest(target)
|
|
||||||
|
request = self._createEmptyRequest(url)
|
||||||
self._last_request_time = time()
|
self._last_request_time = time()
|
||||||
if self._manager is not None:
|
|
||||||
reply = self._manager.post(request, data)
|
if not self._manager:
|
||||||
|
Logger.log("e", "Could not find manager.")
|
||||||
|
return
|
||||||
|
|
||||||
|
body = data if isinstance(data, bytes) else data.encode() # type: bytes
|
||||||
|
reply = self._manager.post(request, body)
|
||||||
if on_progress is not None:
|
if on_progress is not None:
|
||||||
reply.uploadProgress.connect(on_progress)
|
reply.uploadProgress.connect(on_progress)
|
||||||
self._registerOnFinishedCallback(reply, on_finished)
|
self._registerOnFinishedCallback(reply, on_finished)
|
||||||
else:
|
|
||||||
Logger.log("e", "Could not find manager.")
|
|
||||||
|
|
||||||
def postFormWithParts(self, target: str, parts: List[QHttpPart], on_finished: Optional[Callable[[QNetworkReply], None]], on_progress: Callable = None) -> QNetworkReply:
|
def postFormWithParts(self, target: str, parts: List[QHttpPart],
|
||||||
|
on_finished: Optional[Callable[[QNetworkReply], None]],
|
||||||
|
on_progress: Optional[Callable[[int, int], None]] = None) -> QNetworkReply:
|
||||||
self._validateManager()
|
self._validateManager()
|
||||||
request = self._createEmptyRequest(target, content_type=None)
|
request = self._createEmptyRequest(target, content_type=None)
|
||||||
multi_post_part = QHttpMultiPart(QHttpMultiPart.FormDataType)
|
multi_post_part = QHttpMultiPart(QHttpMultiPart.FormDataType)
|
||||||
@ -257,22 +312,37 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice):
|
|||||||
def _createNetworkManager(self) -> None:
|
def _createNetworkManager(self) -> None:
|
||||||
Logger.log("d", "Creating network manager")
|
Logger.log("d", "Creating network manager")
|
||||||
if self._manager:
|
if self._manager:
|
||||||
self._manager.finished.disconnect(self.__handleOnFinished)
|
self._manager.finished.disconnect(self._handleOnFinished)
|
||||||
self._manager.authenticationRequired.disconnect(self._onAuthenticationRequired)
|
self._manager.authenticationRequired.disconnect(self._onAuthenticationRequired)
|
||||||
|
|
||||||
self._manager = QNetworkAccessManager()
|
self._manager = QNetworkAccessManager()
|
||||||
self._manager.finished.connect(self.__handleOnFinished)
|
self._manager.finished.connect(self._handleOnFinished)
|
||||||
self._last_manager_create_time = time()
|
self._last_manager_create_time = time()
|
||||||
self._manager.authenticationRequired.connect(self._onAuthenticationRequired)
|
self._manager.authenticationRequired.connect(self._onAuthenticationRequired)
|
||||||
|
|
||||||
if self._properties.get(b"temporary", b"false") != b"true":
|
if self._properties.get(b"temporary", b"false") != b"true":
|
||||||
CuraApplication.getInstance().getMachineManager().checkCorrectGroupName(self.getId(), self.name)
|
self._checkCorrectGroupName(self.getId(), self.name)
|
||||||
|
|
||||||
def _registerOnFinishedCallback(self, reply: QNetworkReply, on_finished: Optional[Callable[[QNetworkReply], None]]) -> None:
|
def _registerOnFinishedCallback(self, reply: QNetworkReply, on_finished: Optional[Callable[[QNetworkReply], None]]) -> None:
|
||||||
if on_finished is not None:
|
if on_finished is not None:
|
||||||
self._onFinishedCallbacks[reply.url().toString() + str(reply.operation())] = on_finished
|
self._onFinishedCallbacks[reply.url().toString() + str(reply.operation())] = on_finished
|
||||||
|
|
||||||
def __handleOnFinished(self, reply: QNetworkReply) -> None:
|
## This method checks if the name of the group stored in the definition container is correct.
|
||||||
|
# After updating from 3.2 to 3.3 some group names may be temporary. If there is a mismatch in the name of the group
|
||||||
|
# then all the container stacks are updated, both the current and the hidden ones.
|
||||||
|
def _checkCorrectGroupName(self, device_id: str, group_name: str) -> None:
|
||||||
|
global_container_stack = CuraApplication.getInstance().getGlobalContainerStack()
|
||||||
|
active_machine_network_name = CuraApplication.getInstance().getMachineManager().activeMachineNetworkKey()
|
||||||
|
if global_container_stack and device_id == active_machine_network_name:
|
||||||
|
# Check if the group_name is correct. If not, update all the containers connected to the same printer
|
||||||
|
if CuraApplication.getInstance().getMachineManager().activeMachineNetworkGroupName != group_name:
|
||||||
|
metadata_filter = {"um_network_key": active_machine_network_name}
|
||||||
|
containers = CuraContainerRegistry.getInstance().findContainerStacks(type="machine",
|
||||||
|
**metadata_filter)
|
||||||
|
for container in containers:
|
||||||
|
container.setMetaDataEntry("group_name", group_name)
|
||||||
|
|
||||||
|
def _handleOnFinished(self, reply: QNetworkReply) -> None:
|
||||||
# Due to garbage collection, we need to cache certain bits of post operations.
|
# Due to garbage collection, we need to cache certain bits of post operations.
|
||||||
# As we don't want to keep them around forever, delete them if we get a reply.
|
# As we don't want to keep them around forever, delete them if we get a reply.
|
||||||
if reply.operation() == QNetworkAccessManager.PostOperation:
|
if reply.operation() == QNetworkAccessManager.PostOperation:
|
||||||
@ -284,8 +354,8 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice):
|
|||||||
|
|
||||||
self._last_response_time = time()
|
self._last_response_time = time()
|
||||||
|
|
||||||
if self._connection_state == ConnectionState.connecting:
|
if self._connection_state == ConnectionState.Connecting:
|
||||||
self.setConnectionState(ConnectionState.connected)
|
self.setConnectionState(ConnectionState.Connected)
|
||||||
|
|
||||||
callback_key = reply.url().toString() + str(reply.operation())
|
callback_key = reply.url().toString() + str(reply.operation())
|
||||||
try:
|
try:
|
||||||
|
16
cura/PrinterOutput/Peripheral.py
Normal file
16
cura/PrinterOutput/Peripheral.py
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
# Copyright (c) 2019 Ultimaker B.V.
|
||||||
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
|
|
||||||
|
## Data class that represents a peripheral for a printer.
|
||||||
|
#
|
||||||
|
# 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
|
||||||
|
# stage.
|
||||||
|
class Peripheral:
|
||||||
|
## Constructs the peripheral.
|
||||||
|
# \param type A unique ID for the type of peripheral.
|
||||||
|
# \param name A human-readable name for the peripheral.
|
||||||
|
def __init__(self, peripheral_type: str, name: str) -> None:
|
||||||
|
self.type = peripheral_type
|
||||||
|
self.name = name
|
@ -1,150 +1,4 @@
|
|||||||
# Copyright (c) 2017 Ultimaker B.V.
|
import warnings
|
||||||
# Cura is released under the terms of the LGPLv3 or higher.
|
warnings.warn("Importing cura.PrinterOutput.PrintJobOutputModel has been deprecated since 4.1, use cura.PrinterOutput.Models.PrintJobOutputModel instead", DeprecationWarning, stacklevel=2)
|
||||||
|
# We moved the the models to one submodule deeper
|
||||||
from PyQt5.QtCore import pyqtSignal, pyqtProperty, QObject, pyqtSlot
|
from cura.PrinterOutput.Models.PrintJobOutputModel import PrintJobOutputModel
|
||||||
from typing import Optional, TYPE_CHECKING, List
|
|
||||||
|
|
||||||
from PyQt5.QtCore import QUrl
|
|
||||||
from PyQt5.QtGui import QImage
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
from cura.PrinterOutput.PrinterOutputController import PrinterOutputController
|
|
||||||
from cura.PrinterOutput.PrinterOutputModel import PrinterOutputModel
|
|
||||||
from cura.PrinterOutput.ConfigurationModel import ConfigurationModel
|
|
||||||
|
|
||||||
|
|
||||||
class PrintJobOutputModel(QObject):
|
|
||||||
stateChanged = pyqtSignal()
|
|
||||||
timeTotalChanged = pyqtSignal()
|
|
||||||
timeElapsedChanged = pyqtSignal()
|
|
||||||
nameChanged = pyqtSignal()
|
|
||||||
keyChanged = pyqtSignal()
|
|
||||||
assignedPrinterChanged = pyqtSignal()
|
|
||||||
ownerChanged = pyqtSignal()
|
|
||||||
configurationChanged = pyqtSignal()
|
|
||||||
previewImageChanged = pyqtSignal()
|
|
||||||
compatibleMachineFamiliesChanged = pyqtSignal()
|
|
||||||
|
|
||||||
def __init__(self, output_controller: "PrinterOutputController", key: str = "", name: str = "", parent=None) -> None:
|
|
||||||
super().__init__(parent)
|
|
||||||
self._output_controller = output_controller
|
|
||||||
self._state = ""
|
|
||||||
self._time_total = 0
|
|
||||||
self._time_elapsed = 0
|
|
||||||
self._name = name # Human readable name
|
|
||||||
self._key = key # Unique identifier
|
|
||||||
self._assigned_printer = None # type: Optional[PrinterOutputModel]
|
|
||||||
self._owner = "" # Who started/owns the print job?
|
|
||||||
|
|
||||||
self._configuration = None # type: Optional[ConfigurationModel]
|
|
||||||
self._compatible_machine_families = [] # type: List[str]
|
|
||||||
self._preview_image_id = 0
|
|
||||||
|
|
||||||
self._preview_image = None # type: Optional[QImage]
|
|
||||||
|
|
||||||
@pyqtProperty("QStringList", notify=compatibleMachineFamiliesChanged)
|
|
||||||
def compatibleMachineFamilies(self):
|
|
||||||
# Hack; Some versions of cluster will return a family more than once...
|
|
||||||
return set(self._compatible_machine_families)
|
|
||||||
|
|
||||||
def setCompatibleMachineFamilies(self, compatible_machine_families: List[str]) -> None:
|
|
||||||
if self._compatible_machine_families != compatible_machine_families:
|
|
||||||
self._compatible_machine_families = compatible_machine_families
|
|
||||||
self.compatibleMachineFamiliesChanged.emit()
|
|
||||||
|
|
||||||
@pyqtProperty(QUrl, notify=previewImageChanged)
|
|
||||||
def previewImageUrl(self):
|
|
||||||
self._preview_image_id += 1
|
|
||||||
# There is an image provider that is called "camera". In order to ensure that the image qml object, that
|
|
||||||
# requires a QUrl to function, updates correctly we add an increasing number. This causes to see the QUrl
|
|
||||||
# as new (instead of relying on cached version and thus forces an update.
|
|
||||||
temp = "image://print_job_preview/" + str(self._preview_image_id) + "/" + self._key
|
|
||||||
return QUrl(temp, QUrl.TolerantMode)
|
|
||||||
|
|
||||||
def getPreviewImage(self) -> Optional[QImage]:
|
|
||||||
return self._preview_image
|
|
||||||
|
|
||||||
def updatePreviewImage(self, preview_image: Optional[QImage]) -> None:
|
|
||||||
if self._preview_image != preview_image:
|
|
||||||
self._preview_image = preview_image
|
|
||||||
self.previewImageChanged.emit()
|
|
||||||
|
|
||||||
@pyqtProperty(QObject, notify=configurationChanged)
|
|
||||||
def configuration(self) -> Optional["ConfigurationModel"]:
|
|
||||||
return self._configuration
|
|
||||||
|
|
||||||
def updateConfiguration(self, configuration: Optional["ConfigurationModel"]) -> None:
|
|
||||||
if self._configuration != configuration:
|
|
||||||
self._configuration = configuration
|
|
||||||
self.configurationChanged.emit()
|
|
||||||
|
|
||||||
@pyqtProperty(str, notify=ownerChanged)
|
|
||||||
def owner(self):
|
|
||||||
return self._owner
|
|
||||||
|
|
||||||
def updateOwner(self, owner):
|
|
||||||
if self._owner != owner:
|
|
||||||
self._owner = owner
|
|
||||||
self.ownerChanged.emit()
|
|
||||||
|
|
||||||
@pyqtProperty(QObject, notify=assignedPrinterChanged)
|
|
||||||
def assignedPrinter(self):
|
|
||||||
return self._assigned_printer
|
|
||||||
|
|
||||||
def updateAssignedPrinter(self, assigned_printer: "PrinterOutputModel"):
|
|
||||||
if self._assigned_printer != assigned_printer:
|
|
||||||
old_printer = self._assigned_printer
|
|
||||||
self._assigned_printer = assigned_printer
|
|
||||||
if old_printer is not None:
|
|
||||||
# If the previously assigned printer is set, this job is moved away from it.
|
|
||||||
old_printer.updateActivePrintJob(None)
|
|
||||||
self.assignedPrinterChanged.emit()
|
|
||||||
|
|
||||||
@pyqtProperty(str, notify=keyChanged)
|
|
||||||
def key(self):
|
|
||||||
return self._key
|
|
||||||
|
|
||||||
def updateKey(self, key: str):
|
|
||||||
if self._key != key:
|
|
||||||
self._key = key
|
|
||||||
self.keyChanged.emit()
|
|
||||||
|
|
||||||
@pyqtProperty(str, notify = nameChanged)
|
|
||||||
def name(self):
|
|
||||||
return self._name
|
|
||||||
|
|
||||||
def updateName(self, name: str):
|
|
||||||
if self._name != name:
|
|
||||||
self._name = name
|
|
||||||
self.nameChanged.emit()
|
|
||||||
|
|
||||||
@pyqtProperty(int, notify = timeTotalChanged)
|
|
||||||
def timeTotal(self):
|
|
||||||
return self._time_total
|
|
||||||
|
|
||||||
@pyqtProperty(int, notify = timeElapsedChanged)
|
|
||||||
def timeElapsed(self):
|
|
||||||
return self._time_elapsed
|
|
||||||
|
|
||||||
@pyqtProperty(str, notify=stateChanged)
|
|
||||||
def state(self):
|
|
||||||
return self._state
|
|
||||||
|
|
||||||
def updateTimeTotal(self, new_time_total):
|
|
||||||
if self._time_total != new_time_total:
|
|
||||||
self._time_total = new_time_total
|
|
||||||
self.timeTotalChanged.emit()
|
|
||||||
|
|
||||||
def updateTimeElapsed(self, new_time_elapsed):
|
|
||||||
if self._time_elapsed != new_time_elapsed:
|
|
||||||
self._time_elapsed = new_time_elapsed
|
|
||||||
self.timeElapsedChanged.emit()
|
|
||||||
|
|
||||||
def updateState(self, new_state):
|
|
||||||
if self._state != new_state:
|
|
||||||
self._state = new_state
|
|
||||||
self.stateChanged.emit()
|
|
||||||
|
|
||||||
@pyqtSlot(str)
|
|
||||||
def setState(self, state):
|
|
||||||
self._output_controller.setJobState(self, state)
|
|
@ -1,57 +1,66 @@
|
|||||||
# Copyright (c) 2017 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 UM.Logger import Logger
|
from UM.Logger import Logger
|
||||||
|
from UM.Signal import Signal
|
||||||
|
|
||||||
MYPY = False
|
MYPY = False
|
||||||
if MYPY:
|
if MYPY:
|
||||||
from cura.PrinterOutput.PrintJobOutputModel import PrintJobOutputModel
|
from .Models.PrintJobOutputModel import PrintJobOutputModel
|
||||||
from cura.PrinterOutput.ExtruderOutputModel import ExtruderOutputModel
|
from .Models.ExtruderOutputModel import ExtruderOutputModel
|
||||||
from cura.PrinterOutput.PrinterOutputModel import PrinterOutputModel
|
from .Models.PrinterOutputModel import PrinterOutputModel
|
||||||
|
from .PrinterOutputDevice import PrinterOutputDevice
|
||||||
|
|
||||||
|
|
||||||
class PrinterOutputController:
|
class PrinterOutputController:
|
||||||
def __init__(self, output_device):
|
def __init__(self, output_device: "PrinterOutputDevice") -> None:
|
||||||
self.can_pause = True
|
self.can_pause = True
|
||||||
self.can_abort = True
|
self.can_abort = True
|
||||||
self.can_pre_heat_bed = True
|
self.can_pre_heat_bed = True
|
||||||
self.can_pre_heat_hotends = True
|
self.can_pre_heat_hotends = True
|
||||||
self.can_send_raw_gcode = True
|
self.can_send_raw_gcode = True
|
||||||
self.can_control_manually = True
|
self.can_control_manually = True
|
||||||
|
self.can_update_firmware = False
|
||||||
self._output_device = output_device
|
self._output_device = output_device
|
||||||
|
|
||||||
def setTargetHotendTemperature(self, printer: "PrinterOutputModel", extruder: "ExtruderOutputModel", temperature: int):
|
def setTargetHotendTemperature(self, printer: "PrinterOutputModel", position: int, temperature: float) -> None:
|
||||||
Logger.log("w", "Set target hotend temperature not implemented in controller")
|
Logger.log("w", "Set target hotend temperature not implemented in controller")
|
||||||
|
|
||||||
def setTargetBedTemperature(self, printer: "PrinterOutputModel", temperature: int):
|
def setTargetBedTemperature(self, printer: "PrinterOutputModel", temperature: float) -> None:
|
||||||
Logger.log("w", "Set target bed temperature not implemented in controller")
|
Logger.log("w", "Set target bed temperature not implemented in controller")
|
||||||
|
|
||||||
def setJobState(self, job: "PrintJobOutputModel", state: str):
|
def setJobState(self, job: "PrintJobOutputModel", state: str) -> None:
|
||||||
Logger.log("w", "Set job state not implemented in controller")
|
Logger.log("w", "Set job state not implemented in controller")
|
||||||
|
|
||||||
def cancelPreheatBed(self, printer: "PrinterOutputModel"):
|
def cancelPreheatBed(self, printer: "PrinterOutputModel") -> None:
|
||||||
Logger.log("w", "Cancel preheat bed not implemented in controller")
|
Logger.log("w", "Cancel preheat bed not implemented in controller")
|
||||||
|
|
||||||
def preheatBed(self, printer: "PrinterOutputModel", temperature, duration):
|
def preheatBed(self, printer: "PrinterOutputModel", temperature, duration) -> None:
|
||||||
Logger.log("w", "Preheat bed not implemented in controller")
|
Logger.log("w", "Preheat bed not implemented in controller")
|
||||||
|
|
||||||
def cancelPreheatHotend(self, extruder: "ExtruderOutputModel"):
|
def cancelPreheatHotend(self, extruder: "ExtruderOutputModel") -> None:
|
||||||
Logger.log("w", "Cancel preheat hotend not implemented in controller")
|
Logger.log("w", "Cancel preheat hotend not implemented in controller")
|
||||||
|
|
||||||
def preheatHotend(self, extruder: "ExtruderOutputModel", temperature, duration):
|
def preheatHotend(self, extruder: "ExtruderOutputModel", temperature, duration) -> None:
|
||||||
Logger.log("w", "Preheat hotend not implemented in controller")
|
Logger.log("w", "Preheat hotend not implemented in controller")
|
||||||
|
|
||||||
def setHeadPosition(self, printer: "PrinterOutputModel", x, y, z, speed):
|
def setHeadPosition(self, printer: "PrinterOutputModel", x, y, z, speed) -> None:
|
||||||
Logger.log("w", "Set head position not implemented in controller")
|
Logger.log("w", "Set head position not implemented in controller")
|
||||||
|
|
||||||
def moveHead(self, printer: "PrinterOutputModel", x, y, z, speed):
|
def moveHead(self, printer: "PrinterOutputModel", x, y, z, speed) -> None:
|
||||||
Logger.log("w", "Move head not implemented in controller")
|
Logger.log("w", "Move head not implemented in controller")
|
||||||
|
|
||||||
def homeBed(self, printer: "PrinterOutputModel"):
|
def homeBed(self, printer: "PrinterOutputModel") -> None:
|
||||||
Logger.log("w", "Home bed not implemented in controller")
|
Logger.log("w", "Home bed not implemented in controller")
|
||||||
|
|
||||||
def homeHead(self, printer: "PrinterOutputModel"):
|
def homeHead(self, printer: "PrinterOutputModel") -> None:
|
||||||
Logger.log("w", "Home head not implemented in controller")
|
Logger.log("w", "Home head not implemented in controller")
|
||||||
|
|
||||||
def sendRawCommand(self, printer: "PrinterOutputModel", command: str):
|
def sendRawCommand(self, printer: "PrinterOutputModel", command: str) -> None:
|
||||||
Logger.log("w", "Custom command not implemented in controller")
|
Logger.log("w", "Custom command not implemented in controller")
|
||||||
|
|
||||||
|
canUpdateFirmwareChanged = Signal()
|
||||||
|
def setCanUpdateFirmware(self, can_update_firmware: bool) -> None:
|
||||||
|
if can_update_firmware != self.can_update_firmware:
|
||||||
|
self.can_update_firmware = can_update_firmware
|
||||||
|
self.canUpdateFirmwareChanged.emit()
|
261
cura/PrinterOutput/PrinterOutputDevice.py
Normal file
261
cura/PrinterOutput/PrinterOutputDevice.py
Normal file
@ -0,0 +1,261 @@
|
|||||||
|
# Copyright (c) 2018 Ultimaker B.V.
|
||||||
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
from enum import IntEnum
|
||||||
|
from typing import Callable, List, Optional, Union
|
||||||
|
|
||||||
|
from PyQt5.QtCore import pyqtProperty, pyqtSignal, QObject, QTimer, QUrl
|
||||||
|
from PyQt5.QtWidgets import QMessageBox
|
||||||
|
|
||||||
|
from UM.Logger import Logger
|
||||||
|
from UM.Signal import signalemitter
|
||||||
|
from UM.Qt.QtApplication import QtApplication
|
||||||
|
from UM.FlameProfiler import pyqtSlot
|
||||||
|
from UM.Decorators import deprecated
|
||||||
|
from UM.i18n import i18nCatalog
|
||||||
|
from UM.OutputDevice.OutputDevice import OutputDevice
|
||||||
|
|
||||||
|
MYPY = False
|
||||||
|
if MYPY:
|
||||||
|
from UM.FileHandler.FileHandler import FileHandler
|
||||||
|
from UM.Scene.SceneNode import SceneNode
|
||||||
|
from .Models.PrinterOutputModel import PrinterOutputModel
|
||||||
|
from .Models.PrinterConfigurationModel import PrinterConfigurationModel
|
||||||
|
from .FirmwareUpdater import FirmwareUpdater
|
||||||
|
|
||||||
|
i18n_catalog = i18nCatalog("cura")
|
||||||
|
|
||||||
|
|
||||||
|
## The current processing state of the backend.
|
||||||
|
class ConnectionState(IntEnum):
|
||||||
|
Closed = 0
|
||||||
|
Connecting = 1
|
||||||
|
Connected = 2
|
||||||
|
Busy = 3
|
||||||
|
Error = 4
|
||||||
|
|
||||||
|
|
||||||
|
class ConnectionType(IntEnum):
|
||||||
|
NotConnected = 0
|
||||||
|
UsbConnection = 1
|
||||||
|
NetworkConnection = 2
|
||||||
|
CloudConnection = 3
|
||||||
|
|
||||||
|
|
||||||
|
## Printer output device adds extra interface options on top of output device.
|
||||||
|
#
|
||||||
|
# The assumption is made the printer is a FDM printer.
|
||||||
|
#
|
||||||
|
# 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
|
||||||
|
# functions to actually have the implementation.
|
||||||
|
#
|
||||||
|
# For all other uses it should be used in the same way as a "regular" OutputDevice.
|
||||||
|
@signalemitter
|
||||||
|
class PrinterOutputDevice(QObject, OutputDevice):
|
||||||
|
|
||||||
|
printersChanged = pyqtSignal()
|
||||||
|
connectionStateChanged = pyqtSignal(str)
|
||||||
|
acceptsCommandsChanged = pyqtSignal()
|
||||||
|
|
||||||
|
# Signal to indicate that the material of the active printer on the remote changed.
|
||||||
|
materialIdChanged = pyqtSignal()
|
||||||
|
|
||||||
|
# # Signal to indicate that the hotend of the active printer on the remote changed.
|
||||||
|
hotendIdChanged = pyqtSignal()
|
||||||
|
|
||||||
|
# Signal to indicate that the info text about the connection has changed.
|
||||||
|
connectionTextChanged = pyqtSignal()
|
||||||
|
|
||||||
|
# Signal to indicate that the configuration of one of the printers has changed.
|
||||||
|
uniqueConfigurationsChanged = pyqtSignal()
|
||||||
|
|
||||||
|
def __init__(self, device_id: str, connection_type: "ConnectionType" = ConnectionType.NotConnected, parent: QObject = None) -> None:
|
||||||
|
super().__init__(device_id = device_id, parent = parent) # type: ignore # MyPy complains with the multiple inheritance
|
||||||
|
|
||||||
|
self._printers = [] # type: List[PrinterOutputModel]
|
||||||
|
self._unique_configurations = [] # type: List[PrinterConfigurationModel]
|
||||||
|
|
||||||
|
self._monitor_view_qml_path = "" # type: str
|
||||||
|
self._monitor_component = None # type: Optional[QObject]
|
||||||
|
self._monitor_item = None # type: Optional[QObject]
|
||||||
|
|
||||||
|
self._control_view_qml_path = "" # type: str
|
||||||
|
self._control_component = None # type: Optional[QObject]
|
||||||
|
self._control_item = None # type: Optional[QObject]
|
||||||
|
|
||||||
|
self._accepts_commands = False # type: bool
|
||||||
|
|
||||||
|
self._update_timer = QTimer() # type: QTimer
|
||||||
|
self._update_timer.setInterval(2000) # TODO; Add preference for update interval
|
||||||
|
self._update_timer.setSingleShot(False)
|
||||||
|
self._update_timer.timeout.connect(self._update)
|
||||||
|
|
||||||
|
self._connection_state = ConnectionState.Closed # type: ConnectionState
|
||||||
|
self._connection_type = connection_type # type: ConnectionType
|
||||||
|
|
||||||
|
self._firmware_updater = None # type: Optional[FirmwareUpdater]
|
||||||
|
self._firmware_name = None # type: Optional[str]
|
||||||
|
self._address = "" # type: str
|
||||||
|
self._connection_text = "" # type: str
|
||||||
|
self.printersChanged.connect(self._onPrintersChanged)
|
||||||
|
QtApplication.getInstance().getOutputDeviceManager().outputDevicesChanged.connect(self._updateUniqueConfigurations)
|
||||||
|
|
||||||
|
@pyqtProperty(str, notify = connectionTextChanged)
|
||||||
|
def address(self) -> str:
|
||||||
|
return self._address
|
||||||
|
|
||||||
|
def setConnectionText(self, connection_text):
|
||||||
|
if self._connection_text != connection_text:
|
||||||
|
self._connection_text = connection_text
|
||||||
|
self.connectionTextChanged.emit()
|
||||||
|
|
||||||
|
@pyqtProperty(str, constant=True)
|
||||||
|
def connectionText(self) -> str:
|
||||||
|
return self._connection_text
|
||||||
|
|
||||||
|
def materialHotendChangedMessage(self, callback: Callable[[int], None]) -> None:
|
||||||
|
Logger.log("w", "materialHotendChangedMessage needs to be implemented, returning 'Yes'")
|
||||||
|
callback(QMessageBox.Yes)
|
||||||
|
|
||||||
|
def isConnected(self) -> bool:
|
||||||
|
return self._connection_state != ConnectionState.Closed and self._connection_state != ConnectionState.Error
|
||||||
|
|
||||||
|
def setConnectionState(self, connection_state: "ConnectionState") -> None:
|
||||||
|
if self._connection_state != connection_state:
|
||||||
|
self._connection_state = connection_state
|
||||||
|
self.connectionStateChanged.emit(self._id)
|
||||||
|
|
||||||
|
@pyqtProperty(int, constant = True)
|
||||||
|
def connectionType(self) -> "ConnectionType":
|
||||||
|
return self._connection_type
|
||||||
|
|
||||||
|
@pyqtProperty(int, notify = connectionStateChanged)
|
||||||
|
def connectionState(self) -> "ConnectionState":
|
||||||
|
return self._connection_state
|
||||||
|
|
||||||
|
def _update(self) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _getPrinterByKey(self, key: str) -> Optional["PrinterOutputModel"]:
|
||||||
|
for printer in self._printers:
|
||||||
|
if printer.key == key:
|
||||||
|
return printer
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
def requestWrite(self, nodes: List["SceneNode"], file_name: Optional[str] = None, limit_mimetypes: bool = False,
|
||||||
|
file_handler: Optional["FileHandler"] = None, filter_by_machine: bool = False, **kwargs) -> None:
|
||||||
|
raise NotImplementedError("requestWrite needs to be implemented")
|
||||||
|
|
||||||
|
@pyqtProperty(QObject, notify = printersChanged)
|
||||||
|
def activePrinter(self) -> Optional["PrinterOutputModel"]:
|
||||||
|
if len(self._printers):
|
||||||
|
return self._printers[0]
|
||||||
|
return None
|
||||||
|
|
||||||
|
@pyqtProperty("QVariantList", notify = printersChanged)
|
||||||
|
def printers(self) -> List["PrinterOutputModel"]:
|
||||||
|
return self._printers
|
||||||
|
|
||||||
|
@pyqtProperty(QObject, constant = True)
|
||||||
|
def monitorItem(self) -> QObject:
|
||||||
|
# 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
|
||||||
|
# create the item (and fail) every time.
|
||||||
|
if not self._monitor_component:
|
||||||
|
self._createMonitorViewFromQML()
|
||||||
|
return self._monitor_item
|
||||||
|
|
||||||
|
@pyqtProperty(QObject, constant = True)
|
||||||
|
def controlItem(self) -> QObject:
|
||||||
|
if not self._control_component:
|
||||||
|
self._createControlViewFromQML()
|
||||||
|
return self._control_item
|
||||||
|
|
||||||
|
def _createControlViewFromQML(self) -> None:
|
||||||
|
if not self._control_view_qml_path:
|
||||||
|
return
|
||||||
|
if self._control_item is None:
|
||||||
|
self._control_item = QtApplication.getInstance().createQmlComponent(self._control_view_qml_path, {"OutputDevice": self})
|
||||||
|
|
||||||
|
def _createMonitorViewFromQML(self) -> None:
|
||||||
|
if not self._monitor_view_qml_path:
|
||||||
|
return
|
||||||
|
|
||||||
|
if self._monitor_item is None:
|
||||||
|
self._monitor_item = QtApplication.getInstance().createQmlComponent(self._monitor_view_qml_path, {"OutputDevice": self})
|
||||||
|
|
||||||
|
## Attempt to establish connection
|
||||||
|
def connect(self) -> None:
|
||||||
|
self.setConnectionState(ConnectionState.Connecting)
|
||||||
|
self._update_timer.start()
|
||||||
|
|
||||||
|
## Attempt to close the connection
|
||||||
|
def close(self) -> None:
|
||||||
|
self._update_timer.stop()
|
||||||
|
self.setConnectionState(ConnectionState.Closed)
|
||||||
|
|
||||||
|
## Ensure that close gets called when object is destroyed
|
||||||
|
def __del__(self) -> None:
|
||||||
|
self.close()
|
||||||
|
|
||||||
|
@pyqtProperty(bool, notify = acceptsCommandsChanged)
|
||||||
|
def acceptsCommands(self) -> bool:
|
||||||
|
return self._accepts_commands
|
||||||
|
|
||||||
|
@deprecated("Please use the protected function instead", "3.2")
|
||||||
|
def setAcceptsCommands(self, accepts_commands: bool) -> None:
|
||||||
|
self._setAcceptsCommands(accepts_commands)
|
||||||
|
|
||||||
|
## Set a flag to signal the UI that the printer is not (yet) ready to receive commands
|
||||||
|
def _setAcceptsCommands(self, accepts_commands: bool) -> None:
|
||||||
|
if self._accepts_commands != accepts_commands:
|
||||||
|
self._accepts_commands = accepts_commands
|
||||||
|
|
||||||
|
self.acceptsCommandsChanged.emit()
|
||||||
|
|
||||||
|
# Returns the unique configurations of the printers within this output device
|
||||||
|
@pyqtProperty("QVariantList", notify = uniqueConfigurationsChanged)
|
||||||
|
def uniqueConfigurations(self) -> List["PrinterConfigurationModel"]:
|
||||||
|
return self._unique_configurations
|
||||||
|
|
||||||
|
def _updateUniqueConfigurations(self) -> None:
|
||||||
|
self._unique_configurations = sorted(
|
||||||
|
{printer.printerConfiguration for printer in self._printers if printer.printerConfiguration is not None},
|
||||||
|
key=lambda config: config.printerType,
|
||||||
|
)
|
||||||
|
self.uniqueConfigurationsChanged.emit()
|
||||||
|
|
||||||
|
# Returns the unique configurations of the printers within this output device
|
||||||
|
@pyqtProperty("QStringList", notify = uniqueConfigurationsChanged)
|
||||||
|
def uniquePrinterTypes(self) -> List[str]:
|
||||||
|
return list(sorted(set([configuration.printerType for configuration in self._unique_configurations])))
|
||||||
|
|
||||||
|
def _onPrintersChanged(self) -> None:
|
||||||
|
for printer in self._printers:
|
||||||
|
printer.configurationChanged.connect(self._updateUniqueConfigurations)
|
||||||
|
|
||||||
|
# At this point there may be non-updated configurations
|
||||||
|
self._updateUniqueConfigurations()
|
||||||
|
|
||||||
|
## Set the device firmware name
|
||||||
|
#
|
||||||
|
# \param name The name of the firmware.
|
||||||
|
def _setFirmwareName(self, name: str) -> None:
|
||||||
|
self._firmware_name = name
|
||||||
|
|
||||||
|
## Get the name of device firmware
|
||||||
|
#
|
||||||
|
# This name can be used to define device type
|
||||||
|
def getFirmwareName(self) -> Optional[str]:
|
||||||
|
return self._firmware_name
|
||||||
|
|
||||||
|
def getFirmwareUpdater(self) -> Optional["FirmwareUpdater"]:
|
||||||
|
return self._firmware_updater
|
||||||
|
|
||||||
|
@pyqtSlot(str)
|
||||||
|
def updateFirmware(self, firmware_file: Union[str, QUrl]) -> None:
|
||||||
|
if not self._firmware_updater:
|
||||||
|
return
|
||||||
|
|
||||||
|
self._firmware_updater.updateFirmware(firmware_file)
|
@ -1,285 +1,4 @@
|
|||||||
# Copyright (c) 2018 Ultimaker B.V.
|
import warnings
|
||||||
# Cura is released under the terms of the LGPLv3 or higher.
|
warnings.warn("Importing cura.PrinterOutput.PrinterOutputModel has been deprecated since 4.1, use cura.PrinterOutput.Models.PrinterOutputModel instead", DeprecationWarning, stacklevel=2)
|
||||||
|
# We moved the the models to one submodule deeper
|
||||||
from PyQt5.QtCore import pyqtSignal, pyqtProperty, QObject, QVariant, pyqtSlot
|
from cura.PrinterOutput.Models.PrinterOutputModel import PrinterOutputModel
|
||||||
from typing import Optional
|
|
||||||
from UM.Math.Vector import Vector
|
|
||||||
from cura.PrinterOutput.ConfigurationModel import ConfigurationModel
|
|
||||||
from cura.PrinterOutput.ExtruderOutputModel import ExtruderOutputModel
|
|
||||||
|
|
||||||
MYPY = False
|
|
||||||
if MYPY:
|
|
||||||
from cura.PrinterOutput.PrintJobOutputModel import PrintJobOutputModel
|
|
||||||
from cura.PrinterOutput.PrinterOutputController import PrinterOutputController
|
|
||||||
|
|
||||||
|
|
||||||
class PrinterOutputModel(QObject):
|
|
||||||
bedTemperatureChanged = pyqtSignal()
|
|
||||||
targetBedTemperatureChanged = pyqtSignal()
|
|
||||||
isPreheatingChanged = pyqtSignal()
|
|
||||||
stateChanged = pyqtSignal()
|
|
||||||
activePrintJobChanged = pyqtSignal()
|
|
||||||
nameChanged = pyqtSignal()
|
|
||||||
headPositionChanged = pyqtSignal()
|
|
||||||
keyChanged = pyqtSignal()
|
|
||||||
printerTypeChanged = pyqtSignal()
|
|
||||||
buildplateChanged = pyqtSignal()
|
|
||||||
cameraChanged = pyqtSignal()
|
|
||||||
configurationChanged = pyqtSignal()
|
|
||||||
|
|
||||||
def __init__(self, output_controller: "PrinterOutputController", number_of_extruders: int = 1, parent=None, firmware_version = "") -> None:
|
|
||||||
super().__init__(parent)
|
|
||||||
self._bed_temperature = -1 # Use -1 for no heated bed.
|
|
||||||
self._target_bed_temperature = 0
|
|
||||||
self._name = ""
|
|
||||||
self._key = "" # Unique identifier
|
|
||||||
self._controller = output_controller
|
|
||||||
self._extruders = [ExtruderOutputModel(printer = self, position = i) for i in range(number_of_extruders)]
|
|
||||||
self._printer_configuration = ConfigurationModel() # Indicates the current configuration setup in this printer
|
|
||||||
self._head_position = Vector(0, 0, 0)
|
|
||||||
self._active_print_job = None # type: Optional[PrintJobOutputModel]
|
|
||||||
self._firmware_version = firmware_version
|
|
||||||
self._printer_state = "unknown"
|
|
||||||
self._is_preheating = False
|
|
||||||
self._printer_type = ""
|
|
||||||
self._buildplate_name = None
|
|
||||||
|
|
||||||
self._printer_configuration.extruderConfigurations = [extruder.extruderConfiguration for extruder in
|
|
||||||
self._extruders]
|
|
||||||
|
|
||||||
self._camera = None
|
|
||||||
|
|
||||||
@pyqtProperty(str, constant = True)
|
|
||||||
def firmwareVersion(self):
|
|
||||||
return self._firmware_version
|
|
||||||
|
|
||||||
def setCamera(self, camera):
|
|
||||||
if self._camera is not camera:
|
|
||||||
self._camera = camera
|
|
||||||
self.cameraChanged.emit()
|
|
||||||
|
|
||||||
def updateIsPreheating(self, pre_heating):
|
|
||||||
if self._is_preheating != pre_heating:
|
|
||||||
self._is_preheating = pre_heating
|
|
||||||
self.isPreheatingChanged.emit()
|
|
||||||
|
|
||||||
@pyqtProperty(bool, notify=isPreheatingChanged)
|
|
||||||
def isPreheating(self):
|
|
||||||
return self._is_preheating
|
|
||||||
|
|
||||||
@pyqtProperty(QObject, notify=cameraChanged)
|
|
||||||
def camera(self):
|
|
||||||
return self._camera
|
|
||||||
|
|
||||||
@pyqtProperty(str, notify = printerTypeChanged)
|
|
||||||
def type(self):
|
|
||||||
return self._printer_type
|
|
||||||
|
|
||||||
def updateType(self, printer_type):
|
|
||||||
if self._printer_type != printer_type:
|
|
||||||
self._printer_type = printer_type
|
|
||||||
self._printer_configuration.printerType = self._printer_type
|
|
||||||
self.printerTypeChanged.emit()
|
|
||||||
self.configurationChanged.emit()
|
|
||||||
|
|
||||||
@pyqtProperty(str, notify = buildplateChanged)
|
|
||||||
def buildplate(self):
|
|
||||||
return self._buildplate_name
|
|
||||||
|
|
||||||
def updateBuildplateName(self, buildplate_name):
|
|
||||||
if self._buildplate_name != buildplate_name:
|
|
||||||
self._buildplate_name = buildplate_name
|
|
||||||
self._printer_configuration.buildplateConfiguration = self._buildplate_name
|
|
||||||
self.buildplateChanged.emit()
|
|
||||||
self.configurationChanged.emit()
|
|
||||||
|
|
||||||
@pyqtProperty(str, notify=keyChanged)
|
|
||||||
def key(self):
|
|
||||||
return self._key
|
|
||||||
|
|
||||||
def updateKey(self, key: str):
|
|
||||||
if self._key != key:
|
|
||||||
self._key = key
|
|
||||||
self.keyChanged.emit()
|
|
||||||
|
|
||||||
@pyqtSlot()
|
|
||||||
def homeHead(self):
|
|
||||||
self._controller.homeHead(self)
|
|
||||||
|
|
||||||
@pyqtSlot()
|
|
||||||
def homeBed(self):
|
|
||||||
self._controller.homeBed(self)
|
|
||||||
|
|
||||||
@pyqtSlot(str)
|
|
||||||
def sendRawCommand(self, command: str):
|
|
||||||
self._controller.sendRawCommand(self, command)
|
|
||||||
|
|
||||||
@pyqtProperty("QVariantList", constant = True)
|
|
||||||
def extruders(self):
|
|
||||||
return self._extruders
|
|
||||||
|
|
||||||
@pyqtProperty(QVariant, notify = headPositionChanged)
|
|
||||||
def headPosition(self):
|
|
||||||
return {"x": self._head_position.x, "y": self._head_position.y, "z": self.head_position.z}
|
|
||||||
|
|
||||||
def updateHeadPosition(self, x, y, z):
|
|
||||||
if self._head_position.x != x or self._head_position.y != y or self._head_position.z != z:
|
|
||||||
self._head_position = Vector(x, y, z)
|
|
||||||
self.headPositionChanged.emit()
|
|
||||||
|
|
||||||
@pyqtProperty(float, float, float)
|
|
||||||
@pyqtProperty(float, float, float, float)
|
|
||||||
def setHeadPosition(self, x, y, z, speed = 3000):
|
|
||||||
self.updateHeadPosition(x, y, z)
|
|
||||||
self._controller.setHeadPosition(self, x, y, z, speed)
|
|
||||||
|
|
||||||
@pyqtProperty(float)
|
|
||||||
@pyqtProperty(float, float)
|
|
||||||
def setHeadX(self, x, speed = 3000):
|
|
||||||
self.updateHeadPosition(x, self._head_position.y, self._head_position.z)
|
|
||||||
self._controller.setHeadPosition(self, x, self._head_position.y, self._head_position.z, speed)
|
|
||||||
|
|
||||||
@pyqtProperty(float)
|
|
||||||
@pyqtProperty(float, float)
|
|
||||||
def setHeadY(self, y, speed = 3000):
|
|
||||||
self.updateHeadPosition(self._head_position.x, y, self._head_position.z)
|
|
||||||
self._controller.setHeadPosition(self, self._head_position.x, y, self._head_position.z, speed)
|
|
||||||
|
|
||||||
@pyqtProperty(float)
|
|
||||||
@pyqtProperty(float, float)
|
|
||||||
def setHeadZ(self, z, speed = 3000):
|
|
||||||
self.updateHeadPosition(self._head_position.x, self._head_position.y, z)
|
|
||||||
self._controller.setHeadPosition(self, self._head_position.x, self._head_position.y, z, speed)
|
|
||||||
|
|
||||||
@pyqtSlot(float, float, float)
|
|
||||||
@pyqtSlot(float, float, float, float)
|
|
||||||
def moveHead(self, x = 0, y = 0, z = 0, speed = 3000):
|
|
||||||
self._controller.moveHead(self, x, y, z, speed)
|
|
||||||
|
|
||||||
## Pre-heats the heated bed of the printer.
|
|
||||||
#
|
|
||||||
# \param temperature The temperature to heat the bed to, in degrees
|
|
||||||
# Celsius.
|
|
||||||
# \param duration How long the bed should stay warm, in seconds.
|
|
||||||
@pyqtSlot(float, float)
|
|
||||||
def preheatBed(self, temperature, duration):
|
|
||||||
self._controller.preheatBed(self, temperature, duration)
|
|
||||||
|
|
||||||
@pyqtSlot()
|
|
||||||
def cancelPreheatBed(self):
|
|
||||||
self._controller.cancelPreheatBed(self)
|
|
||||||
|
|
||||||
def getController(self):
|
|
||||||
return self._controller
|
|
||||||
|
|
||||||
@pyqtProperty(str, notify=nameChanged)
|
|
||||||
def name(self):
|
|
||||||
return self._name
|
|
||||||
|
|
||||||
def setName(self, name):
|
|
||||||
self._setName(name)
|
|
||||||
self.updateName(name)
|
|
||||||
|
|
||||||
def updateName(self, name):
|
|
||||||
if self._name != name:
|
|
||||||
self._name = name
|
|
||||||
self.nameChanged.emit()
|
|
||||||
|
|
||||||
## Update the bed temperature. This only changes it locally.
|
|
||||||
def updateBedTemperature(self, temperature):
|
|
||||||
if self._bed_temperature != temperature:
|
|
||||||
self._bed_temperature = temperature
|
|
||||||
self.bedTemperatureChanged.emit()
|
|
||||||
|
|
||||||
def updateTargetBedTemperature(self, temperature):
|
|
||||||
if self._target_bed_temperature != temperature:
|
|
||||||
self._target_bed_temperature = temperature
|
|
||||||
self.targetBedTemperatureChanged.emit()
|
|
||||||
|
|
||||||
## Set the target bed temperature. This ensures that it's actually sent to the remote.
|
|
||||||
@pyqtSlot(int)
|
|
||||||
def setTargetBedTemperature(self, temperature):
|
|
||||||
self._controller.setTargetBedTemperature(self, temperature)
|
|
||||||
self.updateTargetBedTemperature(temperature)
|
|
||||||
|
|
||||||
def updateActivePrintJob(self, print_job):
|
|
||||||
if self._active_print_job != print_job:
|
|
||||||
old_print_job = self._active_print_job
|
|
||||||
|
|
||||||
if print_job is not None:
|
|
||||||
print_job.updateAssignedPrinter(self)
|
|
||||||
self._active_print_job = print_job
|
|
||||||
|
|
||||||
if old_print_job is not None:
|
|
||||||
old_print_job.updateAssignedPrinter(None)
|
|
||||||
self.activePrintJobChanged.emit()
|
|
||||||
|
|
||||||
def updateState(self, printer_state):
|
|
||||||
if self._printer_state != printer_state:
|
|
||||||
self._printer_state = printer_state
|
|
||||||
self.stateChanged.emit()
|
|
||||||
|
|
||||||
@pyqtProperty(QObject, notify = activePrintJobChanged)
|
|
||||||
def activePrintJob(self):
|
|
||||||
return self._active_print_job
|
|
||||||
|
|
||||||
@pyqtProperty(str, notify=stateChanged)
|
|
||||||
def state(self):
|
|
||||||
return self._printer_state
|
|
||||||
|
|
||||||
@pyqtProperty(int, notify = bedTemperatureChanged)
|
|
||||||
def bedTemperature(self):
|
|
||||||
return self._bed_temperature
|
|
||||||
|
|
||||||
@pyqtProperty(int, notify=targetBedTemperatureChanged)
|
|
||||||
def targetBedTemperature(self):
|
|
||||||
return self._target_bed_temperature
|
|
||||||
|
|
||||||
# Does the printer support pre-heating the bed at all
|
|
||||||
@pyqtProperty(bool, constant=True)
|
|
||||||
def canPreHeatBed(self):
|
|
||||||
if self._controller:
|
|
||||||
return self._controller.can_pre_heat_bed
|
|
||||||
return False
|
|
||||||
|
|
||||||
# Does the printer support pre-heating the bed at all
|
|
||||||
@pyqtProperty(bool, constant=True)
|
|
||||||
def canPreHeatHotends(self):
|
|
||||||
if self._controller:
|
|
||||||
return self._controller.can_pre_heat_hotends
|
|
||||||
return False
|
|
||||||
|
|
||||||
# Does the printer support sending raw G-code at all
|
|
||||||
@pyqtProperty(bool, constant=True)
|
|
||||||
def canSendRawGcode(self):
|
|
||||||
if self._controller:
|
|
||||||
return self._controller.can_send_raw_gcode
|
|
||||||
return False
|
|
||||||
|
|
||||||
# Does the printer support pause at all
|
|
||||||
@pyqtProperty(bool, constant=True)
|
|
||||||
def canPause(self):
|
|
||||||
if self._controller:
|
|
||||||
return self._controller.can_pause
|
|
||||||
return False
|
|
||||||
|
|
||||||
# Does the printer support abort at all
|
|
||||||
@pyqtProperty(bool, constant=True)
|
|
||||||
def canAbort(self):
|
|
||||||
if self._controller:
|
|
||||||
return self._controller.can_abort
|
|
||||||
return False
|
|
||||||
|
|
||||||
# Does the printer support manual control at all
|
|
||||||
@pyqtProperty(bool, constant=True)
|
|
||||||
def canControlManually(self):
|
|
||||||
if self._controller:
|
|
||||||
return self._controller.can_control_manually
|
|
||||||
return False
|
|
||||||
|
|
||||||
# Returns the configuration (material, variant and buildplate) of the current printer
|
|
||||||
@pyqtProperty(QObject, notify = configurationChanged)
|
|
||||||
def printerConfiguration(self):
|
|
||||||
if self._printer_configuration.isValid():
|
|
||||||
return self._printer_configuration
|
|
||||||
return None
|
|
@ -1,228 +1,4 @@
|
|||||||
# Copyright (c) 2018 Ultimaker B.V.
|
import warnings
|
||||||
# Cura is released under the terms of the LGPLv3 or higher.
|
warnings.warn("Importing cura.PrinterOutputDevice has been deprecated since 4.1, use cura.PrinterOutput.PrinterOutputDevice instead", DeprecationWarning, stacklevel=2)
|
||||||
|
# We moved the PrinterOutput device to it's own submodule.
|
||||||
from UM.Decorators import deprecated
|
from cura.PrinterOutput.PrinterOutputDevice import PrinterOutputDevice, ConnectionState
|
||||||
from UM.i18n import i18nCatalog
|
|
||||||
from UM.OutputDevice.OutputDevice import OutputDevice
|
|
||||||
from PyQt5.QtCore import pyqtProperty, QObject, QTimer, pyqtSignal
|
|
||||||
from PyQt5.QtWidgets import QMessageBox
|
|
||||||
|
|
||||||
from UM.Logger import Logger
|
|
||||||
from UM.FileHandler.FileHandler import FileHandler #For typing.
|
|
||||||
from UM.Scene.SceneNode import SceneNode #For typing.
|
|
||||||
from UM.Signal import signalemitter
|
|
||||||
from UM.Qt.QtApplication import QtApplication
|
|
||||||
|
|
||||||
from enum import IntEnum # For the connection state tracking.
|
|
||||||
from typing import Callable, List, Optional
|
|
||||||
|
|
||||||
MYPY = False
|
|
||||||
if MYPY:
|
|
||||||
from cura.PrinterOutput.PrinterOutputModel import PrinterOutputModel
|
|
||||||
from cura.PrinterOutput.ConfigurationModel import ConfigurationModel
|
|
||||||
|
|
||||||
i18n_catalog = i18nCatalog("cura")
|
|
||||||
|
|
||||||
|
|
||||||
## The current processing state of the backend.
|
|
||||||
class ConnectionState(IntEnum):
|
|
||||||
closed = 0
|
|
||||||
connecting = 1
|
|
||||||
connected = 2
|
|
||||||
busy = 3
|
|
||||||
error = 4
|
|
||||||
|
|
||||||
|
|
||||||
## Printer output device adds extra interface options on top of output device.
|
|
||||||
#
|
|
||||||
# The assumption is made the printer is a FDM printer.
|
|
||||||
#
|
|
||||||
# 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
|
|
||||||
# functions to actually have the implementation.
|
|
||||||
#
|
|
||||||
# For all other uses it should be used in the same way as a "regular" OutputDevice.
|
|
||||||
@signalemitter
|
|
||||||
class PrinterOutputDevice(QObject, OutputDevice):
|
|
||||||
printersChanged = pyqtSignal()
|
|
||||||
connectionStateChanged = pyqtSignal(str)
|
|
||||||
acceptsCommandsChanged = pyqtSignal()
|
|
||||||
|
|
||||||
# Signal to indicate that the material of the active printer on the remote changed.
|
|
||||||
materialIdChanged = pyqtSignal()
|
|
||||||
|
|
||||||
# # Signal to indicate that the hotend of the active printer on the remote changed.
|
|
||||||
hotendIdChanged = pyqtSignal()
|
|
||||||
|
|
||||||
# Signal to indicate that the info text about the connection has changed.
|
|
||||||
connectionTextChanged = pyqtSignal()
|
|
||||||
|
|
||||||
# Signal to indicate that the configuration of one of the printers has changed.
|
|
||||||
uniqueConfigurationsChanged = pyqtSignal()
|
|
||||||
|
|
||||||
def __init__(self, device_id: str, parent: QObject = None) -> None:
|
|
||||||
super().__init__(device_id = device_id, parent = parent) # type: ignore # MyPy complains with the multiple inheritance
|
|
||||||
|
|
||||||
self._printers = [] # type: List[PrinterOutputModel]
|
|
||||||
self._unique_configurations = [] # type: List[ConfigurationModel]
|
|
||||||
|
|
||||||
self._monitor_view_qml_path = "" #type: str
|
|
||||||
self._monitor_component = None #type: Optional[QObject]
|
|
||||||
self._monitor_item = None #type: Optional[QObject]
|
|
||||||
|
|
||||||
self._control_view_qml_path = "" #type: str
|
|
||||||
self._control_component = None #type: Optional[QObject]
|
|
||||||
self._control_item = None #type: Optional[QObject]
|
|
||||||
|
|
||||||
self._accepts_commands = False #type: bool
|
|
||||||
|
|
||||||
self._update_timer = QTimer() #type: QTimer
|
|
||||||
self._update_timer.setInterval(2000) # TODO; Add preference for update interval
|
|
||||||
self._update_timer.setSingleShot(False)
|
|
||||||
self._update_timer.timeout.connect(self._update)
|
|
||||||
|
|
||||||
self._connection_state = ConnectionState.closed #type: ConnectionState
|
|
||||||
|
|
||||||
self._firmware_name = None #type: Optional[str]
|
|
||||||
self._address = "" #type: str
|
|
||||||
self._connection_text = "" #type: str
|
|
||||||
self.printersChanged.connect(self._onPrintersChanged)
|
|
||||||
QtApplication.getInstance().getOutputDeviceManager().outputDevicesChanged.connect(self._updateUniqueConfigurations)
|
|
||||||
|
|
||||||
@pyqtProperty(str, notify = connectionTextChanged)
|
|
||||||
def address(self) -> str:
|
|
||||||
return self._address
|
|
||||||
|
|
||||||
def setConnectionText(self, connection_text):
|
|
||||||
if self._connection_text != connection_text:
|
|
||||||
self._connection_text = connection_text
|
|
||||||
self.connectionTextChanged.emit()
|
|
||||||
|
|
||||||
@pyqtProperty(str, constant=True)
|
|
||||||
def connectionText(self) -> str:
|
|
||||||
return self._connection_text
|
|
||||||
|
|
||||||
def materialHotendChangedMessage(self, callback: Callable[[int], None]) -> None:
|
|
||||||
Logger.log("w", "materialHotendChangedMessage needs to be implemented, returning 'Yes'")
|
|
||||||
callback(QMessageBox.Yes)
|
|
||||||
|
|
||||||
def isConnected(self) -> bool:
|
|
||||||
return self._connection_state != ConnectionState.closed and self._connection_state != ConnectionState.error
|
|
||||||
|
|
||||||
def setConnectionState(self, connection_state: ConnectionState) -> None:
|
|
||||||
if self._connection_state != connection_state:
|
|
||||||
self._connection_state = connection_state
|
|
||||||
self.connectionStateChanged.emit(self._id)
|
|
||||||
|
|
||||||
@pyqtProperty(str, notify = connectionStateChanged)
|
|
||||||
def connectionState(self) -> ConnectionState:
|
|
||||||
return self._connection_state
|
|
||||||
|
|
||||||
def _update(self) -> None:
|
|
||||||
pass
|
|
||||||
|
|
||||||
def _getPrinterByKey(self, key: str) -> Optional["PrinterOutputModel"]:
|
|
||||||
for printer in self._printers:
|
|
||||||
if printer.key == key:
|
|
||||||
return printer
|
|
||||||
|
|
||||||
return None
|
|
||||||
|
|
||||||
def requestWrite(self, nodes: List[SceneNode], file_name: Optional[str] = None, limit_mimetypes: bool = False, file_handler: Optional[FileHandler] = None, **kwargs: str) -> None:
|
|
||||||
raise NotImplementedError("requestWrite needs to be implemented")
|
|
||||||
|
|
||||||
@pyqtProperty(QObject, notify = printersChanged)
|
|
||||||
def activePrinter(self) -> Optional["PrinterOutputModel"]:
|
|
||||||
if len(self._printers):
|
|
||||||
return self._printers[0]
|
|
||||||
return None
|
|
||||||
|
|
||||||
@pyqtProperty("QVariantList", notify = printersChanged)
|
|
||||||
def printers(self) -> List["PrinterOutputModel"]:
|
|
||||||
return self._printers
|
|
||||||
|
|
||||||
@pyqtProperty(QObject, constant = True)
|
|
||||||
def monitorItem(self) -> QObject:
|
|
||||||
# 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
|
|
||||||
# create the item (and fail) every time.
|
|
||||||
if not self._monitor_component:
|
|
||||||
self._createMonitorViewFromQML()
|
|
||||||
return self._monitor_item
|
|
||||||
|
|
||||||
@pyqtProperty(QObject, constant = True)
|
|
||||||
def controlItem(self) -> QObject:
|
|
||||||
if not self._control_component:
|
|
||||||
self._createControlViewFromQML()
|
|
||||||
return self._control_item
|
|
||||||
|
|
||||||
def _createControlViewFromQML(self) -> None:
|
|
||||||
if not self._control_view_qml_path:
|
|
||||||
return
|
|
||||||
if self._control_item is None:
|
|
||||||
self._control_item = QtApplication.getInstance().createQmlComponent(self._control_view_qml_path, {"OutputDevice": self})
|
|
||||||
|
|
||||||
def _createMonitorViewFromQML(self) -> None:
|
|
||||||
if not self._monitor_view_qml_path:
|
|
||||||
return
|
|
||||||
|
|
||||||
if self._monitor_item is None:
|
|
||||||
self._monitor_item = QtApplication.getInstance().createQmlComponent(self._monitor_view_qml_path, {"OutputDevice": self})
|
|
||||||
|
|
||||||
## Attempt to establish connection
|
|
||||||
def connect(self) -> None:
|
|
||||||
self.setConnectionState(ConnectionState.connecting)
|
|
||||||
self._update_timer.start()
|
|
||||||
|
|
||||||
## Attempt to close the connection
|
|
||||||
def close(self) -> None:
|
|
||||||
self._update_timer.stop()
|
|
||||||
self.setConnectionState(ConnectionState.closed)
|
|
||||||
|
|
||||||
## Ensure that close gets called when object is destroyed
|
|
||||||
def __del__(self) -> None:
|
|
||||||
self.close()
|
|
||||||
|
|
||||||
@pyqtProperty(bool, notify = acceptsCommandsChanged)
|
|
||||||
def acceptsCommands(self) -> bool:
|
|
||||||
return self._accepts_commands
|
|
||||||
|
|
||||||
@deprecated("Please use the protected function instead", "3.2")
|
|
||||||
def setAcceptsCommands(self, accepts_commands: bool) -> None:
|
|
||||||
self._setAcceptsCommands(accepts_commands)
|
|
||||||
|
|
||||||
## Set a flag to signal the UI that the printer is not (yet) ready to receive commands
|
|
||||||
def _setAcceptsCommands(self, accepts_commands: bool) -> None:
|
|
||||||
if self._accepts_commands != accepts_commands:
|
|
||||||
self._accepts_commands = accepts_commands
|
|
||||||
|
|
||||||
self.acceptsCommandsChanged.emit()
|
|
||||||
|
|
||||||
# Returns the unique configurations of the printers within this output device
|
|
||||||
@pyqtProperty("QVariantList", notify = uniqueConfigurationsChanged)
|
|
||||||
def uniqueConfigurations(self) -> List["ConfigurationModel"]:
|
|
||||||
return self._unique_configurations
|
|
||||||
|
|
||||||
def _updateUniqueConfigurations(self) -> None:
|
|
||||||
self._unique_configurations = list(set([printer.printerConfiguration for printer in self._printers if printer.printerConfiguration is not None]))
|
|
||||||
self._unique_configurations.sort(key = lambda k: k.printerType)
|
|
||||||
self.uniqueConfigurationsChanged.emit()
|
|
||||||
|
|
||||||
def _onPrintersChanged(self) -> None:
|
|
||||||
for printer in self._printers:
|
|
||||||
printer.configurationChanged.connect(self._updateUniqueConfigurations)
|
|
||||||
|
|
||||||
# At this point there may be non-updated configurations
|
|
||||||
self._updateUniqueConfigurations()
|
|
||||||
|
|
||||||
## Set the device firmware name
|
|
||||||
#
|
|
||||||
# \param name The name of the firmware.
|
|
||||||
def _setFirmwareName(self, name: str) -> None:
|
|
||||||
self._firmware_name = name
|
|
||||||
|
|
||||||
## Get the name of device firmware
|
|
||||||
#
|
|
||||||
# This name can be used to define device type
|
|
||||||
def getFirmwareName(self) -> Optional[str]:
|
|
||||||
return self._firmware_name
|
|
@ -1,9 +1,12 @@
|
|||||||
|
# Copyright (c) 2018 Ultimaker B.V.
|
||||||
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
from UM.Scene.SceneNodeDecorator import SceneNodeDecorator
|
from UM.Scene.SceneNodeDecorator import SceneNodeDecorator
|
||||||
|
|
||||||
|
|
||||||
class BlockSlicingDecorator(SceneNodeDecorator):
|
class BlockSlicingDecorator(SceneNodeDecorator):
|
||||||
def __init__(self):
|
def __init__(self) -> None:
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
|
||||||
def isBlockSlicing(self):
|
def isBlockSlicing(self) -> bool:
|
||||||
return True
|
return True
|
||||||
|
@ -60,13 +60,15 @@ class ConvexHullDecorator(SceneNodeDecorator):
|
|||||||
previous_node = self._node
|
previous_node = self._node
|
||||||
# Disconnect from previous node signals
|
# Disconnect from previous node signals
|
||||||
if previous_node is not None and node is not previous_node:
|
if previous_node is not None and node is not previous_node:
|
||||||
previous_node.transformationChanged.disconnect(self._onChanged)
|
previous_node.boundingBoxChanged.disconnect(self._onChanged)
|
||||||
previous_node.parentChanged.disconnect(self._onChanged)
|
|
||||||
|
|
||||||
super().setNode(node)
|
super().setNode(node)
|
||||||
# Mypy doesn't understand that self._node is no longer optional, so just use the node.
|
|
||||||
node.transformationChanged.connect(self._onChanged)
|
node.boundingBoxChanged.connect(self._onChanged)
|
||||||
node.parentChanged.connect(self._onChanged)
|
|
||||||
|
per_object_stack = node.callDecoration("getStack")
|
||||||
|
if per_object_stack:
|
||||||
|
per_object_stack.propertyChanged.connect(self._onSettingValueChanged)
|
||||||
|
|
||||||
self._onChanged()
|
self._onChanged()
|
||||||
|
|
||||||
@ -78,7 +80,8 @@ class ConvexHullDecorator(SceneNodeDecorator):
|
|||||||
def getConvexHull(self) -> Optional[Polygon]:
|
def getConvexHull(self) -> Optional[Polygon]:
|
||||||
if self._node is None:
|
if self._node is None:
|
||||||
return None
|
return None
|
||||||
|
if self._node.callDecoration("isNonPrintingMesh"):
|
||||||
|
return None
|
||||||
hull = self._compute2DConvexHull()
|
hull = self._compute2DConvexHull()
|
||||||
|
|
||||||
if self._global_stack and self._node is not None and hull is not None:
|
if self._global_stack and self._node is not None and hull is not None:
|
||||||
@ -108,7 +111,8 @@ class ConvexHullDecorator(SceneNodeDecorator):
|
|||||||
def getConvexHullHead(self) -> Optional[Polygon]:
|
def getConvexHullHead(self) -> Optional[Polygon]:
|
||||||
if self._node is None:
|
if self._node is None:
|
||||||
return None
|
return None
|
||||||
|
if self._node.callDecoration("isNonPrintingMesh"):
|
||||||
|
return None
|
||||||
if self._global_stack:
|
if self._global_stack:
|
||||||
if self._global_stack.getProperty("print_sequence", "value") == "one_at_a_time" and not self.hasGroupAsParent(self._node):
|
if self._global_stack.getProperty("print_sequence", "value") == "one_at_a_time" and not self.hasGroupAsParent(self._node):
|
||||||
head_with_fans = self._compute2DConvexHeadMin()
|
head_with_fans = self._compute2DConvexHeadMin()
|
||||||
@ -125,6 +129,9 @@ class ConvexHullDecorator(SceneNodeDecorator):
|
|||||||
if self._node is None:
|
if self._node is None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
if self._node.callDecoration("isNonPrintingMesh"):
|
||||||
|
return None
|
||||||
|
|
||||||
if self._global_stack:
|
if self._global_stack:
|
||||||
if self._global_stack.getProperty("print_sequence", "value") == "one_at_a_time" and not self.hasGroupAsParent(self._node):
|
if self._global_stack.getProperty("print_sequence", "value") == "one_at_a_time" and not self.hasGroupAsParent(self._node):
|
||||||
# Printing one at a time and it's not an object in a group
|
# Printing one at a time and it's not an object in a group
|
||||||
@ -142,6 +149,12 @@ class ConvexHullDecorator(SceneNodeDecorator):
|
|||||||
controller = Application.getInstance().getController()
|
controller = Application.getInstance().getController()
|
||||||
root = controller.getScene().getRoot()
|
root = controller.getScene().getRoot()
|
||||||
if self._node is None or controller.isToolOperationActive() or not self.__isDescendant(root, self._node):
|
if self._node is None or controller.isToolOperationActive() or not self.__isDescendant(root, self._node):
|
||||||
|
# If the tool operation is still active, we need to compute the convex hull later after the controller is
|
||||||
|
# no longer active.
|
||||||
|
if controller.isToolOperationActive():
|
||||||
|
self.recomputeConvexHullDelayed()
|
||||||
|
return
|
||||||
|
|
||||||
if self._convex_hull_node:
|
if self._convex_hull_node:
|
||||||
self._convex_hull_node.setParent(None)
|
self._convex_hull_node.setParent(None)
|
||||||
self._convex_hull_node = None
|
self._convex_hull_node = None
|
||||||
@ -181,7 +194,10 @@ class ConvexHullDecorator(SceneNodeDecorator):
|
|||||||
for child in self._node.getChildren():
|
for child in self._node.getChildren():
|
||||||
child_hull = child.callDecoration("_compute2DConvexHull")
|
child_hull = child.callDecoration("_compute2DConvexHull")
|
||||||
if child_hull:
|
if child_hull:
|
||||||
|
try:
|
||||||
points = numpy.append(points, child_hull.getPoints(), axis = 0)
|
points = numpy.append(points, child_hull.getPoints(), axis = 0)
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
if points.size < 3:
|
if points.size < 3:
|
||||||
return None
|
return None
|
||||||
@ -233,7 +249,7 @@ class ConvexHullDecorator(SceneNodeDecorator):
|
|||||||
# See http://stackoverflow.com/questions/16970982/find-unique-rows-in-numpy-array
|
# See http://stackoverflow.com/questions/16970982/find-unique-rows-in-numpy-array
|
||||||
vertex_byte_view = numpy.ascontiguousarray(vertex_data).view(
|
vertex_byte_view = numpy.ascontiguousarray(vertex_data).view(
|
||||||
numpy.dtype((numpy.void, vertex_data.dtype.itemsize * vertex_data.shape[1])))
|
numpy.dtype((numpy.void, vertex_data.dtype.itemsize * vertex_data.shape[1])))
|
||||||
_, idx = numpy.unique(vertex_byte_view, return_index=True)
|
_, idx = numpy.unique(vertex_byte_view, return_index = True)
|
||||||
vertex_data = vertex_data[idx] # Select the unique rows by index.
|
vertex_data = vertex_data[idx] # Select the unique rows by index.
|
||||||
|
|
||||||
hull = Polygon(vertex_data)
|
hull = Polygon(vertex_data)
|
||||||
@ -266,7 +282,7 @@ class ConvexHullDecorator(SceneNodeDecorator):
|
|||||||
head_and_fans = self._getHeadAndFans().intersectionConvexHulls(mirrored)
|
head_and_fans = self._getHeadAndFans().intersectionConvexHulls(mirrored)
|
||||||
|
|
||||||
# Min head hull is used for the push free
|
# Min head hull is used for the push free
|
||||||
convex_hull = self._compute2DConvexHeadFull()
|
convex_hull = self._compute2DConvexHull()
|
||||||
if convex_hull:
|
if convex_hull:
|
||||||
return convex_hull.getMinkowskiHull(head_and_fans)
|
return convex_hull.getMinkowskiHull(head_and_fans)
|
||||||
return None
|
return None
|
||||||
@ -280,16 +296,21 @@ class ConvexHullDecorator(SceneNodeDecorator):
|
|||||||
# Add extra margin depending on adhesion type
|
# Add extra margin depending on adhesion type
|
||||||
adhesion_type = self._global_stack.getProperty("adhesion_type", "value")
|
adhesion_type = self._global_stack.getProperty("adhesion_type", "value")
|
||||||
|
|
||||||
|
max_length_available = 0.5 * min(
|
||||||
|
self._getSettingProperty("machine_width", "value"),
|
||||||
|
self._getSettingProperty("machine_depth", "value")
|
||||||
|
)
|
||||||
|
|
||||||
if adhesion_type == "raft":
|
if adhesion_type == "raft":
|
||||||
extra_margin = max(0, self._getSettingProperty("raft_margin", "value"))
|
extra_margin = min(max_length_available, max(0, self._getSettingProperty("raft_margin", "value")))
|
||||||
elif adhesion_type == "brim":
|
elif adhesion_type == "brim":
|
||||||
extra_margin = max(0, self._getSettingProperty("brim_line_count", "value") * self._getSettingProperty("skirt_brim_line_width", "value"))
|
extra_margin = min(max_length_available, max(0, self._getSettingProperty("brim_line_count", "value") * self._getSettingProperty("skirt_brim_line_width", "value")))
|
||||||
elif adhesion_type == "none":
|
elif adhesion_type == "none":
|
||||||
extra_margin = 0
|
extra_margin = 0
|
||||||
elif adhesion_type == "skirt":
|
elif adhesion_type == "skirt":
|
||||||
extra_margin = max(
|
extra_margin = min(max_length_available, max(
|
||||||
0, self._getSettingProperty("skirt_gap", "value") +
|
0, self._getSettingProperty("skirt_gap", "value") +
|
||||||
self._getSettingProperty("skirt_line_count", "value") * self._getSettingProperty("skirt_brim_line_width", "value"))
|
self._getSettingProperty("skirt_line_count", "value") * self._getSettingProperty("skirt_brim_line_width", "value")))
|
||||||
else:
|
else:
|
||||||
raise Exception("Unknown bed adhesion type. Did you forget to update the convex hull calculations for your new bed adhesion type?")
|
raise Exception("Unknown bed adhesion type. Did you forget to update the convex hull calculations for your new bed adhesion type?")
|
||||||
|
|
||||||
@ -386,4 +407,4 @@ class ConvexHullDecorator(SceneNodeDecorator):
|
|||||||
## Settings that change the convex hull.
|
## Settings that change the convex hull.
|
||||||
#
|
#
|
||||||
# If these settings change, the convex hull should be recalculated.
|
# If these settings change, the convex hull should be recalculated.
|
||||||
_influencing_settings = {"xy_offset", "xy_offset_layer_0", "mold_enabled", "mold_width"}
|
_influencing_settings = {"xy_offset", "xy_offset_layer_0", "mold_enabled", "mold_width", "anti_overhang_mesh", "infill_mesh", "cutting_mesh"}
|
||||||
|
@ -1,7 +1,10 @@
|
|||||||
# Copyright (c) 2015 Ultimaker B.V.
|
# Copyright (c) 2015 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
|
||||||
|
|
||||||
from UM.Application import Application
|
from UM.Application import Application
|
||||||
|
from UM.Math.Polygon import Polygon
|
||||||
|
from UM.Qt.QtApplication import QtApplication
|
||||||
from UM.Scene.SceneNode import SceneNode
|
from UM.Scene.SceneNode import SceneNode
|
||||||
from UM.Resources import Resources
|
from UM.Resources import Resources
|
||||||
from UM.Math.Color import Color
|
from UM.Math.Color import Color
|
||||||
@ -16,7 +19,7 @@ class ConvexHullNode(SceneNode):
|
|||||||
# location an object uses on the buildplate. This area (or area's in case of one at a time printing) is
|
# location an object uses on the buildplate. This area (or area's in case of one at a time printing) is
|
||||||
# then displayed as a transparent shadow. If the adhesion type is set to raft, the area is extruded
|
# then displayed as a transparent shadow. If the adhesion type is set to raft, the area is extruded
|
||||||
# to represent the raft as well.
|
# to represent the raft as well.
|
||||||
def __init__(self, node, hull, thickness, parent = None):
|
def __init__(self, node: SceneNode, hull: Optional[Polygon], thickness: float, parent: Optional[SceneNode] = None) -> None:
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
|
|
||||||
self.setCalculateBoundingBox(False)
|
self.setCalculateBoundingBox(False)
|
||||||
@ -25,7 +28,11 @@ class ConvexHullNode(SceneNode):
|
|||||||
|
|
||||||
# Color of the drawn convex hull
|
# Color of the drawn convex hull
|
||||||
if not Application.getInstance().getIsHeadLess():
|
if not Application.getInstance().getIsHeadLess():
|
||||||
self._color = Color(*Application.getInstance().getTheme().getColor("convex_hull").getRgb())
|
theme = QtApplication.getInstance().getTheme()
|
||||||
|
if theme:
|
||||||
|
self._color = Color(*theme.getColor("convex_hull").getRgb())
|
||||||
|
else:
|
||||||
|
self._color = Color(0, 0, 0)
|
||||||
else:
|
else:
|
||||||
self._color = Color(0, 0, 0)
|
self._color = Color(0, 0, 0)
|
||||||
|
|
||||||
@ -47,7 +54,7 @@ class ConvexHullNode(SceneNode):
|
|||||||
|
|
||||||
if hull_mesh_builder.addConvexPolygonExtrusion(
|
if hull_mesh_builder.addConvexPolygonExtrusion(
|
||||||
self._hull.getPoints()[::-1], # bottom layer is reversed
|
self._hull.getPoints()[::-1], # bottom layer is reversed
|
||||||
self._mesh_height-thickness, self._mesh_height, color=self._color):
|
self._mesh_height - thickness, self._mesh_height, color = self._color):
|
||||||
|
|
||||||
hull_mesh = hull_mesh_builder.build()
|
hull_mesh = hull_mesh_builder.build()
|
||||||
self.setMeshData(hull_mesh)
|
self.setMeshData(hull_mesh)
|
||||||
@ -75,7 +82,7 @@ class ConvexHullNode(SceneNode):
|
|||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def _onNodeDecoratorsChanged(self, node):
|
def _onNodeDecoratorsChanged(self, node: SceneNode) -> None:
|
||||||
convex_hull_head = self._node.callDecoration("getConvexHullHead")
|
convex_hull_head = self._node.callDecoration("getConvexHullHead")
|
||||||
if convex_hull_head:
|
if convex_hull_head:
|
||||||
convex_hull_head_builder = MeshBuilder()
|
convex_hull_head_builder = MeshBuilder()
|
||||||
|
@ -3,7 +3,8 @@ from UM.Logger import Logger
|
|||||||
from PyQt5.QtCore import Qt, pyqtSlot, QObject
|
from PyQt5.QtCore import Qt, pyqtSlot, QObject
|
||||||
from PyQt5.QtWidgets import QApplication
|
from PyQt5.QtWidgets import QApplication
|
||||||
|
|
||||||
from cura.ObjectsModel import ObjectsModel
|
from UM.Scene.Camera import Camera
|
||||||
|
from cura.UI.ObjectsModel import ObjectsModel
|
||||||
from cura.Machines.Models.MultiBuildPlateModel import MultiBuildPlateModel
|
from cura.Machines.Models.MultiBuildPlateModel import MultiBuildPlateModel
|
||||||
|
|
||||||
from UM.Application import Application
|
from UM.Application import Application
|
||||||
@ -33,7 +34,7 @@ class CuraSceneController(QObject):
|
|||||||
source = args[0]
|
source = args[0]
|
||||||
else:
|
else:
|
||||||
source = None
|
source = None
|
||||||
if not isinstance(source, SceneNode):
|
if not isinstance(source, SceneNode) or isinstance(source, Camera):
|
||||||
return
|
return
|
||||||
max_build_plate = self._calcMaxBuildPlate()
|
max_build_plate = self._calcMaxBuildPlate()
|
||||||
changed = False
|
changed = False
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
# Copyright (c) 2018 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 copy import deepcopy
|
from copy import deepcopy
|
||||||
@ -6,13 +6,14 @@ from typing import cast, Dict, List, Optional
|
|||||||
|
|
||||||
from UM.Application import Application
|
from UM.Application import Application
|
||||||
from UM.Math.AxisAlignedBox import AxisAlignedBox
|
from UM.Math.AxisAlignedBox import AxisAlignedBox
|
||||||
from UM.Math.Polygon import Polygon #For typing.
|
from UM.Math.Polygon import Polygon # For typing.
|
||||||
from UM.Scene.SceneNode import SceneNode
|
from UM.Scene.SceneNode import SceneNode
|
||||||
from UM.Scene.SceneNodeDecorator import SceneNodeDecorator #To cast the deepcopy of every decorator back to SceneNodeDecorator.
|
from UM.Scene.SceneNodeDecorator import SceneNodeDecorator # To cast the deepcopy of every decorator back to SceneNodeDecorator.
|
||||||
|
|
||||||
|
import cura.CuraApplication # To get the build plate.
|
||||||
|
from cura.Settings.ExtruderStack import ExtruderStack # For typing.
|
||||||
|
from cura.Settings.SettingOverrideDecorator import SettingOverrideDecorator # For per-object settings.
|
||||||
|
|
||||||
import cura.CuraApplication #To get the build plate.
|
|
||||||
from cura.Settings.ExtruderStack import ExtruderStack #For typing.
|
|
||||||
from cura.Settings.SettingOverrideDecorator import SettingOverrideDecorator #For per-object settings.
|
|
||||||
|
|
||||||
## Scene nodes that are models are only seen when selecting the corresponding build plate
|
## Scene nodes that are models are only seen when selecting the corresponding build plate
|
||||||
# Note that many other nodes can just be UM SceneNode objects.
|
# Note that many other nodes can just be UM SceneNode objects.
|
||||||
@ -20,7 +21,7 @@ class CuraSceneNode(SceneNode):
|
|||||||
def __init__(self, parent: Optional["SceneNode"] = None, visible: bool = True, name: str = "", no_setting_override: bool = False) -> None:
|
def __init__(self, parent: Optional["SceneNode"] = None, visible: bool = True, name: str = "", no_setting_override: bool = False) -> None:
|
||||||
super().__init__(parent = parent, visible = visible, name = name)
|
super().__init__(parent = parent, visible = visible, name = name)
|
||||||
if not no_setting_override:
|
if not no_setting_override:
|
||||||
self.addDecorator(SettingOverrideDecorator()) # now we always have a getActiveExtruderPosition, unless explicitly disabled
|
self.addDecorator(SettingOverrideDecorator()) # Now we always have a getActiveExtruderPosition, unless explicitly disabled
|
||||||
self._outside_buildarea = False
|
self._outside_buildarea = False
|
||||||
|
|
||||||
def setOutsideBuildArea(self, new_value: bool) -> None:
|
def setOutsideBuildArea(self, new_value: bool) -> None:
|
||||||
@ -85,24 +86,14 @@ class CuraSceneNode(SceneNode):
|
|||||||
1.0
|
1.0
|
||||||
]
|
]
|
||||||
|
|
||||||
## Return if the provided bbox collides with the bbox of this scene node
|
|
||||||
def collidesWithBbox(self, check_bbox: AxisAlignedBox) -> bool:
|
|
||||||
bbox = self.getBoundingBox()
|
|
||||||
if bbox is not None:
|
|
||||||
# Mark the node as outside the build volume if the bounding box test fails.
|
|
||||||
if check_bbox.intersectsBox(bbox) != AxisAlignedBox.IntersectionResult.FullIntersection:
|
|
||||||
return True
|
|
||||||
|
|
||||||
return False
|
|
||||||
|
|
||||||
## Return if any area collides with the convex hull of this scene node
|
## Return if any area collides with the convex hull of this scene node
|
||||||
def collidesWithArea(self, areas: List[Polygon]) -> bool:
|
def collidesWithAreas(self, areas: List[Polygon]) -> bool:
|
||||||
convex_hull = self.callDecoration("getConvexHull")
|
convex_hull = self.callDecoration("getConvexHull")
|
||||||
if convex_hull:
|
if convex_hull:
|
||||||
if not convex_hull.isValid():
|
if not convex_hull.isValid():
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Check for collisions between disallowed areas and the object
|
# Check for collisions between provided areas and the object
|
||||||
for area in areas:
|
for area in areas:
|
||||||
overlap = convex_hull.intersectsPolygon(area)
|
overlap = convex_hull.intersectsPolygon(area)
|
||||||
if overlap is None:
|
if overlap is None:
|
||||||
@ -112,21 +103,24 @@ class CuraSceneNode(SceneNode):
|
|||||||
|
|
||||||
## Override of SceneNode._calculateAABB to exclude non-printing-meshes from bounding box
|
## Override of SceneNode._calculateAABB to exclude non-printing-meshes from bounding box
|
||||||
def _calculateAABB(self) -> None:
|
def _calculateAABB(self) -> None:
|
||||||
|
self._aabb = None
|
||||||
if self._mesh_data:
|
if self._mesh_data:
|
||||||
aabb = self._mesh_data.getExtents(self.getWorldTransformation())
|
self._aabb = self._mesh_data.getExtents(self.getWorldTransformation())
|
||||||
else: # If there is no mesh_data, use a boundingbox that encompasses the local (0,0,0)
|
else: # If there is no mesh_data, use a boundingbox that encompasses the local (0,0,0)
|
||||||
position = self.getWorldPosition()
|
position = self.getWorldPosition()
|
||||||
aabb = AxisAlignedBox(minimum = position, maximum = position)
|
self._aabb = AxisAlignedBox(minimum=position, maximum=position)
|
||||||
|
|
||||||
for child in self._children:
|
for child in self.getAllChildren():
|
||||||
if child.callDecoration("isNonPrintingMesh"):
|
if child.callDecoration("isNonPrintingMesh"):
|
||||||
# Non-printing-meshes inside a group should not affect push apart or drop to build plate
|
# Non-printing-meshes inside a group should not affect push apart or drop to build plate
|
||||||
continue
|
continue
|
||||||
if aabb is None:
|
if not child.getMeshData():
|
||||||
aabb = child.getBoundingBox()
|
# Nodes without mesh data should not affect bounding boxes of their parents.
|
||||||
|
continue
|
||||||
|
if self._aabb is None:
|
||||||
|
self._aabb = child.getBoundingBox()
|
||||||
else:
|
else:
|
||||||
aabb = aabb + child.getBoundingBox()
|
self._aabb = self._aabb + child.getBoundingBox()
|
||||||
self._aabb = aabb
|
|
||||||
|
|
||||||
## Taken from SceneNode, but replaced SceneNode with CuraSceneNode
|
## Taken from SceneNode, but replaced SceneNode with CuraSceneNode
|
||||||
def __deepcopy__(self, memo: Dict[int, object]) -> "CuraSceneNode":
|
def __deepcopy__(self, memo: Dict[int, object]) -> "CuraSceneNode":
|
||||||
|
@ -10,7 +10,7 @@ class GCodeListDecorator(SceneNodeDecorator):
|
|||||||
def getGCodeList(self) -> List[str]:
|
def getGCodeList(self) -> List[str]:
|
||||||
return self._gcode_list
|
return self._gcode_list
|
||||||
|
|
||||||
def setGCodeList(self, list: List[str]):
|
def setGCodeList(self, list: List[str]) -> None:
|
||||||
self._gcode_list = list
|
self._gcode_list = list
|
||||||
|
|
||||||
def __deepcopy__(self, memo) -> "GCodeListDecorator":
|
def __deepcopy__(self, memo) -> "GCodeListDecorator":
|
||||||
|
@ -47,8 +47,10 @@ class ContainerManager(QObject):
|
|||||||
if ContainerManager.__instance is not None:
|
if ContainerManager.__instance is not None:
|
||||||
raise RuntimeError("Try to create singleton '%s' more than once" % self.__class__.__name__)
|
raise RuntimeError("Try to create singleton '%s' more than once" % self.__class__.__name__)
|
||||||
ContainerManager.__instance = self
|
ContainerManager.__instance = self
|
||||||
|
try:
|
||||||
super().__init__(parent = application)
|
super().__init__(parent = application)
|
||||||
|
except TypeError:
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
self._application = application # type: CuraApplication
|
self._application = application # type: CuraApplication
|
||||||
self._plugin_registry = self._application.getPluginRegistry() # type: PluginRegistry
|
self._plugin_registry = self._application.getPluginRegistry() # type: PluginRegistry
|
||||||
@ -419,13 +421,13 @@ class ContainerManager(QObject):
|
|||||||
self._container_name_filters[name_filter] = entry
|
self._container_name_filters[name_filter] = entry
|
||||||
|
|
||||||
## Import single profile, file_url does not have to end with curaprofile
|
## Import single profile, file_url does not have to end with curaprofile
|
||||||
@pyqtSlot(QUrl, result="QVariantMap")
|
@pyqtSlot(QUrl, result = "QVariantMap")
|
||||||
def importProfile(self, file_url: QUrl):
|
def importProfile(self, file_url: QUrl) -> Dict[str, str]:
|
||||||
if not file_url.isValid():
|
if not file_url.isValid():
|
||||||
return
|
return {"status": "error", "message": catalog.i18nc("@info:status", "Invalid file URL:") + " " + str(file_url)}
|
||||||
path = file_url.toLocalFile()
|
path = file_url.toLocalFile()
|
||||||
if not path:
|
if not path:
|
||||||
return
|
return {"status": "error", "message": catalog.i18nc("@info:status", "Invalid file URL:") + " " + str(file_url)}
|
||||||
return self._container_registry.importProfile(path)
|
return self._container_registry.importProfile(path)
|
||||||
|
|
||||||
@pyqtSlot(QObject, QUrl, str)
|
@pyqtSlot(QObject, QUrl, str)
|
||||||
|
@ -1,16 +1,16 @@
|
|||||||
# Copyright (c) 2018 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 os
|
import os
|
||||||
import re
|
import re
|
||||||
import configparser
|
import configparser
|
||||||
|
|
||||||
from typing import cast, Optional
|
from typing import Any, cast, Dict, Optional
|
||||||
|
|
||||||
from PyQt5.QtWidgets import QMessageBox
|
from PyQt5.QtWidgets import QMessageBox
|
||||||
|
|
||||||
from UM.Decorators import override
|
from UM.Decorators import override
|
||||||
from UM.Settings.ContainerFormatError import ContainerFormatError
|
from UM.Settings.ContainerFormatError import ContainerFormatError
|
||||||
|
from UM.Settings.Interfaces import ContainerInterface
|
||||||
from UM.Settings.ContainerRegistry import ContainerRegistry
|
from UM.Settings.ContainerRegistry import ContainerRegistry
|
||||||
from UM.Settings.ContainerStack import ContainerStack
|
from UM.Settings.ContainerStack import ContainerStack
|
||||||
from UM.Settings.InstanceContainer import InstanceContainer
|
from UM.Settings.InstanceContainer import InstanceContainer
|
||||||
@ -28,7 +28,7 @@ from . import GlobalStack
|
|||||||
|
|
||||||
import cura.CuraApplication
|
import cura.CuraApplication
|
||||||
from cura.Machines.QualityManager import getMachineDefinitionIDForQualitySearch
|
from cura.Machines.QualityManager import getMachineDefinitionIDForQualitySearch
|
||||||
from cura.ReaderWriters.ProfileReader import NoProfileException
|
from cura.ReaderWriters.ProfileReader import NoProfileException, ProfileReader
|
||||||
|
|
||||||
from UM.i18n import i18nCatalog
|
from UM.i18n import i18nCatalog
|
||||||
catalog = i18nCatalog("cura")
|
catalog = i18nCatalog("cura")
|
||||||
@ -103,13 +103,14 @@ class CuraContainerRegistry(ContainerRegistry):
|
|||||||
# \param instance_ids \type{list} the IDs of the profiles to export.
|
# \param instance_ids \type{list} the IDs of the profiles to export.
|
||||||
# \param file_name \type{str} the full path and filename to export to.
|
# \param file_name \type{str} the full path and filename to export to.
|
||||||
# \param file_type \type{str} the file type with the format "<description> (*.<extension>)"
|
# \param file_type \type{str} the file type with the format "<description> (*.<extension>)"
|
||||||
def exportQualityProfile(self, container_list, file_name, file_type):
|
# \return True if the export succeeded, false otherwise.
|
||||||
|
def exportQualityProfile(self, container_list, file_name, file_type) -> bool:
|
||||||
# Parse the fileType to deduce what plugin can save the file format.
|
# Parse the fileType to deduce what plugin can save the file format.
|
||||||
# fileType has the format "<description> (*.<extension>)"
|
# fileType has the format "<description> (*.<extension>)"
|
||||||
split = file_type.rfind(" (*.") # Find where the description ends and the extension starts.
|
split = file_type.rfind(" (*.") # Find where the description ends and the extension starts.
|
||||||
if split < 0: # Not found. Invalid format.
|
if split < 0: # Not found. Invalid format.
|
||||||
Logger.log("e", "Invalid file format identifier %s", file_type)
|
Logger.log("e", "Invalid file format identifier %s", file_type)
|
||||||
return
|
return False
|
||||||
description = file_type[:split]
|
description = file_type[:split]
|
||||||
extension = file_type[split + 4:-1] # Leave out the " (*." and ")".
|
extension = file_type[split + 4:-1] # Leave out the " (*." and ")".
|
||||||
if not file_name.endswith("." + extension): # Auto-fill the extension if the user did not provide any.
|
if not file_name.endswith("." + extension): # Auto-fill the extension if the user did not provide any.
|
||||||
@ -121,7 +122,7 @@ class CuraContainerRegistry(ContainerRegistry):
|
|||||||
result = QMessageBox.question(None, catalog.i18nc("@title:window", "File Already Exists"),
|
result = QMessageBox.question(None, catalog.i18nc("@title:window", "File Already Exists"),
|
||||||
catalog.i18nc("@label Don't translate the XML tag <filename>!", "The file <filename>{0}</filename> already exists. Are you sure you want to overwrite it?").format(file_name))
|
catalog.i18nc("@label Don't translate the XML tag <filename>!", "The file <filename>{0}</filename> already exists. Are you sure you want to overwrite it?").format(file_name))
|
||||||
if result == QMessageBox.No:
|
if result == QMessageBox.No:
|
||||||
return
|
return False
|
||||||
|
|
||||||
profile_writer = self._findProfileWriter(extension, description)
|
profile_writer = self._findProfileWriter(extension, description)
|
||||||
try:
|
try:
|
||||||
@ -132,17 +133,18 @@ class CuraContainerRegistry(ContainerRegistry):
|
|||||||
lifetime = 0,
|
lifetime = 0,
|
||||||
title = catalog.i18nc("@info:title", "Error"))
|
title = catalog.i18nc("@info:title", "Error"))
|
||||||
m.show()
|
m.show()
|
||||||
return
|
return False
|
||||||
if not success:
|
if not success:
|
||||||
Logger.log("w", "Failed to export profile to %s: Writer plugin reported failure.", file_name)
|
Logger.log("w", "Failed to export profile to %s: Writer plugin reported failure.", file_name)
|
||||||
m = Message(catalog.i18nc("@info:status Don't translate the XML tag <filename>!", "Failed to export profile to <filename>{0}</filename>: Writer plugin reported failure.", file_name),
|
m = Message(catalog.i18nc("@info:status Don't translate the XML tag <filename>!", "Failed to export profile to <filename>{0}</filename>: Writer plugin reported failure.", file_name),
|
||||||
lifetime = 0,
|
lifetime = 0,
|
||||||
title = catalog.i18nc("@info:title", "Error"))
|
title = catalog.i18nc("@info:title", "Error"))
|
||||||
m.show()
|
m.show()
|
||||||
return
|
return False
|
||||||
m = Message(catalog.i18nc("@info:status Don't translate the XML tag <filename>!", "Exported profile to <filename>{0}</filename>", file_name),
|
m = Message(catalog.i18nc("@info:status Don't translate the XML tag <filename>!", "Exported profile to <filename>{0}</filename>", file_name),
|
||||||
title = catalog.i18nc("@info:title", "Export succeeded"))
|
title = catalog.i18nc("@info:title", "Export succeeded"))
|
||||||
m.show()
|
m.show()
|
||||||
|
return True
|
||||||
|
|
||||||
## Gets the plugin object matching the criteria
|
## Gets the plugin object matching the criteria
|
||||||
# \param extension
|
# \param extension
|
||||||
@ -161,29 +163,29 @@ class CuraContainerRegistry(ContainerRegistry):
|
|||||||
|
|
||||||
## Imports a profile from a file
|
## Imports a profile from a file
|
||||||
#
|
#
|
||||||
# \param file_name \type{str} the full path and filename of the profile to import
|
# \param file_name The full path and filename of the profile to import.
|
||||||
# \return \type{Dict} dict with a 'status' key containing the string 'ok' or 'error', and a 'message' key
|
# \return Dict with a 'status' key containing the string 'ok' or 'error',
|
||||||
# containing a message for the user
|
# and a 'message' key containing a message for the user.
|
||||||
def importProfile(self, file_name):
|
def importProfile(self, file_name: str) -> Dict[str, str]:
|
||||||
Logger.log("d", "Attempting to import profile %s", file_name)
|
Logger.log("d", "Attempting to import profile %s", file_name)
|
||||||
if not file_name:
|
if not file_name:
|
||||||
return { "status": "error", "message": catalog.i18nc("@info:status Don't translate the XML tags <filename> or <message>!", "Failed to import profile from <filename>{0}</filename>: <message>{1}</message>", file_name, "Invalid path")}
|
return { "status": "error", "message": catalog.i18nc("@info:status Don't translate the XML tags <filename>!", "Failed to import profile from <filename>{0}</filename>: {1}", file_name, "Invalid path")}
|
||||||
|
|
||||||
plugin_registry = PluginRegistry.getInstance()
|
|
||||||
extension = file_name.split(".")[-1]
|
|
||||||
|
|
||||||
global_stack = Application.getInstance().getGlobalContainerStack()
|
global_stack = Application.getInstance().getGlobalContainerStack()
|
||||||
if not global_stack:
|
if not global_stack:
|
||||||
return
|
return {"status": "error", "message": catalog.i18nc("@info:status Don't translate the XML tags <filename>!", "Can't import profile from <filename>{0}</filename> before a printer is added.", file_name)}
|
||||||
|
|
||||||
machine_extruders = []
|
machine_extruders = []
|
||||||
for position in sorted(global_stack.extruders):
|
for position in sorted(global_stack.extruders):
|
||||||
machine_extruders.append(global_stack.extruders[position])
|
machine_extruders.append(global_stack.extruders[position])
|
||||||
|
|
||||||
|
plugin_registry = PluginRegistry.getInstance()
|
||||||
|
extension = file_name.split(".")[-1]
|
||||||
|
|
||||||
for plugin_id, meta_data in self._getIOPlugins("profile_reader"):
|
for plugin_id, meta_data in self._getIOPlugins("profile_reader"):
|
||||||
if meta_data["profile_reader"][0]["extension"] != extension:
|
if meta_data["profile_reader"][0]["extension"] != extension:
|
||||||
continue
|
continue
|
||||||
profile_reader = plugin_registry.getPluginObject(plugin_id)
|
profile_reader = cast(ProfileReader, plugin_registry.getPluginObject(plugin_id))
|
||||||
try:
|
try:
|
||||||
profile_or_list = profile_reader.read(file_name) # Try to open the file with the profile reader.
|
profile_or_list = profile_reader.read(file_name) # Try to open the file with the profile reader.
|
||||||
except NoProfileException:
|
except NoProfileException:
|
||||||
@ -221,13 +223,13 @@ class CuraContainerRegistry(ContainerRegistry):
|
|||||||
# Make sure we have a profile_definition in the file:
|
# Make sure we have a profile_definition in the file:
|
||||||
if profile_definition is None:
|
if profile_definition is None:
|
||||||
break
|
break
|
||||||
machine_definition = self.findDefinitionContainers(id = profile_definition)
|
machine_definitions = self.findDefinitionContainers(id = profile_definition)
|
||||||
if not machine_definition:
|
if not machine_definitions:
|
||||||
Logger.log("e", "Incorrect profile [%s]. Unknown machine type [%s]", file_name, profile_definition)
|
Logger.log("e", "Incorrect profile [%s]. Unknown machine type [%s]", file_name, profile_definition)
|
||||||
return {"status": "error",
|
return {"status": "error",
|
||||||
"message": catalog.i18nc("@info:status Don't translate the XML tags <filename>!", "This profile <filename>{0}</filename> contains incorrect data, could not import it.", file_name)
|
"message": catalog.i18nc("@info:status Don't translate the XML tags <filename>!", "This profile <filename>{0}</filename> contains incorrect data, could not import it.", file_name)
|
||||||
}
|
}
|
||||||
machine_definition = machine_definition[0]
|
machine_definition = machine_definitions[0]
|
||||||
|
|
||||||
# Get the expected machine definition.
|
# Get the expected machine definition.
|
||||||
# i.e.: We expect gcode for a UM2 Extended to be defined as normal UM2 gcode...
|
# i.e.: We expect gcode for a UM2 Extended to be defined as normal UM2 gcode...
|
||||||
@ -267,20 +269,21 @@ class CuraContainerRegistry(ContainerRegistry):
|
|||||||
profile.setMetaDataEntry("position", "0")
|
profile.setMetaDataEntry("position", "0")
|
||||||
profile.setDirty(True)
|
profile.setDirty(True)
|
||||||
if idx == 0:
|
if idx == 0:
|
||||||
# move all per-extruder settings to the first extruder's quality_changes
|
# Move all per-extruder settings to the first extruder's quality_changes
|
||||||
for qc_setting_key in global_profile.getAllKeys():
|
for qc_setting_key in global_profile.getAllKeys():
|
||||||
settable_per_extruder = global_stack.getProperty(qc_setting_key, "settable_per_extruder")
|
settable_per_extruder = global_stack.getProperty(qc_setting_key, "settable_per_extruder")
|
||||||
if settable_per_extruder:
|
if settable_per_extruder:
|
||||||
setting_value = global_profile.getProperty(qc_setting_key, "value")
|
setting_value = global_profile.getProperty(qc_setting_key, "value")
|
||||||
|
|
||||||
setting_definition = global_stack.getSettingDefinition(qc_setting_key)
|
setting_definition = global_stack.getSettingDefinition(qc_setting_key)
|
||||||
|
if setting_definition is not None:
|
||||||
new_instance = SettingInstance(setting_definition, profile)
|
new_instance = SettingInstance(setting_definition, profile)
|
||||||
new_instance.setProperty("value", setting_value)
|
new_instance.setProperty("value", setting_value)
|
||||||
new_instance.resetState() # Ensure that the state is not seen as a user state.
|
new_instance.resetState() # Ensure that the state is not seen as a user state.
|
||||||
profile.addInstance(new_instance)
|
profile.addInstance(new_instance)
|
||||||
profile.setDirty(True)
|
profile.setDirty(True)
|
||||||
|
|
||||||
global_profile.removeInstance(qc_setting_key, postpone_emit=True)
|
global_profile.removeInstance(qc_setting_key, postpone_emit = True)
|
||||||
extruder_profiles.append(profile)
|
extruder_profiles.append(profile)
|
||||||
|
|
||||||
for profile in extruder_profiles:
|
for profile in extruder_profiles:
|
||||||
@ -290,7 +293,7 @@ class CuraContainerRegistry(ContainerRegistry):
|
|||||||
for profile_index, profile in enumerate(profile_or_list):
|
for profile_index, profile in enumerate(profile_or_list):
|
||||||
if profile_index == 0:
|
if profile_index == 0:
|
||||||
# This is assumed to be the global profile
|
# This is assumed to be the global profile
|
||||||
profile_id = (global_stack.getBottom().getId() + "_" + name_seed).lower().replace(" ", "_")
|
profile_id = (cast(ContainerInterface, global_stack.getBottom()).getId() + "_" + name_seed).lower().replace(" ", "_")
|
||||||
|
|
||||||
elif profile_index < len(machine_extruders) + 1:
|
elif profile_index < len(machine_extruders) + 1:
|
||||||
# This is assumed to be an extruder profile
|
# This is assumed to be an extruder profile
|
||||||
@ -302,8 +305,8 @@ class CuraContainerRegistry(ContainerRegistry):
|
|||||||
profile.setMetaDataEntry("position", extruder_position)
|
profile.setMetaDataEntry("position", extruder_position)
|
||||||
profile_id = (extruder_id + "_" + name_seed).lower().replace(" ", "_")
|
profile_id = (extruder_id + "_" + name_seed).lower().replace(" ", "_")
|
||||||
|
|
||||||
else: #More extruders in the imported file than in the machine.
|
else: # More extruders in the imported file than in the machine.
|
||||||
continue #Delete the additional profiles.
|
continue # Delete the additional profiles.
|
||||||
|
|
||||||
result = self._configureProfile(profile, profile_id, new_name, expected_machine_definition)
|
result = self._configureProfile(profile, profile_id, new_name, expected_machine_definition)
|
||||||
if result is not None:
|
if result is not None:
|
||||||
@ -326,6 +329,23 @@ class CuraContainerRegistry(ContainerRegistry):
|
|||||||
self._registerSingleExtrusionMachinesExtruderStacks()
|
self._registerSingleExtrusionMachinesExtruderStacks()
|
||||||
self._connectUpgradedExtruderStacksToMachines()
|
self._connectUpgradedExtruderStacksToMachines()
|
||||||
|
|
||||||
|
## Check if the metadata for a container is okay before adding it.
|
||||||
|
#
|
||||||
|
# This overrides the one from UM.Settings.ContainerRegistry because we
|
||||||
|
# also require that the setting_version is correct.
|
||||||
|
@override(ContainerRegistry)
|
||||||
|
def _isMetadataValid(self, metadata: Optional[Dict[str, Any]]) -> bool:
|
||||||
|
if metadata is None:
|
||||||
|
return False
|
||||||
|
if "setting_version" not in metadata:
|
||||||
|
return False
|
||||||
|
try:
|
||||||
|
if int(metadata["setting_version"]) != cura.CuraApplication.CuraApplication.SettingVersion:
|
||||||
|
return False
|
||||||
|
except ValueError: #Not parsable as int.
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
## Update an imported profile to match the current machine configuration.
|
## Update an imported profile to match the current machine configuration.
|
||||||
#
|
#
|
||||||
# \param profile The profile to configure.
|
# \param profile The profile to configure.
|
||||||
@ -385,30 +405,6 @@ class CuraContainerRegistry(ContainerRegistry):
|
|||||||
result.append( (plugin_id, meta_data) )
|
result.append( (plugin_id, meta_data) )
|
||||||
return result
|
return result
|
||||||
|
|
||||||
## Returns true if the current machine requires its own materials
|
|
||||||
# \return True if the current machine requires its own materials
|
|
||||||
def _machineHasOwnMaterials(self):
|
|
||||||
global_container_stack = Application.getInstance().getGlobalContainerStack()
|
|
||||||
if global_container_stack:
|
|
||||||
return global_container_stack.getMetaDataEntry("has_materials", False)
|
|
||||||
return False
|
|
||||||
|
|
||||||
## Gets the ID of the active material
|
|
||||||
# \return the ID of the active material or the empty string
|
|
||||||
def _activeMaterialId(self):
|
|
||||||
global_container_stack = Application.getInstance().getGlobalContainerStack()
|
|
||||||
if global_container_stack and global_container_stack.material:
|
|
||||||
return global_container_stack.material.getId()
|
|
||||||
return ""
|
|
||||||
|
|
||||||
## Returns true if the current machine requires its own quality profiles
|
|
||||||
# \return true if the current machine requires its own quality profiles
|
|
||||||
def _machineHasOwnQualities(self):
|
|
||||||
global_container_stack = Application.getInstance().getGlobalContainerStack()
|
|
||||||
if global_container_stack:
|
|
||||||
return parseBool(global_container_stack.getMetaDataEntry("has_machine_quality", False))
|
|
||||||
return False
|
|
||||||
|
|
||||||
## Convert an "old-style" pure ContainerStack to either an Extruder or Global stack.
|
## Convert an "old-style" pure ContainerStack to either an Extruder or Global stack.
|
||||||
def _convertContainerStack(self, container):
|
def _convertContainerStack(self, container):
|
||||||
assert type(container) == ContainerStack
|
assert type(container) == ContainerStack
|
||||||
@ -520,7 +516,7 @@ class CuraContainerRegistry(ContainerRegistry):
|
|||||||
user_container.setMetaDataEntry("position", extruder_stack.getMetaDataEntry("position"))
|
user_container.setMetaDataEntry("position", extruder_stack.getMetaDataEntry("position"))
|
||||||
|
|
||||||
if machine.userChanges:
|
if machine.userChanges:
|
||||||
# for the newly created extruder stack, we need to move all "per-extruder" settings to the user changes
|
# For the newly created extruder stack, we need to move all "per-extruder" settings to the user changes
|
||||||
# container to the extruder stack.
|
# container to the extruder stack.
|
||||||
for user_setting_key in machine.userChanges.getAllKeys():
|
for user_setting_key in machine.userChanges.getAllKeys():
|
||||||
settable_per_extruder = machine.getProperty(user_setting_key, "settable_per_extruder")
|
settable_per_extruder = machine.getProperty(user_setting_key, "settable_per_extruder")
|
||||||
@ -582,7 +578,7 @@ class CuraContainerRegistry(ContainerRegistry):
|
|||||||
extruder_quality_changes_container.setMetaDataEntry("position", extruder_definition.getMetaDataEntry("position"))
|
extruder_quality_changes_container.setMetaDataEntry("position", extruder_definition.getMetaDataEntry("position"))
|
||||||
extruder_stack.qualityChanges = self.findInstanceContainers(id = quality_changes_id)[0]
|
extruder_stack.qualityChanges = self.findInstanceContainers(id = quality_changes_id)[0]
|
||||||
else:
|
else:
|
||||||
# if we still cannot find a quality changes container for the extruder, create a new one
|
# If we still cannot find a quality changes container for the extruder, create a new one
|
||||||
container_name = machine_quality_changes.getName()
|
container_name = machine_quality_changes.getName()
|
||||||
container_id = self.uniqueName(extruder_stack.getId() + "_qc_" + container_name)
|
container_id = self.uniqueName(extruder_stack.getId() + "_qc_" + container_name)
|
||||||
extruder_quality_changes_container = InstanceContainer(container_id, parent = application)
|
extruder_quality_changes_container = InstanceContainer(container_id, parent = application)
|
||||||
@ -600,7 +596,7 @@ class CuraContainerRegistry(ContainerRegistry):
|
|||||||
Logger.log("w", "Could not find quality_changes named [%s] for extruder [%s]",
|
Logger.log("w", "Could not find quality_changes named [%s] for extruder [%s]",
|
||||||
machine_quality_changes.getName(), extruder_stack.getId())
|
machine_quality_changes.getName(), extruder_stack.getId())
|
||||||
else:
|
else:
|
||||||
# move all per-extruder settings to the extruder's quality changes
|
# Move all per-extruder settings to the extruder's quality changes
|
||||||
for qc_setting_key in machine_quality_changes.getAllKeys():
|
for qc_setting_key in machine_quality_changes.getAllKeys():
|
||||||
settable_per_extruder = machine.getProperty(qc_setting_key, "settable_per_extruder")
|
settable_per_extruder = machine.getProperty(qc_setting_key, "settable_per_extruder")
|
||||||
if settable_per_extruder:
|
if settable_per_extruder:
|
||||||
@ -641,7 +637,7 @@ class CuraContainerRegistry(ContainerRegistry):
|
|||||||
if qc_name not in qc_groups:
|
if qc_name not in qc_groups:
|
||||||
qc_groups[qc_name] = []
|
qc_groups[qc_name] = []
|
||||||
qc_groups[qc_name].append(qc)
|
qc_groups[qc_name].append(qc)
|
||||||
# try to find from the quality changes cura directory too
|
# Try to find from the quality changes cura directory too
|
||||||
quality_changes_container = self._findQualityChangesContainerInCuraFolder(machine_quality_changes.getName())
|
quality_changes_container = self._findQualityChangesContainerInCuraFolder(machine_quality_changes.getName())
|
||||||
if quality_changes_container:
|
if quality_changes_container:
|
||||||
qc_groups[qc_name].append(quality_changes_container)
|
qc_groups[qc_name].append(quality_changes_container)
|
||||||
@ -655,7 +651,7 @@ class CuraContainerRegistry(ContainerRegistry):
|
|||||||
else:
|
else:
|
||||||
qc_dict["global"] = qc
|
qc_dict["global"] = qc
|
||||||
if qc_dict["global"] is not None and len(qc_dict["extruders"]) == 1:
|
if qc_dict["global"] is not None and len(qc_dict["extruders"]) == 1:
|
||||||
# move per-extruder settings
|
# Move per-extruder settings
|
||||||
for qc_setting_key in qc_dict["global"].getAllKeys():
|
for qc_setting_key in qc_dict["global"].getAllKeys():
|
||||||
settable_per_extruder = machine.getProperty(qc_setting_key, "settable_per_extruder")
|
settable_per_extruder = machine.getProperty(qc_setting_key, "settable_per_extruder")
|
||||||
if settable_per_extruder:
|
if settable_per_extruder:
|
||||||
@ -689,17 +685,17 @@ class CuraContainerRegistry(ContainerRegistry):
|
|||||||
try:
|
try:
|
||||||
parser.read([file_path])
|
parser.read([file_path])
|
||||||
except:
|
except:
|
||||||
# skip, it is not a valid stack file
|
# Skip, it is not a valid stack file
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if not parser.has_option("general", "name"):
|
if not parser.has_option("general", "name"):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if parser["general"]["name"] == name:
|
if parser["general"]["name"] == name:
|
||||||
# load the container
|
# Load the container
|
||||||
container_id = os.path.basename(file_path).replace(".inst.cfg", "")
|
container_id = os.path.basename(file_path).replace(".inst.cfg", "")
|
||||||
if self.findInstanceContainers(id = container_id):
|
if self.findInstanceContainers(id = container_id):
|
||||||
# this container is already in the registry, skip it
|
# This container is already in the registry, skip it
|
||||||
continue
|
continue
|
||||||
|
|
||||||
instance_container = InstanceContainer(container_id)
|
instance_container = InstanceContainer(container_id)
|
||||||
@ -733,7 +729,7 @@ class CuraContainerRegistry(ContainerRegistry):
|
|||||||
else:
|
else:
|
||||||
Logger.log("w", "Could not find machine {machine} for extruder {extruder}", machine = extruder_stack.getMetaDataEntry("machine"), extruder = extruder_stack.getId())
|
Logger.log("w", "Could not find machine {machine} for extruder {extruder}", machine = extruder_stack.getMetaDataEntry("machine"), extruder = extruder_stack.getId())
|
||||||
|
|
||||||
#Override just for the type.
|
# Override just for the type.
|
||||||
@classmethod
|
@classmethod
|
||||||
@override(ContainerRegistry)
|
@override(ContainerRegistry)
|
||||||
def getInstance(cls, *args, **kwargs) -> "CuraContainerRegistry":
|
def getInstance(cls, *args, **kwargs) -> "CuraContainerRegistry":
|
||||||
|
@ -145,13 +145,11 @@ class CuraContainerStack(ContainerStack):
|
|||||||
def setDefinition(self, new_definition: DefinitionContainerInterface) -> None:
|
def setDefinition(self, new_definition: DefinitionContainerInterface) -> None:
|
||||||
self.replaceContainer(_ContainerIndexes.Definition, new_definition)
|
self.replaceContainer(_ContainerIndexes.Definition, new_definition)
|
||||||
|
|
||||||
## Get the definition container.
|
def getDefinition(self) -> "DefinitionContainer":
|
||||||
#
|
|
||||||
# \return The definition container. Should always be a valid container, but can be equal to the empty InstanceContainer.
|
|
||||||
@pyqtProperty(QObject, fset = setDefinition, notify = pyqtContainersChanged)
|
|
||||||
def definition(self) -> DefinitionContainer:
|
|
||||||
return cast(DefinitionContainer, self._containers[_ContainerIndexes.Definition])
|
return cast(DefinitionContainer, self._containers[_ContainerIndexes.Definition])
|
||||||
|
|
||||||
|
definition = pyqtProperty(QObject, fget = getDefinition, fset = setDefinition, notify = pyqtContainersChanged)
|
||||||
|
|
||||||
@override(ContainerStack)
|
@override(ContainerStack)
|
||||||
def getBottom(self) -> "DefinitionContainer":
|
def getBottom(self) -> "DefinitionContainer":
|
||||||
return self.definition
|
return self.definition
|
||||||
|
@ -5,6 +5,7 @@ from typing import Any, List, Optional, TYPE_CHECKING
|
|||||||
|
|
||||||
from UM.Settings.PropertyEvaluationContext import PropertyEvaluationContext
|
from UM.Settings.PropertyEvaluationContext import PropertyEvaluationContext
|
||||||
from UM.Settings.SettingFunction import SettingFunction
|
from UM.Settings.SettingFunction import SettingFunction
|
||||||
|
from UM.Logger import Logger
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from cura.CuraApplication import CuraApplication
|
from cura.CuraApplication import CuraApplication
|
||||||
@ -38,7 +39,18 @@ class CuraFormulaFunctions:
|
|||||||
extruder_position = int(machine_manager.defaultExtruderPosition)
|
extruder_position = int(machine_manager.defaultExtruderPosition)
|
||||||
|
|
||||||
global_stack = machine_manager.activeMachine
|
global_stack = machine_manager.activeMachine
|
||||||
|
try:
|
||||||
extruder_stack = global_stack.extruders[str(extruder_position)]
|
extruder_stack = global_stack.extruders[str(extruder_position)]
|
||||||
|
except KeyError:
|
||||||
|
if extruder_position != 0:
|
||||||
|
Logger.log("w", "Value for %s of extruder %s was requested, but that extruder is not available. Returning the result form extruder 0 instead" % (property_key, extruder_position))
|
||||||
|
# This fixes a very specific fringe case; If a profile was created for a custom printer and one of the
|
||||||
|
# extruder settings has been set to non zero and the profile is loaded for a machine that has only a single extruder
|
||||||
|
# it would cause all kinds of issues (and eventually a crash).
|
||||||
|
# See https://github.com/Ultimaker/Cura/issues/5535
|
||||||
|
return self.getValueInExtruder(0, property_key, context)
|
||||||
|
Logger.log("w", "Value for %s of extruder %s was requested, but that extruder is not available. " % (property_key, extruder_position))
|
||||||
|
return None
|
||||||
|
|
||||||
value = extruder_stack.getRawProperty(property_key, "value", context = context)
|
value = extruder_stack.getRawProperty(property_key, "value", context = context)
|
||||||
if isinstance(value, SettingFunction):
|
if isinstance(value, SettingFunction):
|
||||||
|
@ -125,11 +125,16 @@ class CuraStackBuilder:
|
|||||||
|
|
||||||
extruder_definition_dict = global_stack.getMetaDataEntry("machine_extruder_trains")
|
extruder_definition_dict = global_stack.getMetaDataEntry("machine_extruder_trains")
|
||||||
extruder_definition_id = extruder_definition_dict[str(extruder_position)]
|
extruder_definition_id = extruder_definition_dict[str(extruder_position)]
|
||||||
|
try:
|
||||||
extruder_definition = registry.findDefinitionContainers(id = extruder_definition_id)[0]
|
extruder_definition = registry.findDefinitionContainers(id = extruder_definition_id)[0]
|
||||||
|
except IndexError as e:
|
||||||
|
# It still needs to break, but we want to know what extruder ID made it break.
|
||||||
|
Logger.log("e", "Unable to find extruder with the id %s", extruder_definition_id)
|
||||||
|
raise e
|
||||||
|
|
||||||
# get material container for extruders
|
# get material container for extruders
|
||||||
material_container = application.empty_material_container
|
material_container = application.empty_material_container
|
||||||
material_node = material_manager.getDefaultMaterial(global_stack, extruder_position, extruder_variant_name,
|
material_node = material_manager.getDefaultMaterial(global_stack, str(extruder_position), extruder_variant_name,
|
||||||
extruder_definition = extruder_definition)
|
extruder_definition = extruder_definition)
|
||||||
if material_node and material_node.getContainer():
|
if material_node and material_node.getContainer():
|
||||||
material_container = material_node.getContainer()
|
material_container = material_node.getContainer()
|
||||||
@ -145,7 +150,6 @@ class CuraStackBuilder:
|
|||||||
quality_container = application.empty_quality_container
|
quality_container = application.empty_quality_container
|
||||||
)
|
)
|
||||||
new_extruder.setNextStack(global_stack)
|
new_extruder.setNextStack(global_stack)
|
||||||
global_stack.addExtruder(new_extruder)
|
|
||||||
|
|
||||||
registry.addContainer(new_extruder)
|
registry.addContainer(new_extruder)
|
||||||
|
|
||||||
|
@ -12,7 +12,7 @@ from UM.Scene.SceneNode import SceneNode
|
|||||||
from UM.Scene.Selection import Selection
|
from UM.Scene.Selection import Selection
|
||||||
from UM.Scene.Iterator.BreadthFirstIterator import BreadthFirstIterator
|
from UM.Scene.Iterator.BreadthFirstIterator import BreadthFirstIterator
|
||||||
from UM.Settings.ContainerRegistry import ContainerRegistry # Finding containers by ID.
|
from UM.Settings.ContainerRegistry import ContainerRegistry # Finding containers by ID.
|
||||||
from UM.Settings.ContainerStack import ContainerStack
|
from UM.Decorators import deprecated
|
||||||
|
|
||||||
from typing import Any, cast, Dict, List, Optional, TYPE_CHECKING, Union
|
from typing import Any, cast, Dict, List, Optional, TYPE_CHECKING, Union
|
||||||
|
|
||||||
@ -63,7 +63,7 @@ class ExtruderManager(QObject):
|
|||||||
if not self._application.getGlobalContainerStack():
|
if not self._application.getGlobalContainerStack():
|
||||||
return None # No active machine, so no active extruder.
|
return None # No active machine, so no active extruder.
|
||||||
try:
|
try:
|
||||||
return self._extruder_trains[self._application.getGlobalContainerStack().getId()][str(self._active_extruder_index)].getId()
|
return self._extruder_trains[self._application.getGlobalContainerStack().getId()][str(self.activeExtruderIndex)].getId()
|
||||||
except KeyError: # Extruder index could be -1 if the global tab is selected, or the entry doesn't exist if the machine definition is wrong.
|
except KeyError: # Extruder index could be -1 if the global tab is selected, or the entry doesn't exist if the machine definition is wrong.
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@ -83,6 +83,7 @@ class ExtruderManager(QObject):
|
|||||||
# \param index The index of the new active extruder.
|
# \param index The index of the new active extruder.
|
||||||
@pyqtSlot(int)
|
@pyqtSlot(int)
|
||||||
def setActiveExtruderIndex(self, index: int) -> None:
|
def setActiveExtruderIndex(self, index: int) -> None:
|
||||||
|
if self._active_extruder_index != index:
|
||||||
self._active_extruder_index = index
|
self._active_extruder_index = index
|
||||||
self.activeExtruderChanged.emit()
|
self.activeExtruderChanged.emit()
|
||||||
|
|
||||||
@ -94,6 +95,7 @@ class ExtruderManager(QObject):
|
|||||||
#
|
#
|
||||||
# \param index The index of the extruder whose name to get.
|
# \param index The index of the extruder whose name to get.
|
||||||
@pyqtSlot(int, result = str)
|
@pyqtSlot(int, result = str)
|
||||||
|
@deprecated("Use Cura.MachineManager.activeMachine.extruders[index].name instead", "4.3")
|
||||||
def getExtruderName(self, index: int) -> str:
|
def getExtruderName(self, index: int) -> str:
|
||||||
try:
|
try:
|
||||||
return self.getActiveExtruderStacks()[index].getName()
|
return self.getActiveExtruderStacks()[index].getName()
|
||||||
@ -113,7 +115,7 @@ class ExtruderManager(QObject):
|
|||||||
selected_nodes = [] # type: List["SceneNode"]
|
selected_nodes = [] # type: List["SceneNode"]
|
||||||
for node in Selection.getAllSelectedObjects():
|
for node in Selection.getAllSelectedObjects():
|
||||||
if node.callDecoration("isGroup"):
|
if node.callDecoration("isGroup"):
|
||||||
for grouped_node in BreadthFirstIterator(node): #type: ignore #Ignore type error because iter() should get called automatically by Python syntax.
|
for grouped_node in BreadthFirstIterator(node):
|
||||||
if grouped_node.callDecoration("isGroup"):
|
if grouped_node.callDecoration("isGroup"):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@ -130,7 +132,7 @@ class ExtruderManager(QObject):
|
|||||||
elif current_extruder_trains:
|
elif current_extruder_trains:
|
||||||
object_extruders.add(current_extruder_trains[0].getId())
|
object_extruders.add(current_extruder_trains[0].getId())
|
||||||
|
|
||||||
self._selected_object_extruders = list(object_extruders) # type: List[Union[str, "ExtruderStack"]]
|
self._selected_object_extruders = list(object_extruders)
|
||||||
|
|
||||||
return self._selected_object_extruders
|
return self._selected_object_extruders
|
||||||
|
|
||||||
@ -139,12 +141,12 @@ class ExtruderManager(QObject):
|
|||||||
# This will trigger a recalculation of the extruders used for the
|
# This will trigger a recalculation of the extruders used for the
|
||||||
# selection.
|
# selection.
|
||||||
def resetSelectedObjectExtruders(self) -> None:
|
def resetSelectedObjectExtruders(self) -> None:
|
||||||
self._selected_object_extruders = [] # type: List[Union[str, "ExtruderStack"]]
|
self._selected_object_extruders = []
|
||||||
self.selectedObjectExtrudersChanged.emit()
|
self.selectedObjectExtrudersChanged.emit()
|
||||||
|
|
||||||
@pyqtSlot(result = QObject)
|
@pyqtSlot(result = QObject)
|
||||||
def getActiveExtruderStack(self) -> Optional["ExtruderStack"]:
|
def getActiveExtruderStack(self) -> Optional["ExtruderStack"]:
|
||||||
return self.getExtruderStack(self._active_extruder_index)
|
return self.getExtruderStack(self.activeExtruderIndex)
|
||||||
|
|
||||||
## Get an extruder stack by index
|
## Get an extruder stack by index
|
||||||
def getExtruderStack(self, index) -> Optional["ExtruderStack"]:
|
def getExtruderStack(self, index) -> Optional["ExtruderStack"]:
|
||||||
@ -179,7 +181,7 @@ class ExtruderManager(QObject):
|
|||||||
# \param setting_key \type{str} The setting to get the property of.
|
# \param setting_key \type{str} The setting to get the property of.
|
||||||
# \param property \type{str} The property to get.
|
# \param property \type{str} The property to get.
|
||||||
# \return \type{List} the list of results
|
# \return \type{List} the list of results
|
||||||
def getAllExtruderSettings(self, setting_key: str, prop: str) -> List:
|
def getAllExtruderSettings(self, setting_key: str, prop: str) -> List[Any]:
|
||||||
result = []
|
result = []
|
||||||
|
|
||||||
for extruder_stack in self.getActiveExtruderStacks():
|
for extruder_stack in self.getActiveExtruderStacks():
|
||||||
@ -204,7 +206,7 @@ class ExtruderManager(QObject):
|
|||||||
# list.
|
# list.
|
||||||
#
|
#
|
||||||
# \return A list of extruder stacks.
|
# \return A list of extruder stacks.
|
||||||
def getUsedExtruderStacks(self) -> List["ContainerStack"]:
|
def getUsedExtruderStacks(self) -> List["ExtruderStack"]:
|
||||||
global_stack = self._application.getGlobalContainerStack()
|
global_stack = self._application.getGlobalContainerStack()
|
||||||
container_registry = ContainerRegistry.getInstance()
|
container_registry = ContainerRegistry.getInstance()
|
||||||
|
|
||||||
@ -223,7 +225,16 @@ class ExtruderManager(QObject):
|
|||||||
|
|
||||||
# Get the extruders of all printable meshes in the scene
|
# Get the extruders of all printable meshes in the scene
|
||||||
meshes = [node for node in DepthFirstIterator(scene_root) if isinstance(node, SceneNode) and node.isSelectable()] #type: ignore #Ignore type error because iter() should get called automatically by Python syntax.
|
meshes = [node for node in DepthFirstIterator(scene_root) if isinstance(node, SceneNode) and node.isSelectable()] #type: ignore #Ignore type error because iter() should get called automatically by Python syntax.
|
||||||
|
|
||||||
|
# Exclude anti-overhang meshes
|
||||||
|
mesh_list = []
|
||||||
for mesh in meshes:
|
for mesh in meshes:
|
||||||
|
stack = mesh.callDecoration("getStack")
|
||||||
|
if stack is not None and (stack.getProperty("anti_overhang_mesh", "value") or stack.getProperty("support_mesh", "value")):
|
||||||
|
continue
|
||||||
|
mesh_list.append(mesh)
|
||||||
|
|
||||||
|
for mesh in mesh_list:
|
||||||
extruder_stack_id = mesh.callDecoration("getActiveExtruder")
|
extruder_stack_id = mesh.callDecoration("getActiveExtruder")
|
||||||
if not extruder_stack_id:
|
if not extruder_stack_id:
|
||||||
# No per-object settings for this node
|
# No per-object settings for this node
|
||||||
@ -263,10 +274,13 @@ class ExtruderManager(QObject):
|
|||||||
used_extruder_stack_ids.add(self.extruderIds[self.extruderValueWithDefault(str(global_stack.getProperty("support_roof_extruder_nr", "value")))])
|
used_extruder_stack_ids.add(self.extruderIds[self.extruderValueWithDefault(str(global_stack.getProperty("support_roof_extruder_nr", "value")))])
|
||||||
|
|
||||||
# The platform adhesion extruder. Not used if using none.
|
# The platform adhesion extruder. Not used if using none.
|
||||||
if global_stack.getProperty("adhesion_type", "value") != "none":
|
if global_stack.getProperty("adhesion_type", "value") != "none" or (
|
||||||
|
global_stack.getProperty("prime_tower_brim_enable", "value") and
|
||||||
|
global_stack.getProperty("adhesion_type", "value") != 'raft'):
|
||||||
extruder_str_nr = str(global_stack.getProperty("adhesion_extruder_nr", "value"))
|
extruder_str_nr = str(global_stack.getProperty("adhesion_extruder_nr", "value"))
|
||||||
if extruder_str_nr == "-1":
|
if extruder_str_nr == "-1":
|
||||||
extruder_str_nr = self._application.getMachineManager().defaultExtruderPosition
|
extruder_str_nr = self._application.getMachineManager().defaultExtruderPosition
|
||||||
|
if extruder_str_nr in self.extruderIds:
|
||||||
used_extruder_stack_ids.add(self.extruderIds[extruder_str_nr])
|
used_extruder_stack_ids.add(self.extruderIds[extruder_str_nr])
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@ -300,12 +314,7 @@ class ExtruderManager(QObject):
|
|||||||
global_stack = self._application.getGlobalContainerStack()
|
global_stack = self._application.getGlobalContainerStack()
|
||||||
if not global_stack:
|
if not global_stack:
|
||||||
return []
|
return []
|
||||||
|
return global_stack.extruderList
|
||||||
result_tuple_list = sorted(list(global_stack.extruders.items()), key = lambda x: int(x[0]))
|
|
||||||
result_list = [item[1] for item in result_tuple_list]
|
|
||||||
|
|
||||||
machine_extruder_count = global_stack.getProperty("machine_extruder_count", "value")
|
|
||||||
return result_list[:machine_extruder_count]
|
|
||||||
|
|
||||||
def _globalContainerStackChanged(self) -> None:
|
def _globalContainerStackChanged(self) -> None:
|
||||||
# If the global container changed, the machine changed and might have extruders that were not registered yet
|
# If the global container changed, the machine changed and might have extruders that were not registered yet
|
||||||
@ -340,14 +349,15 @@ class ExtruderManager(QObject):
|
|||||||
extruder_train.setNextStack(global_stack)
|
extruder_train.setNextStack(global_stack)
|
||||||
extruders_changed = True
|
extruders_changed = True
|
||||||
|
|
||||||
self._fixSingleExtrusionMachineExtruderDefinition(global_stack)
|
self.fixSingleExtrusionMachineExtruderDefinition(global_stack)
|
||||||
if extruders_changed:
|
if extruders_changed:
|
||||||
self.extrudersChanged.emit(global_stack_id)
|
self.extrudersChanged.emit(global_stack_id)
|
||||||
self.setActiveExtruderIndex(0)
|
self.setActiveExtruderIndex(0)
|
||||||
|
self.activeExtruderChanged.emit()
|
||||||
|
|
||||||
# After 3.4, all single-extrusion machines have their own extruder definition files instead of reusing
|
# After 3.4, all single-extrusion machines have their own extruder definition files instead of reusing
|
||||||
# "fdmextruder". We need to check a machine here so its extruder definition is correct according to this.
|
# "fdmextruder". We need to check a machine here so its extruder definition is correct according to this.
|
||||||
def _fixSingleExtrusionMachineExtruderDefinition(self, global_stack: "GlobalStack") -> None:
|
def fixSingleExtrusionMachineExtruderDefinition(self, global_stack: "GlobalStack") -> None:
|
||||||
container_registry = ContainerRegistry.getInstance()
|
container_registry = ContainerRegistry.getInstance()
|
||||||
expected_extruder_definition_0_id = global_stack.getMetaDataEntry("machine_extruder_trains")["0"]
|
expected_extruder_definition_0_id = global_stack.getMetaDataEntry("machine_extruder_trains")["0"]
|
||||||
extruder_stack_0 = global_stack.extruders.get("0")
|
extruder_stack_0 = global_stack.extruders.get("0")
|
||||||
@ -374,8 +384,6 @@ class ExtruderManager(QObject):
|
|||||||
extruder_definition = container_registry.findDefinitionContainers(id = expected_extruder_definition_0_id)[0]
|
extruder_definition = container_registry.findDefinitionContainers(id = expected_extruder_definition_0_id)[0]
|
||||||
extruder_stack_0.definition = extruder_definition
|
extruder_stack_0.definition = extruder_definition
|
||||||
|
|
||||||
extruder_stack_0.setNextStack(global_stack)
|
|
||||||
|
|
||||||
## Get all extruder values for a certain setting.
|
## Get all extruder values for a certain setting.
|
||||||
#
|
#
|
||||||
# This is exposed to qml for display purposes
|
# This is exposed to qml for display purposes
|
||||||
|
@ -52,8 +52,8 @@ class ExtruderStack(CuraContainerStack):
|
|||||||
return super().getNextStack()
|
return super().getNextStack()
|
||||||
|
|
||||||
def setEnabled(self, enabled: bool) -> None:
|
def setEnabled(self, enabled: bool) -> None:
|
||||||
if "enabled" not in self._metadata:
|
if self.getMetaDataEntry("enabled", True) == enabled: # No change.
|
||||||
self.setMetaDataEntry("enabled", "True")
|
return # Don't emit a signal then.
|
||||||
self.setMetaDataEntry("enabled", str(enabled))
|
self.setMetaDataEntry("enabled", str(enabled))
|
||||||
self.enabledChanged.emit()
|
self.enabledChanged.emit()
|
||||||
|
|
||||||
@ -65,16 +65,33 @@ class ExtruderStack(CuraContainerStack):
|
|||||||
def getLoadingPriority(cls) -> int:
|
def getLoadingPriority(cls) -> int:
|
||||||
return 3
|
return 3
|
||||||
|
|
||||||
|
compatibleMaterialDiameterChanged = pyqtSignal()
|
||||||
|
|
||||||
## Return the filament diameter that the machine requires.
|
## Return the filament diameter that the machine requires.
|
||||||
#
|
#
|
||||||
# If the machine has no requirement for the diameter, -1 is returned.
|
# If the machine has no requirement for the diameter, -1 is returned.
|
||||||
# \return The filament diameter for the printer
|
# \return The filament diameter for the printer
|
||||||
@property
|
def getCompatibleMaterialDiameter(self) -> float:
|
||||||
def materialDiameter(self) -> float:
|
|
||||||
context = PropertyEvaluationContext(self)
|
context = PropertyEvaluationContext(self)
|
||||||
context.context["evaluate_from_container_index"] = _ContainerIndexes.Variant
|
context.context["evaluate_from_container_index"] = _ContainerIndexes.Variant
|
||||||
|
|
||||||
return self.getProperty("material_diameter", "value", context = context)
|
return float(self.getProperty("material_diameter", "value", context = context))
|
||||||
|
|
||||||
|
def setCompatibleMaterialDiameter(self, value: float) -> None:
|
||||||
|
old_approximate_diameter = self.getApproximateMaterialDiameter()
|
||||||
|
if self.getCompatibleMaterialDiameter() != value:
|
||||||
|
self.definitionChanges.setProperty("material_diameter", "value", value)
|
||||||
|
self.compatibleMaterialDiameterChanged.emit()
|
||||||
|
|
||||||
|
# Emit approximate diameter changed signal if needed
|
||||||
|
if old_approximate_diameter != self.getApproximateMaterialDiameter():
|
||||||
|
self.approximateMaterialDiameterChanged.emit()
|
||||||
|
|
||||||
|
compatibleMaterialDiameter = pyqtProperty(float, fset = setCompatibleMaterialDiameter,
|
||||||
|
fget = getCompatibleMaterialDiameter,
|
||||||
|
notify = compatibleMaterialDiameterChanged)
|
||||||
|
|
||||||
|
approximateMaterialDiameterChanged = pyqtSignal()
|
||||||
|
|
||||||
## Return the approximate filament diameter that the machine requires.
|
## Return the approximate filament diameter that the machine requires.
|
||||||
#
|
#
|
||||||
@ -84,9 +101,11 @@ class ExtruderStack(CuraContainerStack):
|
|||||||
# If the machine has no requirement for the diameter, -1 is returned.
|
# If the machine has no requirement for the diameter, -1 is returned.
|
||||||
#
|
#
|
||||||
# \return The approximate filament diameter for the printer
|
# \return The approximate filament diameter for the printer
|
||||||
@pyqtProperty(float)
|
def getApproximateMaterialDiameter(self) -> float:
|
||||||
def approximateMaterialDiameter(self) -> float:
|
return round(self.getCompatibleMaterialDiameter())
|
||||||
return round(float(self.materialDiameter))
|
|
||||||
|
approximateMaterialDiameter = pyqtProperty(float, fget = getApproximateMaterialDiameter,
|
||||||
|
notify = approximateMaterialDiameterChanged)
|
||||||
|
|
||||||
## Overridden from ContainerStack
|
## Overridden from ContainerStack
|
||||||
#
|
#
|
||||||
|
@ -3,8 +3,10 @@
|
|||||||
|
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
import threading
|
import threading
|
||||||
from typing import Any, Dict, Optional, Set, TYPE_CHECKING
|
from typing import Any, Dict, Optional, Set, TYPE_CHECKING, List
|
||||||
from PyQt5.QtCore import pyqtProperty
|
import uuid
|
||||||
|
|
||||||
|
from PyQt5.QtCore import pyqtProperty, pyqtSlot, pyqtSignal
|
||||||
|
|
||||||
from UM.Decorators import override
|
from UM.Decorators import override
|
||||||
from UM.MimeTypeDatabase import MimeType, MimeTypeDatabase
|
from UM.MimeTypeDatabase import MimeType, MimeTypeDatabase
|
||||||
@ -13,6 +15,8 @@ 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
|
||||||
|
from UM.Resources import Resources
|
||||||
|
from UM.Platform import Platform
|
||||||
from UM.Util import parseBool
|
from UM.Util import parseBool
|
||||||
|
|
||||||
import cura.CuraApplication
|
import cura.CuraApplication
|
||||||
@ -32,6 +36,12 @@ class GlobalStack(CuraContainerStack):
|
|||||||
|
|
||||||
self.setMetaDataEntry("type", "machine") # For backward compatibility
|
self.setMetaDataEntry("type", "machine") # For backward compatibility
|
||||||
|
|
||||||
|
# TL;DR: If Cura is looking for printers that belong to the same group, it should use "group_id".
|
||||||
|
# Each GlobalStack by default belongs to a group which is identified via "group_id". This group_id is used to
|
||||||
|
# figure out which GlobalStacks are in the printer cluster for example without knowing the implementation
|
||||||
|
# details such as the um_network_key or some other identifier that's used by the underlying device plugin.
|
||||||
|
self.setMetaDataEntry("group_id", str(uuid.uuid4())) # Assign a new GlobalStack to a unique group by default
|
||||||
|
|
||||||
self._extruders = {} # type: Dict[str, "ExtruderStack"]
|
self._extruders = {} # type: Dict[str, "ExtruderStack"]
|
||||||
|
|
||||||
# This property is used to track which settings we are calculating the "resolve" for
|
# This property is used to track which settings we are calculating the "resolve" for
|
||||||
@ -40,17 +50,79 @@ class GlobalStack(CuraContainerStack):
|
|||||||
# Per thread we have our own resolving_settings, or strange things sometimes occur.
|
# Per thread we have our own resolving_settings, or strange things sometimes occur.
|
||||||
self._resolving_settings = defaultdict(set) #type: Dict[str, Set[str]] # keys are thread names
|
self._resolving_settings = defaultdict(set) #type: Dict[str, Set[str]] # keys are thread names
|
||||||
|
|
||||||
|
# Since the metadatachanged is defined in container stack, we can't use it here as a notifier for pyqt
|
||||||
|
# properties. So we need to tie them together like this.
|
||||||
|
self.metaDataChanged.connect(self.configuredConnectionTypesChanged)
|
||||||
|
|
||||||
|
extrudersChanged = pyqtSignal()
|
||||||
|
configuredConnectionTypesChanged = pyqtSignal()
|
||||||
|
|
||||||
## Get the list of extruders of this stack.
|
## Get the list of extruders of this stack.
|
||||||
#
|
#
|
||||||
# \return The extruders registered with this stack.
|
# \return The extruders registered with this stack.
|
||||||
@pyqtProperty("QVariantMap")
|
@pyqtProperty("QVariantMap", notify = extrudersChanged)
|
||||||
def extruders(self) -> Dict[str, "ExtruderStack"]:
|
def extruders(self) -> Dict[str, "ExtruderStack"]:
|
||||||
return self._extruders
|
return self._extruders
|
||||||
|
|
||||||
|
@pyqtProperty("QVariantList", notify = extrudersChanged)
|
||||||
|
def extruderList(self) -> List["ExtruderStack"]:
|
||||||
|
result_tuple_list = sorted(list(self.extruders.items()), key=lambda x: int(x[0]))
|
||||||
|
result_list = [item[1] for item in result_tuple_list]
|
||||||
|
|
||||||
|
machine_extruder_count = self.getProperty("machine_extruder_count", "value")
|
||||||
|
return result_list[:machine_extruder_count]
|
||||||
|
|
||||||
|
@pyqtProperty(int, constant = True)
|
||||||
|
def maxExtruderCount(self):
|
||||||
|
return len(self.getMetaDataEntry("machine_extruder_trains"))
|
||||||
|
|
||||||
|
@pyqtProperty(bool, notify=configuredConnectionTypesChanged)
|
||||||
|
def supportsNetworkConnection(self):
|
||||||
|
return self.getMetaDataEntry("supports_network_connection", False)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def getLoadingPriority(cls) -> int:
|
def getLoadingPriority(cls) -> int:
|
||||||
return 2
|
return 2
|
||||||
|
|
||||||
|
## 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
|
||||||
|
# know all the details as to how this is exactly done (and without
|
||||||
|
# actually setting the stack to be active).
|
||||||
|
#
|
||||||
|
# This data can then in turn also be used when the global stack is active;
|
||||||
|
# If we can't get a network connection, but it is configured to have one,
|
||||||
|
# we can display a different icon to indicate the difference.
|
||||||
|
@pyqtProperty("QVariantList", notify=configuredConnectionTypesChanged)
|
||||||
|
def configuredConnectionTypes(self) -> List[int]:
|
||||||
|
# Requesting it from the metadata actually gets them as strings (as that's what you get from serializing).
|
||||||
|
# But we do want them returned as a list of ints (so the rest of the code can directly compare)
|
||||||
|
connection_types = self.getMetaDataEntry("connection_type", "").split(",")
|
||||||
|
result = []
|
||||||
|
for connection_type in connection_types:
|
||||||
|
if connection_type != "":
|
||||||
|
try:
|
||||||
|
result.append(int(connection_type))
|
||||||
|
except ValueError:
|
||||||
|
# We got invalid data, probably a None.
|
||||||
|
pass
|
||||||
|
return result
|
||||||
|
|
||||||
|
## \sa configuredConnectionTypes
|
||||||
|
def addConfiguredConnectionType(self, connection_type: int) -> None:
|
||||||
|
configured_connection_types = self.configuredConnectionTypes
|
||||||
|
if connection_type not in configured_connection_types:
|
||||||
|
# Store the values as a string.
|
||||||
|
configured_connection_types.append(connection_type)
|
||||||
|
self.setMetaDataEntry("connection_type", ",".join([str(c_type) for c_type in configured_connection_types]))
|
||||||
|
|
||||||
|
## \sa configuredConnectionTypes
|
||||||
|
def removeConfiguredConnectionType(self, connection_type: int) -> None:
|
||||||
|
configured_connection_types = self.configuredConnectionTypes
|
||||||
|
if connection_type in self.configured_connection_types:
|
||||||
|
# Store the values as a string.
|
||||||
|
configured_connection_types.remove(connection_type)
|
||||||
|
self.setMetaDataEntry("connection_type", ",".join([str(c_type) for c_type in configured_connection_types]))
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def getConfigurationTypeFromSerialized(cls, serialized: str) -> Optional[str]:
|
def getConfigurationTypeFromSerialized(cls, serialized: str) -> Optional[str]:
|
||||||
configuration_type = super().getConfigurationTypeFromSerialized(serialized)
|
configuration_type = super().getConfigurationTypeFromSerialized(serialized)
|
||||||
@ -85,6 +157,7 @@ class GlobalStack(CuraContainerStack):
|
|||||||
return
|
return
|
||||||
|
|
||||||
self._extruders[position] = extruder
|
self._extruders[position] = extruder
|
||||||
|
self.extrudersChanged.emit()
|
||||||
Logger.log("i", "Extruder[%s] added to [%s] at position [%s]", extruder.id, self.id, position)
|
Logger.log("i", "Extruder[%s] added to [%s] at position [%s]", extruder.id, self.id, position)
|
||||||
|
|
||||||
## Overridden from ContainerStack
|
## Overridden from ContainerStack
|
||||||
@ -151,7 +224,7 @@ class GlobalStack(CuraContainerStack):
|
|||||||
# Determine whether or not we should try to get the "resolve" property instead of the
|
# Determine whether or not we should try to get the "resolve" property instead of the
|
||||||
# requested property.
|
# requested property.
|
||||||
def _shouldResolve(self, key: str, property_name: str, context: Optional[PropertyEvaluationContext] = None) -> bool:
|
def _shouldResolve(self, key: str, property_name: str, context: Optional[PropertyEvaluationContext] = None) -> bool:
|
||||||
if property_name is not "value":
|
if property_name != "value":
|
||||||
# Do not try to resolve anything but the "value" property
|
# Do not try to resolve anything but the "value" property
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@ -191,15 +264,43 @@ class GlobalStack(CuraContainerStack):
|
|||||||
def getHeadAndFansCoordinates(self):
|
def getHeadAndFansCoordinates(self):
|
||||||
return self.getProperty("machine_head_with_fans_polygon", "value")
|
return self.getProperty("machine_head_with_fans_polygon", "value")
|
||||||
|
|
||||||
def getHasMaterials(self) -> bool:
|
@pyqtProperty(int, constant=True)
|
||||||
|
def hasMaterials(self):
|
||||||
return parseBool(self.getMetaDataEntry("has_materials", False))
|
return parseBool(self.getMetaDataEntry("has_materials", False))
|
||||||
|
|
||||||
def getHasVariants(self) -> bool:
|
@pyqtProperty(int, constant=True)
|
||||||
|
def hasVariants(self):
|
||||||
return parseBool(self.getMetaDataEntry("has_variants", False))
|
return parseBool(self.getMetaDataEntry("has_variants", False))
|
||||||
|
|
||||||
def getHasMachineQuality(self) -> bool:
|
@pyqtProperty(int, constant=True)
|
||||||
return parseBool(self.getMetaDataEntry("has_machine_quality", False))
|
def hasVariantBuildplates(self) -> bool:
|
||||||
|
return parseBool(self.getMetaDataEntry("has_variant_buildplates", False))
|
||||||
|
|
||||||
|
## Get default firmware file name if one is specified in the firmware
|
||||||
|
@pyqtSlot(result = str)
|
||||||
|
def getDefaultFirmwareName(self) -> str:
|
||||||
|
machine_has_heated_bed = self.getProperty("machine_heated_bed", "value")
|
||||||
|
|
||||||
|
baudrate = 250000
|
||||||
|
if Platform.isLinux():
|
||||||
|
# Linux prefers a baudrate of 115200 here because older versions of
|
||||||
|
# pySerial did not support a baudrate of 250000
|
||||||
|
baudrate = 115200
|
||||||
|
|
||||||
|
# If a firmware file is available, it should be specified in the definition for the printer
|
||||||
|
hex_file = self.getMetaDataEntry("firmware_file", None)
|
||||||
|
if machine_has_heated_bed:
|
||||||
|
hex_file = self.getMetaDataEntry("firmware_hbk_file", hex_file)
|
||||||
|
|
||||||
|
if not hex_file:
|
||||||
|
Logger.log("w", "There is no firmware for machine %s.", self.getBottom().id)
|
||||||
|
return ""
|
||||||
|
|
||||||
|
try:
|
||||||
|
return Resources.getPath(cura.CuraApplication.CuraApplication.ResourceTypes.Firmware, hex_file.format(baudrate=baudrate))
|
||||||
|
except FileNotFoundError:
|
||||||
|
Logger.log("w", "Firmware file %s not found.", hex_file)
|
||||||
|
return ""
|
||||||
|
|
||||||
## private:
|
## private:
|
||||||
global_stack_mime = MimeType(
|
global_stack_mime = MimeType(
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -34,7 +34,7 @@ class PerObjectContainerStack(CuraContainerStack):
|
|||||||
if limit_to_extruder is not None:
|
if limit_to_extruder is not None:
|
||||||
limit_to_extruder = str(limit_to_extruder)
|
limit_to_extruder = str(limit_to_extruder)
|
||||||
|
|
||||||
# if this stack has the limit_to_extruder "not overriden", use the original limit_to_extruder as the current
|
# if this stack has the limit_to_extruder "not overridden", use the original limit_to_extruder as the current
|
||||||
# limit_to_extruder, so the values retrieved will be from the perspective of the original limit_to_extruder
|
# limit_to_extruder, so the values retrieved will be from the perspective of the original limit_to_extruder
|
||||||
# stack.
|
# stack.
|
||||||
if limit_to_extruder == "-1":
|
if limit_to_extruder == "-1":
|
||||||
@ -42,7 +42,7 @@ class PerObjectContainerStack(CuraContainerStack):
|
|||||||
limit_to_extruder = context.context["original_limit_to_extruder"]
|
limit_to_extruder = context.context["original_limit_to_extruder"]
|
||||||
|
|
||||||
if limit_to_extruder is not None and limit_to_extruder != "-1" and limit_to_extruder in global_stack.extruders:
|
if limit_to_extruder is not None and limit_to_extruder != "-1" and limit_to_extruder in global_stack.extruders:
|
||||||
# set the original limit_to_extruder if this is the first stack that has a non-overriden limit_to_extruder
|
# set the original limit_to_extruder if this is the first stack that has a non-overridden limit_to_extruder
|
||||||
if "original_limit_to_extruder" not in context.context:
|
if "original_limit_to_extruder" not in context.context:
|
||||||
context.context["original_limit_to_extruder"] = limit_to_extruder
|
context.context["original_limit_to_extruder"] = limit_to_extruder
|
||||||
|
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user