mirror of
https://git.mirrors.martin98.com/https://github.com/Ultimaker/Cura
synced 2025-09-24 13:43:12 +08:00
Merge branch 'master' into feature_z_hop_extruder_switch
This commit is contained in:
commit
fec0272c5f
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
|
||||||
|
12
.gitlab-ci.yml
Normal file
12
.gitlab-ci.yml
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
image: registry.gitlab.com/ultimaker/cura/cura-build-environment:centos7
|
||||||
|
|
||||||
|
stages:
|
||||||
|
- build
|
||||||
|
|
||||||
|
build-and-test:
|
||||||
|
stage: build
|
||||||
|
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,6 +16,8 @@ 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_SDK_VERSION "" CACHE STRING "SDK version of Cura")
|
||||||
@ -26,6 +27,26 @@ set(CURA_CLOUD_API_VERSION "" CACHE STRING "Alternative Cura cloud 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 +59,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 +72,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 +97,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()
|
||||||
|
43
Jenkinsfile
vendored
43
Jenkinsfile
vendored
@ -38,20 +38,9 @@ parallel_nodes(['linux && cura', 'windows && cura'])
|
|||||||
{
|
{
|
||||||
if (isUnix())
|
if (isUnix())
|
||||||
{
|
{
|
||||||
// For Linux to show everything
|
// For Linux
|
||||||
def branch = env.BRANCH_NAME
|
|
||||||
if(!fileExists("${env.CURA_ENVIRONMENT_PATH}/${branch}"))
|
|
||||||
{
|
|
||||||
branch = "master"
|
|
||||||
}
|
|
||||||
def uranium_dir = get_workspace_dir("Ultimaker/Uranium/${branch}")
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
sh """
|
sh 'make CTEST_OUTPUT_ON_FAILURE=TRUE test'
|
||||||
cd ..
|
|
||||||
export PYTHONPATH=.:"${uranium_dir}"
|
|
||||||
${env.CURA_ENVIRONMENT_PATH}/${branch}/bin/pytest -x --verbose --full-trace --capture=no ./tests
|
|
||||||
"""
|
|
||||||
} catch(e)
|
} catch(e)
|
||||||
{
|
{
|
||||||
currentBuild.result = "UNSTABLE"
|
currentBuild.result = "UNSTABLE"
|
||||||
@ -70,34 +59,6 @@ parallel_nodes(['linux && cura', 'windows && cura'])
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
stage('Code Style')
|
|
||||||
{
|
|
||||||
if (isUnix())
|
|
||||||
{
|
|
||||||
// For Linux to show everything.
|
|
||||||
// CMake also runs this test, but if it fails then the test just shows "failed" without details of what exactly failed.
|
|
||||||
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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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 --verbose --full-trace --capture=no --no-print-log --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,13 +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
|
COMMAND ${Python3_EXECUTABLE} run_mypy.py
|
||||||
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
|
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
|
||||||
)
|
)
|
||||||
|
|
||||||
#Add test for whether the shortcut alt-keys are unique in every translation.
|
#Add test for whether the shortcut alt-keys are unique in every translation.
|
||||||
add_test(
|
add_test(
|
||||||
NAME "shortcut-keys"
|
NAME "shortcut-keys"
|
||||||
COMMAND ${PYTHON_EXECUTABLE} scripts/check_shortcut_keys.py
|
COMMAND ${Python3_EXECUTABLE} scripts/check_shortcut_keys.py
|
||||||
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
|
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
|
||||||
@ -37,15 +38,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="account.user.read drive.backup.read drive.backup.write packages.download packages.rating.read packages.rating.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 +57,15 @@ 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.loadAuthDataFromPreferences()
|
self._authorization_service.loadAuthDataFromPreferences()
|
||||||
|
|
||||||
|
## 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 +76,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
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
50
cura/ApplicationMetadata.py
Normal file
50
cura/ApplicationMetadata.py
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
# 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.0.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
|
||||||
|
|
||||||
|
try:
|
||||||
|
from cura.CuraVersion import CuraSDKVersion # type: ignore
|
||||||
|
if CuraSDKVersion == "":
|
||||||
|
CuraSDKVersion = DEFAULT_CURA_SDK_VERSION
|
||||||
|
except ImportError:
|
||||||
|
CuraSDKVersion = DEFAULT_CURA_SDK_VERSION
|
@ -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
|
||||||
|
@ -42,7 +42,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, min_offset, scale = 0.5, include_children = False):
|
||||||
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 +52,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
|
||||||
|
@ -46,12 +46,13 @@ 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))
|
||||||
Logger.log("d", "Copying preferences file from %s to %s", preferences_file, backup_preferences_file)
|
if os.path.exists(preferences_file) and (not os.path.exists(backup_preferences_file) or not os.path.samefile(preferences_file, backup_preferences_file)):
|
||||||
shutil.copyfile(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)
|
||||||
|
|
||||||
# Create an empty buffer and write the archive to it.
|
# Create an empty buffer and write the archive to it.
|
||||||
buffer = io.BytesIO()
|
buffer = io.BytesIO()
|
||||||
@ -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()
|
||||||
|
@ -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 UM.Scene.Camera import Camera
|
||||||
from cura.Scene.CuraSceneNode import CuraSceneNode
|
from cura.Scene.CuraSceneNode import CuraSceneNode
|
||||||
from cura.Settings.ExtruderManager import ExtruderManager
|
from cura.Settings.ExtruderManager import ExtruderManager
|
||||||
from UM.Application import Application #To modify the maximum zoom level.
|
from UM.Application import Application #To modify the maximum zoom level.
|
||||||
@ -83,7 +83,14 @@ class BuildVolume(SceneNode):
|
|||||||
" with printed models."), title = catalog.i18nc("@info:title", "Build Volume"))
|
" with printed models."), title = catalog.i18nc("@info:title", "Build Volume"))
|
||||||
|
|
||||||
self._global_container_stack = None
|
self._global_container_stack = None
|
||||||
|
|
||||||
|
self._stack_change_timer = QTimer()
|
||||||
|
self._stack_change_timer.setInterval(100)
|
||||||
|
self._stack_change_timer.setSingleShot(True)
|
||||||
|
self._stack_change_timer.timeout.connect(self._onStackChangeTimerFinished)
|
||||||
|
|
||||||
self._application.globalContainerStackChanged.connect(self._onStackChanged)
|
self._application.globalContainerStackChanged.connect(self._onStackChanged)
|
||||||
|
|
||||||
self._onStackChanged()
|
self._onStackChanged()
|
||||||
|
|
||||||
self._engine_ready = False
|
self._engine_ready = False
|
||||||
@ -122,7 +129,9 @@ class BuildVolume(SceneNode):
|
|||||||
|
|
||||||
def _onSceneChanged(self, source):
|
def _onSceneChanged(self, source):
|
||||||
if self._global_container_stack:
|
if self._global_container_stack:
|
||||||
self._scene_change_timer.start()
|
# Ignore anything that is not something we can slice in the first place!
|
||||||
|
if source.callDecoration("isSliceable"):
|
||||||
|
self._scene_change_timer.start()
|
||||||
|
|
||||||
def _onSceneChangeTimerFinished(self):
|
def _onSceneChangeTimerFinished(self):
|
||||||
root = self._application.getController().getScene().getRoot()
|
root = self._application.getController().getScene().getRoot()
|
||||||
@ -139,7 +148,7 @@ class BuildVolume(SceneNode):
|
|||||||
if active_extruder_changed is not None:
|
if active_extruder_changed is not None:
|
||||||
node.callDecoration("getActiveExtruderChangedSignal").disconnect(self._updateDisallowedAreasAndRebuild)
|
node.callDecoration("getActiveExtruderChangedSignal").disconnect(self._updateDisallowedAreasAndRebuild)
|
||||||
node.decoratorsChanged.disconnect(self._updateNodeListeners)
|
node.decoratorsChanged.disconnect(self._updateNodeListeners)
|
||||||
self._updateDisallowedAreasAndRebuild() # make sure we didn't miss anything before we updated the node listeners
|
self.rebuild()
|
||||||
|
|
||||||
self._scene_objects = new_scene_objects
|
self._scene_objects = new_scene_objects
|
||||||
self._onSettingPropertyChanged("print_sequence", "value") # Create fake event, so right settings are triggered.
|
self._onSettingPropertyChanged("print_sequence", "value") # Create fake event, so right settings are triggered.
|
||||||
@ -479,6 +488,8 @@ class BuildVolume(SceneNode):
|
|||||||
maximum = Vector(max_w - bed_adhesion_size - 1, max_h - self._raft_thickness - self._extra_z_clearance, max_d - disallowed_area_size + bed_adhesion_size - 1)
|
maximum = Vector(max_w - bed_adhesion_size - 1, max_h - self._raft_thickness - self._extra_z_clearance, max_d - disallowed_area_size + bed_adhesion_size - 1)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
self._application.getController().getScene()._maximum_bounds = scale_to_max_bounds
|
||||||
|
|
||||||
self.updateNodeBoundaryCheck()
|
self.updateNodeBoundaryCheck()
|
||||||
|
|
||||||
def getBoundingBox(self) -> AxisAlignedBox:
|
def getBoundingBox(self) -> AxisAlignedBox:
|
||||||
@ -489,7 +500,9 @@ class BuildVolume(SceneNode):
|
|||||||
|
|
||||||
def _updateRaftThickness(self):
|
def _updateRaftThickness(self):
|
||||||
old_raft_thickness = self._raft_thickness
|
old_raft_thickness = self._raft_thickness
|
||||||
self._adhesion_type = self._global_container_stack.getProperty("adhesion_type", "value")
|
if self._global_container_stack.extruders:
|
||||||
|
# This might be called before the extruder stacks have initialised, in which case getting the adhesion_type fails
|
||||||
|
self._adhesion_type = self._global_container_stack.getProperty("adhesion_type", "value")
|
||||||
self._raft_thickness = 0.0
|
self._raft_thickness = 0.0
|
||||||
if self._adhesion_type == "raft":
|
if self._adhesion_type == "raft":
|
||||||
self._raft_thickness = (
|
self._raft_thickness = (
|
||||||
@ -522,8 +535,11 @@ class BuildVolume(SceneNode):
|
|||||||
if extra_z != self._extra_z_clearance:
|
if extra_z != self._extra_z_clearance:
|
||||||
self._extra_z_clearance = extra_z
|
self._extra_z_clearance = extra_z
|
||||||
|
|
||||||
## Update the build volume visualization
|
|
||||||
def _onStackChanged(self):
|
def _onStackChanged(self):
|
||||||
|
self._stack_change_timer.start()
|
||||||
|
|
||||||
|
## Update the build volume visualization
|
||||||
|
def _onStackChangeTimerFinished(self):
|
||||||
if self._global_container_stack:
|
if self._global_container_stack:
|
||||||
self._global_container_stack.propertyChanged.disconnect(self._onSettingPropertyChanged)
|
self._global_container_stack.propertyChanged.disconnect(self._onSettingPropertyChanged)
|
||||||
extruders = ExtruderManager.getInstance().getActiveExtruderStacks()
|
extruders = ExtruderManager.getInstance().getActiveExtruderStacks()
|
||||||
@ -651,6 +667,7 @@ class BuildVolume(SceneNode):
|
|||||||
# ``_updateDisallowedAreas`` method itself shouldn't call ``rebuild``,
|
# ``_updateDisallowedAreas`` method itself shouldn't call ``rebuild``,
|
||||||
# since there may be other changes before it needs to be rebuilt, which
|
# since there may be other changes before it needs to be rebuilt, which
|
||||||
# would hit performance.
|
# would hit performance.
|
||||||
|
|
||||||
def _updateDisallowedAreasAndRebuild(self):
|
def _updateDisallowedAreasAndRebuild(self):
|
||||||
self._updateDisallowedAreas()
|
self._updateDisallowedAreas()
|
||||||
self._updateRaftThickness()
|
self._updateRaftThickness()
|
||||||
@ -723,13 +740,17 @@ class BuildVolume(SceneNode):
|
|||||||
prime_tower_collision = False
|
prime_tower_collision = False
|
||||||
prime_tower_areas = self._computeDisallowedAreasPrinted(used_extruders)
|
prime_tower_areas = self._computeDisallowedAreasPrinted(used_extruders)
|
||||||
for extruder_id in prime_tower_areas:
|
for extruder_id in prime_tower_areas:
|
||||||
for prime_tower_area in prime_tower_areas[extruder_id]:
|
for i_area, prime_tower_area in enumerate(prime_tower_areas[extruder_id]):
|
||||||
for area in result_areas[extruder_id]:
|
for area in result_areas[extruder_id]:
|
||||||
if prime_tower_area.intersectsPolygon(area) is not None:
|
if prime_tower_area.intersectsPolygon(area) is not None:
|
||||||
prime_tower_collision = True
|
prime_tower_collision = True
|
||||||
break
|
break
|
||||||
if prime_tower_collision: #Already found a collision.
|
if prime_tower_collision: #Already found a collision.
|
||||||
break
|
break
|
||||||
|
if (ExtruderManager.getInstance().getResolveOrValue("prime_tower_brim_enable") and
|
||||||
|
ExtruderManager.getInstance().getResolveOrValue("adhesion_type") != "raft"):
|
||||||
|
prime_tower_areas[extruder_id][i_area] = prime_tower_area.getMinkowskiHull(
|
||||||
|
Polygon.approximatedCircle(disallowed_border_size))
|
||||||
if not prime_tower_collision:
|
if not prime_tower_collision:
|
||||||
result_areas[extruder_id].extend(prime_tower_areas[extruder_id])
|
result_areas[extruder_id].extend(prime_tower_areas[extruder_id])
|
||||||
result_areas_no_brim[extruder_id].extend(prime_tower_areas[extruder_id])
|
result_areas_no_brim[extruder_id].extend(prime_tower_areas[extruder_id])
|
||||||
@ -769,6 +790,16 @@ class BuildVolume(SceneNode):
|
|||||||
prime_tower_x = prime_tower_x - machine_width / 2 #Offset by half machine_width and _depth to put the origin in the front-left.
|
prime_tower_x = prime_tower_x - machine_width / 2 #Offset by half machine_width and _depth to put the origin in the front-left.
|
||||||
prime_tower_y = prime_tower_y + machine_depth / 2
|
prime_tower_y = prime_tower_y + machine_depth / 2
|
||||||
|
|
||||||
|
if (ExtruderManager.getInstance().getResolveOrValue("prime_tower_brim_enable") and
|
||||||
|
ExtruderManager.getInstance().getResolveOrValue("adhesion_type") != "raft"):
|
||||||
|
brim_size = (
|
||||||
|
extruder.getProperty("brim_line_count", "value") *
|
||||||
|
extruder.getProperty("skirt_brim_line_width", "value") / 100.0 *
|
||||||
|
extruder.getProperty("initial_layer_line_width_factor", "value")
|
||||||
|
)
|
||||||
|
prime_tower_x -= brim_size
|
||||||
|
prime_tower_y += brim_size
|
||||||
|
|
||||||
if self._global_container_stack.getProperty("prime_tower_circular", "value"):
|
if self._global_container_stack.getProperty("prime_tower_circular", "value"):
|
||||||
radius = prime_tower_size / 2
|
radius = prime_tower_size / 2
|
||||||
prime_tower_area = Polygon.approximatedCircle(radius)
|
prime_tower_area = Polygon.approximatedCircle(radius)
|
||||||
@ -1008,7 +1039,9 @@ class BuildVolume(SceneNode):
|
|||||||
# We don't create an additional line for the extruder we're printing the skirt with.
|
# We don't create an additional line for the extruder we're printing the skirt with.
|
||||||
bed_adhesion_size -= skirt_brim_line_width * initial_layer_line_width_factor / 100.0
|
bed_adhesion_size -= skirt_brim_line_width * initial_layer_line_width_factor / 100.0
|
||||||
|
|
||||||
elif adhesion_type == "brim":
|
elif (adhesion_type == "brim" or
|
||||||
|
(self._global_container_stack.getProperty("prime_tower_brim_enable", "value") and
|
||||||
|
self._global_container_stack.getProperty("adhesion_type", "value") != "raft")):
|
||||||
brim_line_count = self._global_container_stack.getProperty("brim_line_count", "value")
|
brim_line_count = self._global_container_stack.getProperty("brim_line_count", "value")
|
||||||
bed_adhesion_size = skirt_brim_line_width * brim_line_count * initial_layer_line_width_factor / 100.0
|
bed_adhesion_size = skirt_brim_line_width * brim_line_count * initial_layer_line_width_factor / 100.0
|
||||||
|
|
||||||
@ -1027,6 +1060,12 @@ class BuildVolume(SceneNode):
|
|||||||
else:
|
else:
|
||||||
raise Exception("Unknown bed adhesion type. Did you forget to update the build volume calculations for your new bed adhesion type?")
|
raise Exception("Unknown bed adhesion type. Did you forget to update the build volume calculations for your new bed adhesion type?")
|
||||||
|
|
||||||
|
max_length_available = 0.5 * min(
|
||||||
|
self._global_container_stack.getProperty("machine_width", "value"),
|
||||||
|
self._global_container_stack.getProperty("machine_depth", "value")
|
||||||
|
)
|
||||||
|
bed_adhesion_size = min(bed_adhesion_size, max_length_available)
|
||||||
|
|
||||||
support_expansion = 0
|
support_expansion = 0
|
||||||
support_enabled = self._global_container_stack.getProperty("support_enable", "value")
|
support_enabled = self._global_container_stack.getProperty("support_enable", "value")
|
||||||
support_offset = self._global_container_stack.getProperty("support_offset", "value")
|
support_offset = self._global_container_stack.getProperty("support_offset", "value")
|
||||||
@ -1061,7 +1100,7 @@ class BuildVolume(SceneNode):
|
|||||||
_raft_settings = ["adhesion_type", "raft_base_thickness", "raft_interface_thickness", "raft_surface_layers", "raft_surface_thickness", "raft_airgap", "layer_0_z_overlap"]
|
_raft_settings = ["adhesion_type", "raft_base_thickness", "raft_interface_thickness", "raft_surface_layers", "raft_surface_thickness", "raft_airgap", "layer_0_z_overlap"]
|
||||||
_extra_z_settings = ["retraction_hop_enabled", "retraction_hop"]
|
_extra_z_settings = ["retraction_hop_enabled", "retraction_hop"]
|
||||||
_prime_settings = ["extruder_prime_pos_x", "extruder_prime_pos_y", "extruder_prime_pos_z", "prime_blob_enable"]
|
_prime_settings = ["extruder_prime_pos_x", "extruder_prime_pos_y", "extruder_prime_pos_z", "prime_blob_enable"]
|
||||||
_tower_settings = ["prime_tower_enable", "prime_tower_circular", "prime_tower_size", "prime_tower_position_x", "prime_tower_position_y"]
|
_tower_settings = ["prime_tower_enable", "prime_tower_circular", "prime_tower_size", "prime_tower_position_x", "prime_tower_position_y", "prime_tower_brim_enable"]
|
||||||
_ooze_shield_settings = ["ooze_shield_enabled", "ooze_shield_dist"]
|
_ooze_shield_settings = ["ooze_shield_enabled", "ooze_shield_dist"]
|
||||||
_distance_settings = ["infill_wipe_dist", "travel_avoid_distance", "support_offset", "support_enable", "travel_avoid_other_parts", "travel_avoid_supports"]
|
_distance_settings = ["infill_wipe_dist", "travel_avoid_distance", "support_offset", "support_enable", "travel_avoid_other_parts", "travel_avoid_supports"]
|
||||||
_extruder_settings = ["support_enable", "support_bottom_enable", "support_roof_enable", "support_infill_extruder_nr", "support_extruder_nr_layer_0", "support_bottom_extruder_nr", "support_roof_extruder_nr", "brim_line_count", "adhesion_extruder_nr", "adhesion_type"] #Settings that can affect which extruders are used.
|
_extruder_settings = ["support_enable", "support_bottom_enable", "support_roof_enable", "support_infill_extruder_nr", "support_extruder_nr_layer_0", "support_bottom_extruder_nr", "support_roof_extruder_nr", "brim_line_count", "adhesion_extruder_nr", "adhesion_type"] #Settings that can affect which extruders are used.
|
||||||
|
@ -1,40 +0,0 @@
|
|||||||
# Copyright (c) 2018 Ultimaker B.V.
|
|
||||||
# Cura is released under the terms of the LGPLv3 or higher.
|
|
||||||
|
|
||||||
from PyQt5.QtGui import QImage
|
|
||||||
from PyQt5.QtQuick import QQuickImageProvider
|
|
||||||
from PyQt5.QtCore import QSize
|
|
||||||
|
|
||||||
from UM.Application import Application
|
|
||||||
|
|
||||||
## Creates screenshots of the current scene.
|
|
||||||
class CameraImageProvider(QQuickImageProvider):
|
|
||||||
def __init__(self):
|
|
||||||
super().__init__(QQuickImageProvider.Image)
|
|
||||||
|
|
||||||
## Request a new image.
|
|
||||||
#
|
|
||||||
# The image will be taken using the current camera position.
|
|
||||||
# Only the actual objects in the scene will get rendered. Not the build
|
|
||||||
# plate and such!
|
|
||||||
# \param id The ID for the image to create. This is the requested image
|
|
||||||
# source, with the "image:" scheme and provider identifier removed. It's
|
|
||||||
# a Qt thing, they'll provide this parameter.
|
|
||||||
# \param size The dimensions of the image to scale to.
|
|
||||||
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:
|
||||||
@ -387,7 +383,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, TYPE_CHECKING, cast
|
||||||
|
|
||||||
from UM.Event import CallFunctionEvent
|
from UM.Event import CallFunctionEvent
|
||||||
from UM.FlameProfiler import pyqtSlot
|
from UM.FlameProfiler import pyqtSlot
|
||||||
@ -36,12 +36,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 +61,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 +152,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:
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
# 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 sys
|
import sys
|
||||||
import time
|
import time
|
||||||
from typing import cast, TYPE_CHECKING, Optional, Callable
|
from typing import cast, TYPE_CHECKING, Optional, Callable, List
|
||||||
|
|
||||||
import numpy
|
import numpy
|
||||||
|
|
||||||
@ -13,109 +13,118 @@ from PyQt5.QtGui import QColor, QIcon
|
|||||||
from PyQt5.QtWidgets import QMessageBox
|
from PyQt5.QtWidgets import QMessageBox
|
||||||
from PyQt5.QtQml import qmlRegisterUncreatableType, qmlRegisterSingletonType, qmlRegisterType
|
from PyQt5.QtQml import qmlRegisterUncreatableType, qmlRegisterSingletonType, qmlRegisterType
|
||||||
|
|
||||||
|
from UM.i18n import i18nCatalog
|
||||||
from UM.Application import Application
|
from UM.Application import Application
|
||||||
|
from UM.Decorators import override
|
||||||
|
from UM.FlameProfiler import pyqtSlot
|
||||||
|
from UM.Logger import Logger
|
||||||
|
from UM.Message import Message
|
||||||
|
from UM.Platform import Platform
|
||||||
from UM.PluginError import PluginNotFoundError
|
from UM.PluginError import PluginNotFoundError
|
||||||
from UM.Scene.SceneNode import SceneNode
|
from UM.Resources import Resources
|
||||||
from UM.Scene.Camera import Camera
|
from UM.Preferences import Preferences
|
||||||
from UM.Math.Vector import Vector
|
from UM.Qt.QtApplication import QtApplication # The class we're inheriting from.
|
||||||
from UM.Math.Quaternion import Quaternion
|
import UM.Util
|
||||||
|
from UM.View.SelectionPass import SelectionPass # For typing.
|
||||||
|
|
||||||
from UM.Math.AxisAlignedBox import AxisAlignedBox
|
from UM.Math.AxisAlignedBox import AxisAlignedBox
|
||||||
from UM.Math.Matrix import Matrix
|
from UM.Math.Matrix import Matrix
|
||||||
from UM.Platform import Platform
|
from UM.Math.Quaternion import Quaternion
|
||||||
from UM.Resources import Resources
|
from UM.Math.Vector import Vector
|
||||||
from UM.Scene.ToolHandle import ToolHandle
|
|
||||||
from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
|
|
||||||
from UM.Mesh.ReadMeshJob import ReadMeshJob
|
from UM.Mesh.ReadMeshJob import ReadMeshJob
|
||||||
from UM.Logger import Logger
|
|
||||||
from UM.Preferences import Preferences
|
|
||||||
from UM.Qt.QtApplication import QtApplication #The class we're inheriting from.
|
|
||||||
from UM.View.SelectionPass import SelectionPass #For typing.
|
|
||||||
from UM.Scene.Selection import Selection
|
|
||||||
from UM.Scene.GroupDecorator import GroupDecorator
|
|
||||||
from UM.Settings.ContainerStack import ContainerStack
|
|
||||||
from UM.Settings.InstanceContainer import InstanceContainer
|
|
||||||
from UM.Settings.Validator import Validator
|
|
||||||
from UM.Message import Message
|
|
||||||
from UM.i18n import i18nCatalog
|
|
||||||
from UM.Workspace.WorkspaceReader import WorkspaceReader
|
|
||||||
from UM.Decorators import deprecated
|
|
||||||
|
|
||||||
from UM.Operations.AddSceneNodeOperation import AddSceneNodeOperation
|
from UM.Operations.AddSceneNodeOperation import AddSceneNodeOperation
|
||||||
from UM.Operations.RemoveSceneNodeOperation import RemoveSceneNodeOperation
|
|
||||||
from UM.Operations.GroupedOperation import GroupedOperation
|
from UM.Operations.GroupedOperation import GroupedOperation
|
||||||
from UM.Operations.SetTransformOperation import SetTransformOperation
|
from UM.Operations.SetTransformOperation import SetTransformOperation
|
||||||
|
|
||||||
|
from UM.Scene.Camera import Camera
|
||||||
|
from UM.Scene.GroupDecorator import GroupDecorator
|
||||||
|
from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
|
||||||
|
from UM.Scene.SceneNode import SceneNode
|
||||||
|
from UM.Scene.Selection import Selection
|
||||||
|
from UM.Scene.ToolHandle import ToolHandle
|
||||||
|
|
||||||
|
from UM.Settings.ContainerRegistry import ContainerRegistry
|
||||||
|
from UM.Settings.ContainerStack import ContainerStack
|
||||||
|
from UM.Settings.InstanceContainer import InstanceContainer
|
||||||
|
from UM.Settings.SettingDefinition import SettingDefinition, DefinitionPropertyType
|
||||||
|
from UM.Settings.SettingFunction import SettingFunction
|
||||||
|
from UM.Settings.Validator import Validator
|
||||||
|
|
||||||
|
from UM.Workspace.WorkspaceReader import WorkspaceReader
|
||||||
|
|
||||||
from cura.API import CuraAPI
|
from cura.API import CuraAPI
|
||||||
|
|
||||||
from cura.Arranging.Arrange import Arrange
|
from cura.Arranging.Arrange import Arrange
|
||||||
from cura.Arranging.ArrangeObjectsJob import ArrangeObjectsJob
|
from cura.Arranging.ArrangeObjectsJob import ArrangeObjectsJob
|
||||||
from cura.Arranging.ArrangeObjectsAllBuildPlatesJob import ArrangeObjectsAllBuildPlatesJob
|
from cura.Arranging.ArrangeObjectsAllBuildPlatesJob import ArrangeObjectsAllBuildPlatesJob
|
||||||
from cura.Arranging.ShapeArray import ShapeArray
|
from cura.Arranging.ShapeArray import ShapeArray
|
||||||
from cura.MultiplyObjectsJob import MultiplyObjectsJob
|
|
||||||
from cura.Scene.ConvexHullDecorator import ConvexHullDecorator
|
|
||||||
from cura.Operations.SetParentOperation import SetParentOperation
|
from cura.Operations.SetParentOperation import SetParentOperation
|
||||||
from cura.Scene.SliceableObjectDecorator import SliceableObjectDecorator
|
|
||||||
from cura.Scene.BlockSlicingDecorator import BlockSlicingDecorator
|
from cura.Scene.BlockSlicingDecorator import BlockSlicingDecorator
|
||||||
from cura.Scene.BuildPlateDecorator import BuildPlateDecorator
|
from cura.Scene.BuildPlateDecorator import BuildPlateDecorator
|
||||||
from cura.Scene.CuraSceneNode import CuraSceneNode
|
from cura.Scene.ConvexHullDecorator import ConvexHullDecorator
|
||||||
|
|
||||||
from cura.Scene.CuraSceneController import CuraSceneController
|
from cura.Scene.CuraSceneController import CuraSceneController
|
||||||
|
from cura.Scene.CuraSceneNode import CuraSceneNode
|
||||||
from UM.Settings.SettingDefinition import SettingDefinition, DefinitionPropertyType
|
from cura.Scene.SliceableObjectDecorator import SliceableObjectDecorator
|
||||||
from UM.Settings.ContainerRegistry import ContainerRegistry
|
from cura.Scene import ZOffsetDecorator
|
||||||
from UM.Settings.SettingFunction import SettingFunction
|
|
||||||
from cura.Settings.CuraContainerRegistry import CuraContainerRegistry
|
|
||||||
from cura.Settings.MachineNameValidator import MachineNameValidator
|
|
||||||
|
|
||||||
from cura.Machines.Models.BuildPlateModel import BuildPlateModel
|
|
||||||
from cura.Machines.Models.NozzleModel import NozzleModel
|
|
||||||
from cura.Machines.Models.QualityProfilesDropDownMenuModel import QualityProfilesDropDownMenuModel
|
|
||||||
from cura.Machines.Models.CustomQualityProfilesDropDownMenuModel import CustomQualityProfilesDropDownMenuModel
|
|
||||||
from cura.Machines.Models.MultiBuildPlateModel import MultiBuildPlateModel
|
|
||||||
from cura.Machines.Models.FavoriteMaterialsModel import FavoriteMaterialsModel
|
|
||||||
from cura.Machines.Models.GenericMaterialsModel import GenericMaterialsModel
|
|
||||||
from cura.Machines.Models.MaterialBrandsModel import MaterialBrandsModel
|
|
||||||
from cura.Machines.Models.QualityManagementModel import QualityManagementModel
|
|
||||||
from cura.Machines.Models.QualitySettingsModel import QualitySettingsModel
|
|
||||||
from cura.Machines.Models.MachineManagementModel import MachineManagementModel
|
|
||||||
|
|
||||||
from cura.Machines.Models.SettingVisibilityPresetsModel import SettingVisibilityPresetsModel
|
|
||||||
|
|
||||||
from cura.Machines.MachineErrorChecker import MachineErrorChecker
|
from cura.Machines.MachineErrorChecker import MachineErrorChecker
|
||||||
|
from cura.Machines.VariantManager import VariantManager
|
||||||
|
|
||||||
|
from cura.Machines.Models.BuildPlateModel import BuildPlateModel
|
||||||
|
from cura.Machines.Models.CustomQualityProfilesDropDownMenuModel import CustomQualityProfilesDropDownMenuModel
|
||||||
|
from cura.Machines.Models.DiscoveredPrintersModel import DiscoveredPrintersModel
|
||||||
|
from cura.Machines.Models.ExtrudersModel import ExtrudersModel
|
||||||
|
from cura.Machines.Models.FavoriteMaterialsModel import FavoriteMaterialsModel
|
||||||
|
from cura.Machines.Models.FirstStartMachineActionsModel import FirstStartMachineActionsModel
|
||||||
|
from cura.Machines.Models.GenericMaterialsModel import GenericMaterialsModel
|
||||||
|
from cura.Machines.Models.GlobalStacksModel import GlobalStacksModel
|
||||||
|
from cura.Machines.Models.MaterialBrandsModel import MaterialBrandsModel
|
||||||
|
from cura.Machines.Models.MultiBuildPlateModel import MultiBuildPlateModel
|
||||||
|
from cura.Machines.Models.NozzleModel import NozzleModel
|
||||||
|
from cura.Machines.Models.QualityManagementModel import QualityManagementModel
|
||||||
|
from cura.Machines.Models.QualityProfilesDropDownMenuModel import QualityProfilesDropDownMenuModel
|
||||||
|
from cura.Machines.Models.QualitySettingsModel import QualitySettingsModel
|
||||||
|
from cura.Machines.Models.SettingVisibilityPresetsModel import SettingVisibilityPresetsModel
|
||||||
|
from cura.Machines.Models.UserChangesModel import UserChangesModel
|
||||||
|
|
||||||
|
from cura.PrinterOutput.PrinterOutputDevice import PrinterOutputDevice
|
||||||
|
from cura.PrinterOutput.NetworkMJPGImage import NetworkMJPGImage
|
||||||
|
|
||||||
|
import cura.Settings.cura_empty_instance_containers
|
||||||
|
from cura.Settings.ContainerManager import ContainerManager
|
||||||
|
from cura.Settings.CuraContainerRegistry import CuraContainerRegistry
|
||||||
|
from cura.Settings.CuraFormulaFunctions import CuraFormulaFunctions
|
||||||
|
from cura.Settings.ExtruderManager import ExtruderManager
|
||||||
|
from cura.Settings.MachineManager import MachineManager
|
||||||
|
from cura.Settings.MachineNameValidator import MachineNameValidator
|
||||||
|
from cura.Settings.MaterialSettingsVisibilityHandler import MaterialSettingsVisibilityHandler
|
||||||
from cura.Settings.SettingInheritanceManager import SettingInheritanceManager
|
from cura.Settings.SettingInheritanceManager import SettingInheritanceManager
|
||||||
|
from cura.Settings.SidebarCustomMenuItemsModel import SidebarCustomMenuItemsModel
|
||||||
from cura.Settings.SimpleModeSettingsManager import SimpleModeSettingsManager
|
from cura.Settings.SimpleModeSettingsManager import SimpleModeSettingsManager
|
||||||
|
|
||||||
from cura.Machines.VariantManager import VariantManager
|
from cura.TaskManagement.OnExitCallbackManager import OnExitCallbackManager
|
||||||
|
|
||||||
|
from cura.UI import CuraSplashScreen, MachineActionManager, PrintInformation
|
||||||
|
from cura.UI.MachineSettingsManager import MachineSettingsManager
|
||||||
|
from cura.UI.ObjectsModel import ObjectsModel
|
||||||
|
from cura.UI.TextManager import TextManager
|
||||||
|
from cura.UI.AddPrinterPagesModel import AddPrinterPagesModel
|
||||||
|
from cura.UI.WelcomePagesModel import WelcomePagesModel
|
||||||
|
from cura.UI.WhatsNewPagesModel import WhatsNewPagesModel
|
||||||
|
|
||||||
from .SingleInstance import SingleInstance
|
from .SingleInstance import SingleInstance
|
||||||
from .AutoSave import AutoSave
|
from .AutoSave import AutoSave
|
||||||
from . import PlatformPhysics
|
from . import PlatformPhysics
|
||||||
from . import BuildVolume
|
from . import BuildVolume
|
||||||
from . import CameraAnimation
|
from . import CameraAnimation
|
||||||
from . import PrintInformation
|
|
||||||
from . import CuraActions
|
from . import CuraActions
|
||||||
from cura.Scene import ZOffsetDecorator
|
|
||||||
from . import CuraSplashScreen
|
|
||||||
from . import CameraImageProvider
|
|
||||||
from . import PrintJobPreviewImageProvider
|
from . import PrintJobPreviewImageProvider
|
||||||
from . import MachineActionManager
|
|
||||||
|
|
||||||
from cura.TaskManagement.OnExitCallbackManager import OnExitCallbackManager
|
from cura import ApplicationMetadata, UltimakerCloudAuthentication
|
||||||
|
|
||||||
from cura.Settings.MachineManager import MachineManager
|
|
||||||
from cura.Settings.ExtruderManager import ExtruderManager
|
|
||||||
from cura.Settings.UserChangesModel import UserChangesModel
|
|
||||||
from cura.Settings.ExtrudersModel import ExtrudersModel
|
|
||||||
from cura.Settings.MaterialSettingsVisibilityHandler import MaterialSettingsVisibilityHandler
|
|
||||||
from cura.Settings.ContainerManager import ContainerManager
|
|
||||||
from cura.Settings.SidebarCustomMenuItemsModel import SidebarCustomMenuItemsModel
|
|
||||||
import cura.Settings.cura_empty_instance_containers
|
|
||||||
from cura.Settings.CuraFormulaFunctions import CuraFormulaFunctions
|
|
||||||
|
|
||||||
from cura.ObjectsModel import ObjectsModel
|
|
||||||
|
|
||||||
from UM.FlameProfiler import pyqtSlot
|
|
||||||
from UM.Decorators import override
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from cura.Machines.MaterialManager import MaterialManager
|
from cura.Machines.MaterialManager import MaterialManager
|
||||||
@ -126,20 +135,12 @@ if TYPE_CHECKING:
|
|||||||
|
|
||||||
numpy.seterr(all = "ignore")
|
numpy.seterr(all = "ignore")
|
||||||
|
|
||||||
try:
|
|
||||||
from cura.CuraVersion import CuraVersion, CuraBuildType, CuraDebugMode, CuraSDKVersion
|
|
||||||
except ImportError:
|
|
||||||
CuraVersion = "master" # [CodeStyle: Reflecting imported value]
|
|
||||||
CuraBuildType = ""
|
|
||||||
CuraDebugMode = False
|
|
||||||
CuraSDKVersion = ""
|
|
||||||
|
|
||||||
|
|
||||||
class CuraApplication(QtApplication):
|
class CuraApplication(QtApplication):
|
||||||
# SettingVersion represents the set of settings available in the machine/extruder definitions.
|
# SettingVersion represents the set of settings available in the machine/extruder definitions.
|
||||||
# You need to make sure that this version number needs to be increased if there is any non-backwards-compatible
|
# You need to make sure that this version number needs to be increased if there is any non-backwards-compatible
|
||||||
# changes of the settings.
|
# changes of the settings.
|
||||||
SettingVersion = 5
|
SettingVersion = 7
|
||||||
|
|
||||||
Created = False
|
Created = False
|
||||||
|
|
||||||
@ -159,10 +160,12 @@ class CuraApplication(QtApplication):
|
|||||||
Q_ENUMS(ResourceTypes)
|
Q_ENUMS(ResourceTypes)
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(name = "cura",
|
super().__init__(name = ApplicationMetadata.CuraAppName,
|
||||||
version = CuraVersion,
|
app_display_name = ApplicationMetadata.CuraAppDisplayName,
|
||||||
buildtype = CuraBuildType,
|
version = ApplicationMetadata.CuraVersion,
|
||||||
is_debug_mode = CuraDebugMode,
|
api_version = ApplicationMetadata.CuraSDKVersion,
|
||||||
|
buildtype = ApplicationMetadata.CuraBuildType,
|
||||||
|
is_debug_mode = ApplicationMetadata.CuraDebugMode,
|
||||||
tray_icon_name = "cura-icon-32.png",
|
tray_icon_name = "cura-icon-32.png",
|
||||||
**kwargs)
|
**kwargs)
|
||||||
|
|
||||||
@ -177,7 +180,6 @@ class CuraApplication(QtApplication):
|
|||||||
# Variables set from CLI
|
# Variables set from CLI
|
||||||
self._files_to_open = []
|
self._files_to_open = []
|
||||||
self._use_single_instance = False
|
self._use_single_instance = False
|
||||||
self._trigger_early_crash = False # For debug only
|
|
||||||
|
|
||||||
self._single_instance = None
|
self._single_instance = None
|
||||||
|
|
||||||
@ -202,6 +204,8 @@ class CuraApplication(QtApplication):
|
|||||||
self._container_manager = None
|
self._container_manager = None
|
||||||
|
|
||||||
self._object_manager = None
|
self._object_manager = None
|
||||||
|
self._extruders_model = None
|
||||||
|
self._extruders_model_with_optional = None
|
||||||
self._build_plate_model = None
|
self._build_plate_model = None
|
||||||
self._multi_build_plate_model = None
|
self._multi_build_plate_model = None
|
||||||
self._setting_visibility_presets_model = None
|
self._setting_visibility_presets_model = None
|
||||||
@ -210,6 +214,15 @@ class CuraApplication(QtApplication):
|
|||||||
self._cura_scene_controller = None
|
self._cura_scene_controller = None
|
||||||
self._machine_error_checker = None
|
self._machine_error_checker = None
|
||||||
|
|
||||||
|
self._machine_settings_manager = MachineSettingsManager(self, parent = self)
|
||||||
|
|
||||||
|
self._discovered_printer_model = DiscoveredPrintersModel(parent = self)
|
||||||
|
self._first_start_machine_actions_model = FirstStartMachineActionsModel(self, parent = self)
|
||||||
|
self._welcome_pages_model = WelcomePagesModel(self, parent = self)
|
||||||
|
self._add_printer_pages_model = AddPrinterPagesModel(self, parent = self)
|
||||||
|
self._whats_new_pages_model = WhatsNewPagesModel(self, parent = self)
|
||||||
|
self._text_manager = TextManager(parent = self)
|
||||||
|
|
||||||
self._quality_profile_drop_down_menu_model = None
|
self._quality_profile_drop_down_menu_model = None
|
||||||
self._custom_quality_profile_drop_down_menu_model = None
|
self._custom_quality_profile_drop_down_menu_model = None
|
||||||
self._cura_API = CuraAPI(self)
|
self._cura_API = CuraAPI(self)
|
||||||
@ -239,8 +252,6 @@ class CuraApplication(QtApplication):
|
|||||||
|
|
||||||
self._update_platform_activity_timer = None
|
self._update_platform_activity_timer = None
|
||||||
|
|
||||||
self._need_to_show_user_agreement = True
|
|
||||||
|
|
||||||
self._sidebar_custom_menu_items = [] # type: list # Keeps list of custom menu items for the side bar
|
self._sidebar_custom_menu_items = [] # type: list # Keeps list of custom menu items for the side bar
|
||||||
|
|
||||||
self._plugins_loaded = False
|
self._plugins_loaded = False
|
||||||
@ -256,6 +267,14 @@ class CuraApplication(QtApplication):
|
|||||||
from cura.CuraPackageManager import CuraPackageManager
|
from cura.CuraPackageManager import CuraPackageManager
|
||||||
self._package_manager_class = CuraPackageManager
|
self._package_manager_class = CuraPackageManager
|
||||||
|
|
||||||
|
@pyqtProperty(str, constant=True)
|
||||||
|
def ultimakerCloudApiRootUrl(self) -> str:
|
||||||
|
return UltimakerCloudAuthentication.CuraCloudAPIRoot
|
||||||
|
|
||||||
|
@pyqtProperty(str, constant = True)
|
||||||
|
def ultimakerCloudAccountRootUrl(self) -> str:
|
||||||
|
return UltimakerCloudAuthentication.CuraCloudAccountAPIRoot
|
||||||
|
|
||||||
# Adds command line options to the command line parser. This should be called after the application is created and
|
# Adds command line options to the command line parser. This should be called after the application is created and
|
||||||
# before the pre-start.
|
# before the pre-start.
|
||||||
def addCommandLineOptions(self):
|
def addCommandLineOptions(self):
|
||||||
@ -288,7 +307,10 @@ class CuraApplication(QtApplication):
|
|||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
|
|
||||||
self._use_single_instance = self._cli_args.single_instance
|
self._use_single_instance = self._cli_args.single_instance
|
||||||
self._trigger_early_crash = self._cli_args.trigger_early_crash
|
# FOR TESTING ONLY
|
||||||
|
if self._cli_args.trigger_early_crash:
|
||||||
|
assert not "This crash is triggered by the trigger_early_crash command line argument."
|
||||||
|
|
||||||
for filename in self._cli_args.file:
|
for filename in self._cli_args.file:
|
||||||
self._files_to_open.append(os.path.abspath(filename))
|
self._files_to_open.append(os.path.abspath(filename))
|
||||||
|
|
||||||
@ -298,7 +320,8 @@ class CuraApplication(QtApplication):
|
|||||||
super().initialize()
|
super().initialize()
|
||||||
|
|
||||||
self.__sendCommandToSingleInstance()
|
self.__sendCommandToSingleInstance()
|
||||||
self.__initializeSettingDefinitionsAndFunctions()
|
self._initializeSettingDefinitions()
|
||||||
|
self._initializeSettingFunctions()
|
||||||
self.__addAllResourcesAndContainerResources()
|
self.__addAllResourcesAndContainerResources()
|
||||||
self.__addAllEmptyContainers()
|
self.__addAllEmptyContainers()
|
||||||
self.__setLatestResouceVersionsForVersionUpgrade()
|
self.__setLatestResouceVersionsForVersionUpgrade()
|
||||||
@ -327,31 +350,40 @@ class CuraApplication(QtApplication):
|
|||||||
resource_path = os.path.join(os.path.abspath(os.path.dirname(__file__)), "..", "resources")
|
resource_path = os.path.join(os.path.abspath(os.path.dirname(__file__)), "..", "resources")
|
||||||
Resources.addSearchPath(resource_path)
|
Resources.addSearchPath(resource_path)
|
||||||
|
|
||||||
# Adds custom property types, settings types, and extra operators (functions) that need to be registered in
|
@classmethod
|
||||||
# SettingDefinition and SettingFunction.
|
def _initializeSettingDefinitions(cls):
|
||||||
def __initializeSettingDefinitionsAndFunctions(self):
|
|
||||||
self._cura_formula_functions = CuraFormulaFunctions(self)
|
|
||||||
|
|
||||||
# Need to do this before ContainerRegistry tries to load the machines
|
# Need to do this before ContainerRegistry tries to load the machines
|
||||||
SettingDefinition.addSupportedProperty("settable_per_mesh", DefinitionPropertyType.Any, default = True, read_only = True)
|
SettingDefinition.addSupportedProperty("settable_per_mesh", DefinitionPropertyType.Any, default=True,
|
||||||
SettingDefinition.addSupportedProperty("settable_per_extruder", DefinitionPropertyType.Any, default = True, read_only = True)
|
read_only=True)
|
||||||
|
SettingDefinition.addSupportedProperty("settable_per_extruder", DefinitionPropertyType.Any, default=True,
|
||||||
|
read_only=True)
|
||||||
# this setting can be changed for each group in one-at-a-time mode
|
# this setting can be changed for each group in one-at-a-time mode
|
||||||
SettingDefinition.addSupportedProperty("settable_per_meshgroup", DefinitionPropertyType.Any, default = True, read_only = True)
|
SettingDefinition.addSupportedProperty("settable_per_meshgroup", DefinitionPropertyType.Any, default=True,
|
||||||
SettingDefinition.addSupportedProperty("settable_globally", DefinitionPropertyType.Any, default = True, read_only = True)
|
read_only=True)
|
||||||
|
SettingDefinition.addSupportedProperty("settable_globally", DefinitionPropertyType.Any, default=True,
|
||||||
|
read_only=True)
|
||||||
|
|
||||||
# From which stack the setting would inherit if not defined per object (handled in the engine)
|
# From which stack the setting would inherit if not defined per object (handled in the engine)
|
||||||
# AND for settings which are not settable_per_mesh:
|
# AND for settings which are not settable_per_mesh:
|
||||||
# which extruder is the only extruder this setting is obtained from
|
# which extruder is the only extruder this setting is obtained from
|
||||||
SettingDefinition.addSupportedProperty("limit_to_extruder", DefinitionPropertyType.Function, default = "-1", depends_on = "value")
|
SettingDefinition.addSupportedProperty("limit_to_extruder", DefinitionPropertyType.Function, default="-1",
|
||||||
|
depends_on="value")
|
||||||
|
|
||||||
# For settings which are not settable_per_mesh and not settable_per_extruder:
|
# For settings which are not settable_per_mesh and not settable_per_extruder:
|
||||||
# A function which determines the glabel/meshgroup value by looking at the values of the setting in all (used) extruders
|
# A function which determines the glabel/meshgroup value by looking at the values of the setting in all (used) extruders
|
||||||
SettingDefinition.addSupportedProperty("resolve", DefinitionPropertyType.Function, default = None, depends_on = "value")
|
SettingDefinition.addSupportedProperty("resolve", DefinitionPropertyType.Function, default=None,
|
||||||
|
depends_on="value")
|
||||||
|
|
||||||
SettingDefinition.addSettingType("extruder", None, str, Validator)
|
SettingDefinition.addSettingType("extruder", None, str, Validator)
|
||||||
SettingDefinition.addSettingType("optional_extruder", None, str, None)
|
SettingDefinition.addSettingType("optional_extruder", None, str, None)
|
||||||
SettingDefinition.addSettingType("[int]", None, str, None)
|
SettingDefinition.addSettingType("[int]", None, str, None)
|
||||||
|
|
||||||
|
|
||||||
|
# Adds custom property types, settings types, and extra operators (functions) that need to be registered in
|
||||||
|
# SettingDefinition and SettingFunction.
|
||||||
|
def _initializeSettingFunctions(self):
|
||||||
|
self._cura_formula_functions = CuraFormulaFunctions(self)
|
||||||
|
|
||||||
SettingFunction.registerOperator("extruderValue", self._cura_formula_functions.getValueInExtruder)
|
SettingFunction.registerOperator("extruderValue", self._cura_formula_functions.getValueInExtruder)
|
||||||
SettingFunction.registerOperator("extruderValues", self._cura_formula_functions.getValuesInAllExtruders)
|
SettingFunction.registerOperator("extruderValues", self._cura_formula_functions.getValuesInAllExtruders)
|
||||||
SettingFunction.registerOperator("resolveOrValue", self._cura_formula_functions.getResolveOrValue)
|
SettingFunction.registerOperator("resolveOrValue", self._cura_formula_functions.getResolveOrValue)
|
||||||
@ -424,38 +456,35 @@ class CuraApplication(QtApplication):
|
|||||||
def startSplashWindowPhase(self) -> None:
|
def startSplashWindowPhase(self) -> None:
|
||||||
super().startSplashWindowPhase()
|
super().startSplashWindowPhase()
|
||||||
|
|
||||||
self.setWindowIcon(QIcon(Resources.getPath(Resources.Images, "cura-icon.png")))
|
if not self.getIsHeadLess():
|
||||||
|
self.setWindowIcon(QIcon(Resources.getPath(Resources.Images, "cura-icon.png")))
|
||||||
|
|
||||||
self.setRequiredPlugins([
|
self.setRequiredPlugins([
|
||||||
# Misc.:
|
# Misc.:
|
||||||
"ConsoleLogger",
|
"ConsoleLogger", #You want to be able to read the log if something goes wrong.
|
||||||
"CuraEngineBackend",
|
"CuraEngineBackend", #Cura is useless without this one since you can't slice.
|
||||||
"UserAgreement",
|
"FileLogger", #You want to be able to read the log if something goes wrong.
|
||||||
"FileLogger",
|
"XmlMaterialProfile", #Cura crashes without this one.
|
||||||
"XmlMaterialProfile",
|
"Toolbox", #This contains the interface to enable/disable plug-ins, so if you disable it you can't enable it back.
|
||||||
"Toolbox",
|
"PrepareStage", #Cura is useless without this one since you can't load models.
|
||||||
"PrepareStage",
|
"PreviewStage", #This shows the list of the plugin views that are installed in Cura.
|
||||||
"MonitorStage",
|
"MonitorStage", #Major part of Cura's functionality.
|
||||||
"LocalFileOutputDevice",
|
"LocalFileOutputDevice", #Major part of Cura's functionality.
|
||||||
"LocalContainerProvider",
|
"LocalContainerProvider", #Cura is useless without any profiles or setting definitions.
|
||||||
|
|
||||||
# Views:
|
# Views:
|
||||||
"SimpleView",
|
"SimpleView", #Dependency of SolidView.
|
||||||
"SimulationView",
|
"SolidView", #Displays models. Cura is useless without it.
|
||||||
"SolidView",
|
|
||||||
|
|
||||||
# Readers & Writers:
|
# Readers & Writers:
|
||||||
"GCodeWriter",
|
"GCodeWriter", #Cura is useless if it can't write its output.
|
||||||
"STLReader",
|
"STLReader", #Most common model format, so disabling this makes Cura 90% useless.
|
||||||
"3MFWriter",
|
"3MFWriter", #Required for writing project files.
|
||||||
|
|
||||||
# Tools:
|
# Tools:
|
||||||
"CameraTool",
|
"CameraTool", #Needed to see the scene. Cura is useless without it.
|
||||||
"MirrorTool",
|
"SelectionTool", #Dependency of the rest of the tools.
|
||||||
"RotateTool",
|
"TranslateTool", #You'll need this for almost every print.
|
||||||
"ScaleTool",
|
|
||||||
"SelectionTool",
|
|
||||||
"TranslateTool",
|
|
||||||
])
|
])
|
||||||
self._i18n_catalog = i18nCatalog("cura")
|
self._i18n_catalog = i18nCatalog("cura")
|
||||||
|
|
||||||
@ -492,7 +521,8 @@ class CuraApplication(QtApplication):
|
|||||||
preferences.addPreference("cura/choice_on_profile_override", "always_ask")
|
preferences.addPreference("cura/choice_on_profile_override", "always_ask")
|
||||||
preferences.addPreference("cura/choice_on_open_project", "always_ask")
|
preferences.addPreference("cura/choice_on_open_project", "always_ask")
|
||||||
preferences.addPreference("cura/use_multi_build_plate", False)
|
preferences.addPreference("cura/use_multi_build_plate", False)
|
||||||
|
preferences.addPreference("view/settings_list_height", 400)
|
||||||
|
preferences.addPreference("view/settings_visible", False)
|
||||||
preferences.addPreference("cura/currency", "€")
|
preferences.addPreference("cura/currency", "€")
|
||||||
preferences.addPreference("cura/material_settings", "{}")
|
preferences.addPreference("cura/material_settings", "{}")
|
||||||
|
|
||||||
@ -504,7 +534,7 @@ class CuraApplication(QtApplication):
|
|||||||
preferences.addPreference("cura/expanded_brands", "")
|
preferences.addPreference("cura/expanded_brands", "")
|
||||||
preferences.addPreference("cura/expanded_types", "")
|
preferences.addPreference("cura/expanded_types", "")
|
||||||
|
|
||||||
self._need_to_show_user_agreement = not preferences.getValue("general/accepted_user_agreement")
|
preferences.addPreference("general/accepted_user_agreement", False)
|
||||||
|
|
||||||
for key in [
|
for key in [
|
||||||
"dialog_load_path", # dialog_save_path is in LocalFileOutputDevicePlugin
|
"dialog_load_path", # dialog_save_path is in LocalFileOutputDevicePlugin
|
||||||
@ -523,18 +553,24 @@ class CuraApplication(QtApplication):
|
|||||||
CuraApplication.Created = True
|
CuraApplication.Created = True
|
||||||
|
|
||||||
def _onEngineCreated(self):
|
def _onEngineCreated(self):
|
||||||
self._qml_engine.addImageProvider("camera", CameraImageProvider.CameraImageProvider())
|
|
||||||
self._qml_engine.addImageProvider("print_job_preview", PrintJobPreviewImageProvider.PrintJobPreviewImageProvider())
|
self._qml_engine.addImageProvider("print_job_preview", PrintJobPreviewImageProvider.PrintJobPreviewImageProvider())
|
||||||
|
|
||||||
@pyqtProperty(bool)
|
@pyqtProperty(bool)
|
||||||
def needToShowUserAgreement(self) -> bool:
|
def needToShowUserAgreement(self) -> bool:
|
||||||
return self._need_to_show_user_agreement
|
return not UM.Util.parseBool(self.getPreferences().getValue("general/accepted_user_agreement"))
|
||||||
|
|
||||||
def setNeedToShowUserAgreement(self, set_value = True) -> None:
|
@pyqtSlot(bool)
|
||||||
self._need_to_show_user_agreement = set_value
|
def setNeedToShowUserAgreement(self, set_value: bool = True) -> None:
|
||||||
|
self.getPreferences().setValue("general/accepted_user_agreement", str(not set_value))
|
||||||
|
|
||||||
|
@pyqtSlot(str, str)
|
||||||
|
def writeToLog(self, severity: str, message: str) -> None:
|
||||||
|
Logger.log(severity, message)
|
||||||
|
|
||||||
# DO NOT call this function to close the application, use checkAndExitApplication() instead which will perform
|
# DO NOT call this function to close the application, use checkAndExitApplication() instead which will perform
|
||||||
# pre-exit checks such as checking for in-progress USB printing, etc.
|
# pre-exit checks such as checking for in-progress USB printing, etc.
|
||||||
|
# Except for the 'Decline and close' in the 'User Agreement'-step in the Welcome-pages, that should be a hard exit.
|
||||||
|
@pyqtSlot()
|
||||||
def closeApplication(self) -> None:
|
def closeApplication(self) -> None:
|
||||||
Logger.log("i", "Close application")
|
Logger.log("i", "Close application")
|
||||||
main_window = self.getMainWindow()
|
main_window = self.getMainWindow()
|
||||||
@ -633,8 +669,6 @@ class CuraApplication(QtApplication):
|
|||||||
self._message_box_callback = None
|
self._message_box_callback = None
|
||||||
self._message_box_callback_arguments = []
|
self._message_box_callback_arguments = []
|
||||||
|
|
||||||
showPrintMonitor = pyqtSignal(bool, arguments = ["show"])
|
|
||||||
|
|
||||||
def setSaveDataEnabled(self, enabled: bool) -> None:
|
def setSaveDataEnabled(self, enabled: bool) -> None:
|
||||||
self._save_data_enabled = enabled
|
self._save_data_enabled = enabled
|
||||||
|
|
||||||
@ -660,12 +694,12 @@ class CuraApplication(QtApplication):
|
|||||||
|
|
||||||
## Handle loading of all plugin types (and the backend explicitly)
|
## Handle loading of all plugin types (and the backend explicitly)
|
||||||
# \sa PluginRegistry
|
# \sa PluginRegistry
|
||||||
def _loadPlugins(self):
|
def _loadPlugins(self) -> None:
|
||||||
self._plugin_registry.addType("profile_reader", self._addProfileReader)
|
self._plugin_registry.addType("profile_reader", self._addProfileReader)
|
||||||
self._plugin_registry.addType("profile_writer", self._addProfileWriter)
|
self._plugin_registry.addType("profile_writer", self._addProfileWriter)
|
||||||
|
|
||||||
if Platform.isLinux():
|
if Platform.isLinux():
|
||||||
lib_suffixes = {"", "64", "32", "x32"} #A few common ones on different distributions.
|
lib_suffixes = {"", "64", "32", "x32"} # A few common ones on different distributions.
|
||||||
else:
|
else:
|
||||||
lib_suffixes = {""}
|
lib_suffixes = {""}
|
||||||
for suffix in lib_suffixes:
|
for suffix in lib_suffixes:
|
||||||
@ -730,6 +764,11 @@ class CuraApplication(QtApplication):
|
|||||||
# Initialize Cura API
|
# Initialize Cura API
|
||||||
self._cura_API.initialize()
|
self._cura_API.initialize()
|
||||||
|
|
||||||
|
self._output_device_manager.start()
|
||||||
|
self._welcome_pages_model.initialize()
|
||||||
|
self._add_printer_pages_model.initialize()
|
||||||
|
self._whats_new_pages_model.initialize()
|
||||||
|
|
||||||
# Detect in which mode to run and execute that mode
|
# Detect in which mode to run and execute that mode
|
||||||
if self._is_headless:
|
if self._is_headless:
|
||||||
self.runWithoutGUI()
|
self.runWithoutGUI()
|
||||||
@ -824,10 +863,38 @@ class CuraApplication(QtApplication):
|
|||||||
# Hide the splash screen
|
# Hide the splash screen
|
||||||
self.closeSplash()
|
self.closeSplash()
|
||||||
|
|
||||||
|
@pyqtSlot(result = QObject)
|
||||||
|
def getDiscoveredPrintersModel(self, *args) -> "DiscoveredPrintersModel":
|
||||||
|
return self._discovered_printer_model
|
||||||
|
|
||||||
|
@pyqtSlot(result = QObject)
|
||||||
|
def getFirstStartMachineActionsModel(self, *args) -> "FirstStartMachineActionsModel":
|
||||||
|
return self._first_start_machine_actions_model
|
||||||
|
|
||||||
@pyqtSlot(result = QObject)
|
@pyqtSlot(result = QObject)
|
||||||
def getSettingVisibilityPresetsModel(self, *args) -> SettingVisibilityPresetsModel:
|
def getSettingVisibilityPresetsModel(self, *args) -> SettingVisibilityPresetsModel:
|
||||||
return self._setting_visibility_presets_model
|
return self._setting_visibility_presets_model
|
||||||
|
|
||||||
|
@pyqtSlot(result = QObject)
|
||||||
|
def getWelcomePagesModel(self, *args) -> "WelcomePagesModel":
|
||||||
|
return self._welcome_pages_model
|
||||||
|
|
||||||
|
@pyqtSlot(result = QObject)
|
||||||
|
def getAddPrinterPagesModel(self, *args) -> "AddPrinterPagesModel":
|
||||||
|
return self._add_printer_pages_model
|
||||||
|
|
||||||
|
@pyqtSlot(result = QObject)
|
||||||
|
def getWhatsNewPagesModel(self, *args) -> "WhatsNewPagesModel":
|
||||||
|
return self._whats_new_pages_model
|
||||||
|
|
||||||
|
@pyqtSlot(result = QObject)
|
||||||
|
def getMachineSettingsManager(self, *args) -> "MachineSettingsManager":
|
||||||
|
return self._machine_settings_manager
|
||||||
|
|
||||||
|
@pyqtSlot(result = QObject)
|
||||||
|
def getTextManager(self, *args) -> "TextManager":
|
||||||
|
return self._text_manager
|
||||||
|
|
||||||
def getCuraFormulaFunctions(self, *args) -> "CuraFormulaFunctions":
|
def getCuraFormulaFunctions(self, *args) -> "CuraFormulaFunctions":
|
||||||
if self._cura_formula_functions is None:
|
if self._cura_formula_functions is None:
|
||||||
self._cura_formula_functions = CuraFormulaFunctions(self)
|
self._cura_formula_functions = CuraFormulaFunctions(self)
|
||||||
@ -862,6 +929,19 @@ class CuraApplication(QtApplication):
|
|||||||
self._object_manager = ObjectsModel.createObjectsModel()
|
self._object_manager = ObjectsModel.createObjectsModel()
|
||||||
return self._object_manager
|
return self._object_manager
|
||||||
|
|
||||||
|
@pyqtSlot(result = QObject)
|
||||||
|
def getExtrudersModel(self, *args) -> "ExtrudersModel":
|
||||||
|
if self._extruders_model is None:
|
||||||
|
self._extruders_model = ExtrudersModel(self)
|
||||||
|
return self._extruders_model
|
||||||
|
|
||||||
|
@pyqtSlot(result = QObject)
|
||||||
|
def getExtrudersModelWithOptional(self, *args) -> "ExtrudersModel":
|
||||||
|
if self._extruders_model_with_optional is None:
|
||||||
|
self._extruders_model_with_optional = ExtrudersModel(self)
|
||||||
|
self._extruders_model_with_optional.setAddOptionalExtruder(True)
|
||||||
|
return self._extruders_model_with_optional
|
||||||
|
|
||||||
@pyqtSlot(result = QObject)
|
@pyqtSlot(result = QObject)
|
||||||
def getMultiBuildPlateModel(self, *args) -> MultiBuildPlateModel:
|
def getMultiBuildPlateModel(self, *args) -> MultiBuildPlateModel:
|
||||||
if self._multi_build_plate_model is None:
|
if self._multi_build_plate_model is None:
|
||||||
@ -936,7 +1016,7 @@ class CuraApplication(QtApplication):
|
|||||||
engine.rootContext().setContextProperty("CuraApplication", self)
|
engine.rootContext().setContextProperty("CuraApplication", self)
|
||||||
engine.rootContext().setContextProperty("PrintInformation", self._print_information)
|
engine.rootContext().setContextProperty("PrintInformation", self._print_information)
|
||||||
engine.rootContext().setContextProperty("CuraActions", self._cura_actions)
|
engine.rootContext().setContextProperty("CuraActions", self._cura_actions)
|
||||||
engine.rootContext().setContextProperty("CuraSDKVersion", CuraSDKVersion)
|
engine.rootContext().setContextProperty("CuraSDKVersion", ApplicationMetadata.CuraSDKVersion)
|
||||||
|
|
||||||
qmlRegisterUncreatableType(CuraApplication, "Cura", 1, 0, "ResourceTypes", "Just an Enum type")
|
qmlRegisterUncreatableType(CuraApplication, "Cura", 1, 0, "ResourceTypes", "Just an Enum type")
|
||||||
|
|
||||||
@ -947,17 +1027,26 @@ class CuraApplication(QtApplication):
|
|||||||
qmlRegisterSingletonType(SimpleModeSettingsManager, "Cura", 1, 0, "SimpleModeSettingsManager", self.getSimpleModeSettingsManager)
|
qmlRegisterSingletonType(SimpleModeSettingsManager, "Cura", 1, 0, "SimpleModeSettingsManager", self.getSimpleModeSettingsManager)
|
||||||
qmlRegisterSingletonType(MachineActionManager.MachineActionManager, "Cura", 1, 0, "MachineActionManager", self.getMachineActionManager)
|
qmlRegisterSingletonType(MachineActionManager.MachineActionManager, "Cura", 1, 0, "MachineActionManager", self.getMachineActionManager)
|
||||||
|
|
||||||
|
qmlRegisterType(WelcomePagesModel, "Cura", 1, 0, "WelcomePagesModel")
|
||||||
|
qmlRegisterType(WhatsNewPagesModel, "Cura", 1, 0, "WhatsNewPagesModel")
|
||||||
|
qmlRegisterType(AddPrinterPagesModel, "Cura", 1, 0, "AddPrinterPagesModel")
|
||||||
|
qmlRegisterType(TextManager, "Cura", 1, 0, "TextManager")
|
||||||
|
|
||||||
|
qmlRegisterType(NetworkMJPGImage, "Cura", 1, 0, "NetworkMJPGImage")
|
||||||
|
|
||||||
qmlRegisterSingletonType(ObjectsModel, "Cura", 1, 0, "ObjectsModel", self.getObjectsModel)
|
qmlRegisterSingletonType(ObjectsModel, "Cura", 1, 0, "ObjectsModel", self.getObjectsModel)
|
||||||
qmlRegisterType(BuildPlateModel, "Cura", 1, 0, "BuildPlateModel")
|
qmlRegisterType(BuildPlateModel, "Cura", 1, 0, "BuildPlateModel")
|
||||||
qmlRegisterType(MultiBuildPlateModel, "Cura", 1, 0, "MultiBuildPlateModel")
|
qmlRegisterType(MultiBuildPlateModel, "Cura", 1, 0, "MultiBuildPlateModel")
|
||||||
qmlRegisterType(InstanceContainer, "Cura", 1, 0, "InstanceContainer")
|
qmlRegisterType(InstanceContainer, "Cura", 1, 0, "InstanceContainer")
|
||||||
qmlRegisterType(ExtrudersModel, "Cura", 1, 0, "ExtrudersModel")
|
qmlRegisterType(ExtrudersModel, "Cura", 1, 0, "ExtrudersModel")
|
||||||
|
qmlRegisterType(GlobalStacksModel, "Cura", 1, 0, "GlobalStacksModel")
|
||||||
|
|
||||||
qmlRegisterType(FavoriteMaterialsModel, "Cura", 1, 0, "FavoriteMaterialsModel")
|
qmlRegisterType(FavoriteMaterialsModel, "Cura", 1, 0, "FavoriteMaterialsModel")
|
||||||
qmlRegisterType(GenericMaterialsModel, "Cura", 1, 0, "GenericMaterialsModel")
|
qmlRegisterType(GenericMaterialsModel, "Cura", 1, 0, "GenericMaterialsModel")
|
||||||
qmlRegisterType(MaterialBrandsModel, "Cura", 1, 0, "MaterialBrandsModel")
|
qmlRegisterType(MaterialBrandsModel, "Cura", 1, 0, "MaterialBrandsModel")
|
||||||
qmlRegisterType(QualityManagementModel, "Cura", 1, 0, "QualityManagementModel")
|
qmlRegisterType(QualityManagementModel, "Cura", 1, 0, "QualityManagementModel")
|
||||||
qmlRegisterType(MachineManagementModel, "Cura", 1, 0, "MachineManagementModel")
|
|
||||||
|
qmlRegisterType(DiscoveredPrintersModel, "Cura", 1, 0, "DiscoveredPrintersModel")
|
||||||
|
|
||||||
qmlRegisterSingletonType(QualityProfilesDropDownMenuModel, "Cura", 1, 0,
|
qmlRegisterSingletonType(QualityProfilesDropDownMenuModel, "Cura", 1, 0,
|
||||||
"QualityProfilesDropDownMenuModel", self.getQualityProfilesDropDownMenuModel)
|
"QualityProfilesDropDownMenuModel", self.getQualityProfilesDropDownMenuModel)
|
||||||
@ -968,11 +1057,14 @@ class CuraApplication(QtApplication):
|
|||||||
qmlRegisterType(MaterialSettingsVisibilityHandler, "Cura", 1, 0, "MaterialSettingsVisibilityHandler")
|
qmlRegisterType(MaterialSettingsVisibilityHandler, "Cura", 1, 0, "MaterialSettingsVisibilityHandler")
|
||||||
qmlRegisterType(SettingVisibilityPresetsModel, "Cura", 1, 0, "SettingVisibilityPresetsModel")
|
qmlRegisterType(SettingVisibilityPresetsModel, "Cura", 1, 0, "SettingVisibilityPresetsModel")
|
||||||
qmlRegisterType(QualitySettingsModel, "Cura", 1, 0, "QualitySettingsModel")
|
qmlRegisterType(QualitySettingsModel, "Cura", 1, 0, "QualitySettingsModel")
|
||||||
|
qmlRegisterType(FirstStartMachineActionsModel, "Cura", 1, 0, "FirstStartMachineActionsModel")
|
||||||
qmlRegisterType(MachineNameValidator, "Cura", 1, 0, "MachineNameValidator")
|
qmlRegisterType(MachineNameValidator, "Cura", 1, 0, "MachineNameValidator")
|
||||||
qmlRegisterType(UserChangesModel, "Cura", 1, 0, "UserChangesModel")
|
qmlRegisterType(UserChangesModel, "Cura", 1, 0, "UserChangesModel")
|
||||||
qmlRegisterSingletonType(ContainerManager, "Cura", 1, 0, "ContainerManager", ContainerManager.getInstance)
|
qmlRegisterSingletonType(ContainerManager, "Cura", 1, 0, "ContainerManager", ContainerManager.getInstance)
|
||||||
qmlRegisterType(SidebarCustomMenuItemsModel, "Cura", 1, 0, "SidebarCustomMenuItemsModel")
|
qmlRegisterType(SidebarCustomMenuItemsModel, "Cura", 1, 0, "SidebarCustomMenuItemsModel")
|
||||||
|
|
||||||
|
qmlRegisterType(PrinterOutputDevice, "Cura", 1, 0, "PrinterOutputDevice")
|
||||||
|
|
||||||
from cura.API import CuraAPI
|
from cura.API import CuraAPI
|
||||||
qmlRegisterSingletonType(CuraAPI, "Cura", 1, 1, "API", self.getCuraAPI)
|
qmlRegisterSingletonType(CuraAPI, "Cura", 1, 1, "API", self.getCuraAPI)
|
||||||
|
|
||||||
@ -1022,7 +1114,6 @@ class CuraApplication(QtApplication):
|
|||||||
self._camera_animation.setTarget(Selection.getSelectedObject(0).getWorldPosition())
|
self._camera_animation.setTarget(Selection.getSelectedObject(0).getWorldPosition())
|
||||||
self._camera_animation.start()
|
self._camera_animation.start()
|
||||||
|
|
||||||
requestAddPrinter = pyqtSignal()
|
|
||||||
activityChanged = pyqtSignal()
|
activityChanged = pyqtSignal()
|
||||||
sceneBoundingBoxChanged = pyqtSignal()
|
sceneBoundingBoxChanged = pyqtSignal()
|
||||||
|
|
||||||
@ -1083,88 +1174,6 @@ class CuraApplication(QtApplication):
|
|||||||
self._platform_activity = True if count > 0 else False
|
self._platform_activity = True if count > 0 else False
|
||||||
self.activityChanged.emit()
|
self.activityChanged.emit()
|
||||||
|
|
||||||
# Remove all selected objects from the scene.
|
|
||||||
@pyqtSlot()
|
|
||||||
@deprecated("Moved to CuraActions", "2.6")
|
|
||||||
def deleteSelection(self):
|
|
||||||
if not self.getController().getToolsEnabled():
|
|
||||||
return
|
|
||||||
removed_group_nodes = []
|
|
||||||
op = GroupedOperation()
|
|
||||||
nodes = Selection.getAllSelectedObjects()
|
|
||||||
for node in nodes:
|
|
||||||
op.addOperation(RemoveSceneNodeOperation(node))
|
|
||||||
group_node = node.getParent()
|
|
||||||
if group_node and group_node.callDecoration("isGroup") and group_node not in removed_group_nodes:
|
|
||||||
remaining_nodes_in_group = list(set(group_node.getChildren()) - set(nodes))
|
|
||||||
if len(remaining_nodes_in_group) == 1:
|
|
||||||
removed_group_nodes.append(group_node)
|
|
||||||
op.addOperation(SetParentOperation(remaining_nodes_in_group[0], group_node.getParent()))
|
|
||||||
op.addOperation(RemoveSceneNodeOperation(group_node))
|
|
||||||
op.push()
|
|
||||||
|
|
||||||
## Remove an object from the scene.
|
|
||||||
# Note that this only removes an object if it is selected.
|
|
||||||
@pyqtSlot("quint64")
|
|
||||||
@deprecated("Use deleteSelection instead", "2.6")
|
|
||||||
def deleteObject(self, object_id):
|
|
||||||
if not self.getController().getToolsEnabled():
|
|
||||||
return
|
|
||||||
|
|
||||||
node = self.getController().getScene().findObject(object_id)
|
|
||||||
|
|
||||||
if not node and object_id != 0: # Workaround for tool handles overlapping the selected object
|
|
||||||
node = Selection.getSelectedObject(0)
|
|
||||||
|
|
||||||
if node:
|
|
||||||
op = GroupedOperation()
|
|
||||||
op.addOperation(RemoveSceneNodeOperation(node))
|
|
||||||
|
|
||||||
group_node = node.getParent()
|
|
||||||
if group_node:
|
|
||||||
# Note that at this point the node has not yet been deleted
|
|
||||||
if len(group_node.getChildren()) <= 2 and group_node.callDecoration("isGroup"):
|
|
||||||
op.addOperation(SetParentOperation(group_node.getChildren()[0], group_node.getParent()))
|
|
||||||
op.addOperation(RemoveSceneNodeOperation(group_node))
|
|
||||||
|
|
||||||
op.push()
|
|
||||||
|
|
||||||
## Create a number of copies of existing object.
|
|
||||||
# \param object_id
|
|
||||||
# \param count number of copies
|
|
||||||
# \param min_offset minimum offset to other objects.
|
|
||||||
@pyqtSlot("quint64", int)
|
|
||||||
@deprecated("Use CuraActions::multiplySelection", "2.6")
|
|
||||||
def multiplyObject(self, object_id, count, min_offset = 8):
|
|
||||||
node = self.getController().getScene().findObject(object_id)
|
|
||||||
if not node:
|
|
||||||
node = Selection.getSelectedObject(0)
|
|
||||||
|
|
||||||
while node.getParent() and node.getParent().callDecoration("isGroup"):
|
|
||||||
node = node.getParent()
|
|
||||||
|
|
||||||
job = MultiplyObjectsJob([node], count, min_offset)
|
|
||||||
job.start()
|
|
||||||
return
|
|
||||||
|
|
||||||
## Center object on platform.
|
|
||||||
@pyqtSlot("quint64")
|
|
||||||
@deprecated("Use CuraActions::centerSelection", "2.6")
|
|
||||||
def centerObject(self, object_id):
|
|
||||||
node = self.getController().getScene().findObject(object_id)
|
|
||||||
if not node and object_id != 0: # Workaround for tool handles overlapping the selected object
|
|
||||||
node = Selection.getSelectedObject(0)
|
|
||||||
|
|
||||||
if not node:
|
|
||||||
return
|
|
||||||
|
|
||||||
if node.getParent() and node.getParent().callDecoration("isGroup"):
|
|
||||||
node = node.getParent()
|
|
||||||
|
|
||||||
if node:
|
|
||||||
op = SetTransformOperation(node, Vector())
|
|
||||||
op.push()
|
|
||||||
|
|
||||||
## Select all nodes containing mesh data in the scene.
|
## Select all nodes containing mesh data in the scene.
|
||||||
@pyqtSlot()
|
@pyqtSlot()
|
||||||
def selectAll(self):
|
def selectAll(self):
|
||||||
@ -1244,62 +1253,75 @@ class CuraApplication(QtApplication):
|
|||||||
|
|
||||||
## Arrange all objects.
|
## Arrange all objects.
|
||||||
@pyqtSlot()
|
@pyqtSlot()
|
||||||
def arrangeObjectsToAllBuildPlates(self):
|
def arrangeObjectsToAllBuildPlates(self) -> None:
|
||||||
nodes = []
|
nodes_to_arrange = []
|
||||||
for node in DepthFirstIterator(self.getController().getScene().getRoot()):
|
for node in DepthFirstIterator(self.getController().getScene().getRoot()): # type: ignore
|
||||||
if not isinstance(node, SceneNode):
|
if not isinstance(node, SceneNode):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if not node.getMeshData() and not node.callDecoration("isGroup"):
|
if not node.getMeshData() and not node.callDecoration("isGroup"):
|
||||||
continue # Node that doesnt have a mesh and is not a group.
|
continue # Node that doesnt have a mesh and is not a group.
|
||||||
if node.getParent() and node.getParent().callDecoration("isGroup"):
|
|
||||||
continue # Grouped nodes don't need resetting as their parent (the group) is resetted)
|
parent_node = node.getParent()
|
||||||
|
if parent_node and parent_node.callDecoration("isGroup"):
|
||||||
|
continue # Grouped nodes don't need resetting as their parent (the group) is reset)
|
||||||
|
|
||||||
if not node.callDecoration("isSliceable") and not node.callDecoration("isGroup"):
|
if not node.callDecoration("isSliceable") and not node.callDecoration("isGroup"):
|
||||||
continue # i.e. node with layer data
|
continue # i.e. node with layer data
|
||||||
|
|
||||||
|
bounding_box = node.getBoundingBox()
|
||||||
# Skip nodes that are too big
|
# Skip nodes that are too big
|
||||||
if node.getBoundingBox().width < self._volume.getBoundingBox().width or node.getBoundingBox().depth < self._volume.getBoundingBox().depth:
|
if bounding_box is None or bounding_box.width < self._volume.getBoundingBox().width or bounding_box.depth < self._volume.getBoundingBox().depth:
|
||||||
nodes.append(node)
|
nodes_to_arrange.append(node)
|
||||||
job = ArrangeObjectsAllBuildPlatesJob(nodes)
|
job = ArrangeObjectsAllBuildPlatesJob(nodes_to_arrange)
|
||||||
job.start()
|
job.start()
|
||||||
self.getCuraSceneController().setActiveBuildPlate(0) # Select first build plate
|
self.getCuraSceneController().setActiveBuildPlate(0) # Select first build plate
|
||||||
|
|
||||||
# Single build plate
|
# Single build plate
|
||||||
@pyqtSlot()
|
@pyqtSlot()
|
||||||
def arrangeAll(self):
|
def arrangeAll(self) -> None:
|
||||||
nodes = []
|
nodes_to_arrange = []
|
||||||
active_build_plate = self.getMultiBuildPlateModel().activeBuildPlate
|
active_build_plate = self.getMultiBuildPlateModel().activeBuildPlate
|
||||||
for node in DepthFirstIterator(self.getController().getScene().getRoot()):
|
for node in DepthFirstIterator(self.getController().getScene().getRoot()): # type: ignore
|
||||||
if not isinstance(node, SceneNode):
|
if not isinstance(node, SceneNode):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if not node.getMeshData() and not node.callDecoration("isGroup"):
|
if not node.getMeshData() and not node.callDecoration("isGroup"):
|
||||||
continue # Node that doesnt have a mesh and is not a group.
|
continue # Node that doesnt have a mesh and is not a group.
|
||||||
if node.getParent() and node.getParent().callDecoration("isGroup"):
|
|
||||||
|
parent_node = node.getParent()
|
||||||
|
if parent_node and parent_node.callDecoration("isGroup"):
|
||||||
continue # Grouped nodes don't need resetting as their parent (the group) is resetted)
|
continue # Grouped nodes don't need resetting as their parent (the group) is resetted)
|
||||||
|
|
||||||
if not node.isSelectable():
|
if not node.isSelectable():
|
||||||
continue # i.e. node with layer data
|
continue # i.e. node with layer data
|
||||||
|
|
||||||
if not node.callDecoration("isSliceable") and not node.callDecoration("isGroup"):
|
if not node.callDecoration("isSliceable") and not node.callDecoration("isGroup"):
|
||||||
continue # i.e. node with layer data
|
continue # i.e. node with layer data
|
||||||
|
|
||||||
if node.callDecoration("getBuildPlateNumber") == active_build_plate:
|
if node.callDecoration("getBuildPlateNumber") == active_build_plate:
|
||||||
# Skip nodes that are too big
|
# Skip nodes that are too big
|
||||||
if node.getBoundingBox().width < self._volume.getBoundingBox().width or node.getBoundingBox().depth < self._volume.getBoundingBox().depth:
|
bounding_box = node.getBoundingBox()
|
||||||
nodes.append(node)
|
if bounding_box is None or bounding_box.width < self._volume.getBoundingBox().width or bounding_box.depth < self._volume.getBoundingBox().depth:
|
||||||
self.arrange(nodes, fixed_nodes = [])
|
nodes_to_arrange.append(node)
|
||||||
|
self.arrange(nodes_to_arrange, fixed_nodes = [])
|
||||||
|
|
||||||
## Arrange a set of nodes given a set of fixed nodes
|
## Arrange a set of nodes given a set of fixed nodes
|
||||||
# \param nodes nodes that we have to place
|
# \param nodes nodes that we have to place
|
||||||
# \param fixed_nodes nodes that are placed in the arranger before finding spots for nodes
|
# \param fixed_nodes nodes that are placed in the arranger before finding spots for nodes
|
||||||
def arrange(self, nodes, fixed_nodes):
|
def arrange(self, nodes: List[SceneNode], fixed_nodes: List[SceneNode]) -> None:
|
||||||
min_offset = self.getBuildVolume().getEdgeDisallowedSize() + 2 # Allow for some rounding errors
|
min_offset = self.getBuildVolume().getEdgeDisallowedSize() + 2 # Allow for some rounding errors
|
||||||
job = ArrangeObjectsJob(nodes, fixed_nodes, min_offset = max(min_offset, 8))
|
job = ArrangeObjectsJob(nodes, fixed_nodes, min_offset = max(min_offset, 8))
|
||||||
job.start()
|
job.start()
|
||||||
|
|
||||||
## Reload all mesh data on the screen from file.
|
## Reload all mesh data on the screen from file.
|
||||||
@pyqtSlot()
|
@pyqtSlot()
|
||||||
def reloadAll(self):
|
def reloadAll(self) -> None:
|
||||||
Logger.log("i", "Reloading all loaded mesh data.")
|
Logger.log("i", "Reloading all loaded mesh data.")
|
||||||
nodes = []
|
nodes = []
|
||||||
has_merged_nodes = False
|
has_merged_nodes = False
|
||||||
for node in DepthFirstIterator(self.getController().getScene().getRoot()):
|
for node in DepthFirstIterator(self.getController().getScene().getRoot()): # type: ignore
|
||||||
if not isinstance(node, CuraSceneNode) or not node.getMeshData() :
|
if not isinstance(node, CuraSceneNode) or not node.getMeshData():
|
||||||
if node.getName() == "MergedMesh":
|
if node.getName() == "MergedMesh":
|
||||||
has_merged_nodes = True
|
has_merged_nodes = True
|
||||||
continue
|
continue
|
||||||
@ -1313,7 +1335,7 @@ class CuraApplication(QtApplication):
|
|||||||
file_name = node.getMeshData().getFileName()
|
file_name = node.getMeshData().getFileName()
|
||||||
if file_name:
|
if file_name:
|
||||||
job = ReadMeshJob(file_name)
|
job = ReadMeshJob(file_name)
|
||||||
job._node = node
|
job._node = node # type: ignore
|
||||||
job.finished.connect(self._reloadMeshFinished)
|
job.finished.connect(self._reloadMeshFinished)
|
||||||
if has_merged_nodes:
|
if has_merged_nodes:
|
||||||
job.finished.connect(self.updateOriginOfMergedMeshes)
|
job.finished.connect(self.updateOriginOfMergedMeshes)
|
||||||
@ -1322,20 +1344,8 @@ class CuraApplication(QtApplication):
|
|||||||
else:
|
else:
|
||||||
Logger.log("w", "Unable to reload data because we don't have a filename.")
|
Logger.log("w", "Unable to reload data because we don't have a filename.")
|
||||||
|
|
||||||
|
|
||||||
## Get logging data of the backend engine
|
|
||||||
# \returns \type{string} Logging data
|
|
||||||
@pyqtSlot(result = str)
|
|
||||||
def getEngineLog(self):
|
|
||||||
log = ""
|
|
||||||
|
|
||||||
for entry in self.getBackend().getLog():
|
|
||||||
log += entry.decode()
|
|
||||||
|
|
||||||
return log
|
|
||||||
|
|
||||||
@pyqtSlot("QStringList")
|
@pyqtSlot("QStringList")
|
||||||
def setExpandedCategories(self, categories):
|
def setExpandedCategories(self, categories: List[str]) -> None:
|
||||||
categories = list(set(categories))
|
categories = list(set(categories))
|
||||||
categories.sort()
|
categories.sort()
|
||||||
joined = ";".join(categories)
|
joined = ";".join(categories)
|
||||||
@ -1346,7 +1356,7 @@ class CuraApplication(QtApplication):
|
|||||||
expandedCategoriesChanged = pyqtSignal()
|
expandedCategoriesChanged = pyqtSignal()
|
||||||
|
|
||||||
@pyqtProperty("QStringList", notify = expandedCategoriesChanged)
|
@pyqtProperty("QStringList", notify = expandedCategoriesChanged)
|
||||||
def expandedCategories(self):
|
def expandedCategories(self) -> List[str]:
|
||||||
return self.getPreferences().getValue("cura/categories_expanded").split(";")
|
return self.getPreferences().getValue("cura/categories_expanded").split(";")
|
||||||
|
|
||||||
@pyqtSlot()
|
@pyqtSlot()
|
||||||
@ -1396,13 +1406,12 @@ class CuraApplication(QtApplication):
|
|||||||
|
|
||||||
|
|
||||||
## Updates origin position of all merged meshes
|
## Updates origin position of all merged meshes
|
||||||
# \param jobNode \type{Job} empty object which passed which is required by JobQueue
|
def updateOriginOfMergedMeshes(self, _):
|
||||||
def updateOriginOfMergedMeshes(self, jobNode):
|
|
||||||
group_nodes = []
|
group_nodes = []
|
||||||
for node in DepthFirstIterator(self.getController().getScene().getRoot()):
|
for node in DepthFirstIterator(self.getController().getScene().getRoot()):
|
||||||
if isinstance(node, CuraSceneNode) and node.getName() == "MergedMesh":
|
if isinstance(node, CuraSceneNode) and node.getName() == "MergedMesh":
|
||||||
|
|
||||||
#checking by name might be not enough, the merged mesh should has "GroupDecorator" decorator
|
# Checking by name might be not enough, the merged mesh should has "GroupDecorator" decorator
|
||||||
for decorator in node.getDecorators():
|
for decorator in node.getDecorators():
|
||||||
if isinstance(decorator, GroupDecorator):
|
if isinstance(decorator, GroupDecorator):
|
||||||
group_nodes.append(node)
|
group_nodes.append(node)
|
||||||
@ -1446,7 +1455,7 @@ class CuraApplication(QtApplication):
|
|||||||
|
|
||||||
|
|
||||||
@pyqtSlot()
|
@pyqtSlot()
|
||||||
def groupSelected(self):
|
def groupSelected(self) -> None:
|
||||||
# Create a group-node
|
# Create a group-node
|
||||||
group_node = CuraSceneNode()
|
group_node = CuraSceneNode()
|
||||||
group_decorator = GroupDecorator()
|
group_decorator = GroupDecorator()
|
||||||
@ -1462,7 +1471,8 @@ class CuraApplication(QtApplication):
|
|||||||
# Remove nodes that are directly parented to another selected node from the selection so they remain parented
|
# Remove nodes that are directly parented to another selected node from the selection so they remain parented
|
||||||
selected_nodes = Selection.getAllSelectedObjects().copy()
|
selected_nodes = Selection.getAllSelectedObjects().copy()
|
||||||
for node in selected_nodes:
|
for node in selected_nodes:
|
||||||
if node.getParent() in selected_nodes and not node.getParent().callDecoration("isGroup"):
|
parent = node.getParent()
|
||||||
|
if parent is not None and parent in selected_nodes and not parent.callDecoration("isGroup"):
|
||||||
Selection.remove(node)
|
Selection.remove(node)
|
||||||
|
|
||||||
# Move selected nodes into the group-node
|
# Move selected nodes into the group-node
|
||||||
@ -1474,7 +1484,7 @@ class CuraApplication(QtApplication):
|
|||||||
Selection.add(group_node)
|
Selection.add(group_node)
|
||||||
|
|
||||||
@pyqtSlot()
|
@pyqtSlot()
|
||||||
def ungroupSelected(self):
|
def ungroupSelected(self) -> None:
|
||||||
selected_objects = Selection.getAllSelectedObjects().copy()
|
selected_objects = Selection.getAllSelectedObjects().copy()
|
||||||
for node in selected_objects:
|
for node in selected_objects:
|
||||||
if node.callDecoration("isGroup"):
|
if node.callDecoration("isGroup"):
|
||||||
@ -1497,7 +1507,7 @@ class CuraApplication(QtApplication):
|
|||||||
# Note: The group removes itself from the scene once all its children have left it,
|
# Note: The group removes itself from the scene once all its children have left it,
|
||||||
# see GroupDecorator._onChildrenChanged
|
# see GroupDecorator._onChildrenChanged
|
||||||
|
|
||||||
def _createSplashScreen(self):
|
def _createSplashScreen(self) -> Optional[CuraSplashScreen.CuraSplashScreen]:
|
||||||
if self._is_headless:
|
if self._is_headless:
|
||||||
return None
|
return None
|
||||||
return CuraSplashScreen.CuraSplashScreen()
|
return CuraSplashScreen.CuraSplashScreen()
|
||||||
@ -1663,7 +1673,9 @@ class CuraApplication(QtApplication):
|
|||||||
is_non_sliceable = "." + file_extension in self._non_sliceable_extensions
|
is_non_sliceable = "." + file_extension in self._non_sliceable_extensions
|
||||||
|
|
||||||
if is_non_sliceable:
|
if is_non_sliceable:
|
||||||
self.callLater(lambda: self.getController().setActiveView("SimulationView"))
|
# Need to switch first to the preview stage and then to layer view
|
||||||
|
self.callLater(lambda: (self.getController().setActiveStage("PreviewStage"),
|
||||||
|
self.getController().setActiveView("SimulationView")))
|
||||||
|
|
||||||
block_slicing_decorator = BlockSlicingDecorator()
|
block_slicing_decorator = BlockSlicingDecorator()
|
||||||
node.addDecorator(block_slicing_decorator)
|
node.addDecorator(block_slicing_decorator)
|
||||||
@ -1761,3 +1773,16 @@ class CuraApplication(QtApplication):
|
|||||||
def getSidebarCustomMenuItems(self) -> list:
|
def getSidebarCustomMenuItems(self) -> list:
|
||||||
return self._sidebar_custom_menu_items
|
return self._sidebar_custom_menu_items
|
||||||
|
|
||||||
|
@pyqtSlot(result = bool)
|
||||||
|
def shouldShowWelcomeDialog(self) -> bool:
|
||||||
|
# Only show the complete flow if there is no printer yet.
|
||||||
|
return self._machine_manager.activeMachine is None
|
||||||
|
|
||||||
|
@pyqtSlot(result = bool)
|
||||||
|
def shouldShowWhatsNewDialog(self) -> bool:
|
||||||
|
has_active_machine = self._machine_manager.activeMachine is not None
|
||||||
|
has_app_just_upgraded = self.hasJustUpdatedFromOldVersion()
|
||||||
|
|
||||||
|
# Only show the what's new dialog if there's no machine and we have just upgraded
|
||||||
|
show_whatsnew_only = has_active_machine and has_app_just_upgraded
|
||||||
|
return show_whatsnew_only
|
||||||
|
@ -1,9 +1,12 @@
|
|||||||
# 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@"
|
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@"
|
||||||
|
24
cura/CuraView.py
Normal file
24
cura/CuraView.py
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
# 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.View.View import View
|
||||||
|
|
||||||
|
|
||||||
|
# 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) -> None:
|
||||||
|
super().__init__(parent)
|
||||||
|
|
||||||
|
@pyqtProperty(QUrl, constant = True)
|
||||||
|
def mainComponent(self) -> QUrl:
|
||||||
|
return self.getDisplayComponent("main")
|
||||||
|
|
||||||
|
@pyqtProperty(QUrl, constant = True)
|
||||||
|
def stageMenuComponent(self) -> QUrl:
|
||||||
|
return self.getDisplayComponent("menu")
|
@ -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
|
||||||
|
@ -2,9 +2,11 @@
|
|||||||
# 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
|
PrimeTower = 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,16 +57,16 @@ 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)
|
||||||
@ -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
|
||||||
|
|
||||||
@ -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()
|
||||||
])
|
])
|
||||||
|
|
||||||
return cls.__color_map
|
return cls.__color_map
|
||||||
|
@ -33,6 +33,12 @@ class MachineAction(QObject, PluginObject):
|
|||||||
def getKey(self) -> str:
|
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) -> str:
|
def label(self) -> str:
|
||||||
return self._label
|
return self._label
|
||||||
|
@ -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:
|
||||||
|
@ -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
|
||||||
|
|
||||||
@ -218,7 +219,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 +299,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 +332,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
|
||||||
@ -446,6 +450,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
|
||||||
@ -510,16 +536,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")
|
||||||
@ -602,7 +652,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)
|
||||||
@ -622,8 +671,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)
|
||||||
|
|
||||||
@ -654,7 +705,11 @@ class MaterialManager(QObject):
|
|||||||
|
|
||||||
@pyqtSlot(str)
|
@pyqtSlot(str)
|
||||||
def removeFavorite(self, root_material_id: str) -> None:
|
def removeFavorite(self, root_material_id: str) -> None:
|
||||||
self._favorites.remove(root_material_id)
|
try:
|
||||||
|
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
|
||||||
@ -82,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):
|
||||||
@ -93,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)
|
||||||
@ -102,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
|
||||||
|
167
cura/Machines/Models/DiscoveredPrintersModel.py
Normal file
167
cura/Machines/Models/DiscoveredPrintersModel.py
Normal file
@ -0,0 +1,167 @@
|
|||||||
|
# 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
|
||||||
|
|
||||||
|
from UM.i18n import i18nCatalog
|
||||||
|
from UM.Logger import Logger
|
||||||
|
from UM.Util import parseBool
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from PyQt5.QtCore import QObject
|
||||||
|
|
||||||
|
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()
|
||||||
|
|
||||||
|
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()
|
||||||
|
|
||||||
|
# Human readable machine type string
|
||||||
|
@pyqtProperty(str, notify = machineTypeChanged)
|
||||||
|
def readableMachineType(self) -> str:
|
||||||
|
from cura.CuraApplication import CuraApplication
|
||||||
|
readable_type = CuraApplication.getInstance().getMachineManager().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:
|
||||||
|
from cura.CuraApplication import CuraApplication
|
||||||
|
readable_type = CuraApplication.getInstance().getMachineManager().getMachineTypeNameFromId(self._machine_type)
|
||||||
|
return not readable_type
|
||||||
|
|
||||||
|
@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, parent: Optional["QObject"] = None) -> None:
|
||||||
|
super().__init__(parent)
|
||||||
|
|
||||||
|
self._discovered_printer_by_ip_dict = dict() # type: Dict[str, DiscoveredPrinter]
|
||||||
|
|
||||||
|
discoveredPrintersChanged = pyqtSignal()
|
||||||
|
|
||||||
|
@pyqtProperty(list, 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())
|
||||||
|
|
||||||
|
@pyqtSlot(str)
|
||||||
|
def createMachineFromDiscoveredPrinterAddress(self, ip_address: str) -> None:
|
||||||
|
if ip_address not in self._discovered_printer_by_ip_dict:
|
||||||
|
Logger.log("i", "Key [%s] does not exist in the discovered printers list.", ip_address)
|
||||||
|
return
|
||||||
|
|
||||||
|
self.createMachineFromDiscoveredPrinter(self._discovered_printer_by_ip_dict[ip_address])
|
@ -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
|
||||||
|
104
cura/Machines/Models/FirstStartMachineActionsModel.py
Normal file
104
cura/Machines/Models/FirstStartMachineActionsModel.py
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
# 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)
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
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.displayItem,
|
||||||
|
"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 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 container_stack.getMetaDataEntry("hidden", False) in ["True", True]:
|
||||||
|
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"])
|
||||||
|
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,8 +35,9 @@ 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):
|
||||||
self._max_build_plate = max_build_plate
|
if self._max_build_plate != max_build_plate:
|
||||||
self.maxBuildPlateChanged.emit()
|
self._max_build_plate = max_build_plate
|
||||||
|
self.maxBuildPlateChanged.emit()
|
||||||
|
|
||||||
## Return the highest build plate number
|
## Return the highest build plate number
|
||||||
@pyqtProperty(int, notify = maxBuildPlateChanged)
|
@pyqtProperty(int, notify = maxBuildPlateChanged)
|
||||||
@ -43,15 +45,17 @@ class MultiBuildPlateModel(ListModel):
|
|||||||
return self._max_build_plate
|
return self._max_build_plate
|
||||||
|
|
||||||
def setActiveBuildPlate(self, nr):
|
def setActiveBuildPlate(self, nr):
|
||||||
self._active_build_plate = nr
|
if self._active_build_plate != nr:
|
||||||
self.activeBuildPlateChanged.emit()
|
self._active_build_plate = nr
|
||||||
|
self.activeBuildPlateChanged.emit()
|
||||||
|
|
||||||
@pyqtProperty(int, notify = activeBuildPlateChanged)
|
@pyqtProperty(int, notify = activeBuildPlateChanged)
|
||||||
def activeBuildPlate(self):
|
def activeBuildPlate(self):
|
||||||
return self._active_build_plate
|
return self._active_build_plate
|
||||||
|
|
||||||
def _updateSelectedObjectBuildPlateNumbersDelayed(self, *args):
|
def _updateSelectedObjectBuildPlateNumbersDelayed(self, *args):
|
||||||
self._update_timer.start()
|
if not isinstance(args[0], Camera):
|
||||||
|
self._update_timer.start()
|
||||||
|
|
||||||
def _updateSelectedObjectBuildPlateNumbers(self, *args):
|
def _updateSelectedObjectBuildPlateNumbers(self, *args):
|
||||||
result = set()
|
result = set()
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
@ -6,6 +6,7 @@ from typing import Optional, List
|
|||||||
from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject
|
from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject
|
||||||
|
|
||||||
from UM.Logger import Logger
|
from UM.Logger import Logger
|
||||||
|
from UM.Preferences import Preferences
|
||||||
from UM.Resources import Resources
|
from UM.Resources import Resources
|
||||||
|
|
||||||
from UM.i18n import i18nCatalog
|
from UM.i18n import i18nCatalog
|
||||||
@ -18,14 +19,20 @@ class SettingVisibilityPresetsModel(QObject):
|
|||||||
onItemsChanged = pyqtSignal()
|
onItemsChanged = pyqtSignal()
|
||||||
activePresetChanged = pyqtSignal()
|
activePresetChanged = pyqtSignal()
|
||||||
|
|
||||||
def __init__(self, preferences, parent = None):
|
def __init__(self, preferences: Preferences, parent = None) -> None:
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
|
|
||||||
self._items = [] # type: List[SettingVisibilityPreset]
|
self._items = [] # type: List[SettingVisibilityPreset]
|
||||||
|
self._custom_preset = SettingVisibilityPreset(preset_id = "custom", name = "Custom selection", weight = -100)
|
||||||
|
|
||||||
self._populate()
|
self._populate()
|
||||||
|
|
||||||
basic_item = self.getVisibilityPresetById("basic")
|
basic_item = self.getVisibilityPresetById("basic")
|
||||||
basic_visibile_settings = ";".join(basic_item.settings)
|
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
|
self._preferences = preferences
|
||||||
|
|
||||||
@ -42,7 +49,8 @@ class SettingVisibilityPresetsModel(QObject):
|
|||||||
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")
|
||||||
|
|
||||||
@ -59,9 +67,7 @@ class SettingVisibilityPresetsModel(QObject):
|
|||||||
def _populate(self) -> None:
|
def _populate(self) -> None:
|
||||||
from cura.CuraApplication import CuraApplication
|
from cura.CuraApplication import CuraApplication
|
||||||
items = [] # type: List[SettingVisibilityPreset]
|
items = [] # type: List[SettingVisibilityPreset]
|
||||||
|
items.append(self._custom_preset)
|
||||||
custom_preset = SettingVisibilityPreset(preset_id="custom", name ="Custom selection", weight = -100)
|
|
||||||
items.append(custom_preset)
|
|
||||||
for file_path in Resources.getAllResourcesOfType(CuraApplication.ResourceTypes.SettingVisibilityPreset):
|
for file_path in Resources.getAllResourcesOfType(CuraApplication.ResourceTypes.SettingVisibilityPreset):
|
||||||
setting_visibility_preset = SettingVisibilityPreset()
|
setting_visibility_preset = SettingVisibilityPreset()
|
||||||
try:
|
try:
|
||||||
@ -77,7 +83,7 @@ class SettingVisibilityPresetsModel(QObject):
|
|||||||
self.setItems(items)
|
self.setItems(items)
|
||||||
|
|
||||||
@pyqtProperty("QVariantList", notify = onItemsChanged)
|
@pyqtProperty("QVariantList", notify = onItemsChanged)
|
||||||
def items(self):
|
def items(self) -> List[SettingVisibilityPreset]:
|
||||||
return self._items
|
return self._items
|
||||||
|
|
||||||
def setItems(self, items: List[SettingVisibilityPreset]) -> None:
|
def setItems(self, items: List[SettingVisibilityPreset]) -> None:
|
||||||
@ -87,7 +93,7 @@ class SettingVisibilityPresetsModel(QObject):
|
|||||||
|
|
||||||
@pyqtSlot(str)
|
@pyqtSlot(str)
|
||||||
def setActivePreset(self, preset_id: str) -> None:
|
def setActivePreset(self, preset_id: str) -> None:
|
||||||
if preset_id == self._active_preset_item.presetId:
|
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
|
||||||
|
|
||||||
@ -96,7 +102,7 @@ class SettingVisibilityPresetsModel(QObject):
|
|||||||
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.presetId == "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")
|
||||||
@ -117,7 +123,9 @@ class SettingVisibilityPresetsModel(QObject):
|
|||||||
|
|
||||||
@pyqtProperty(str, notify = activePresetChanged)
|
@pyqtProperty(str, notify = activePresetChanged)
|
||||||
def activePreset(self) -> str:
|
def activePreset(self) -> str:
|
||||||
return self._active_preset_item.presetId
|
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":
|
||||||
@ -140,7 +148,7 @@ class SettingVisibilityPresetsModel(QObject):
|
|||||||
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.presetId == "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:
|
||||||
@ -149,7 +157,12 @@ class SettingVisibilityPresetsModel(QObject):
|
|||||||
else:
|
else:
|
||||||
item_to_set = matching_preset_item
|
item_to_set = matching_preset_item
|
||||||
|
|
||||||
|
# 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:
|
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.presetId)
|
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
|
||||||
@ -209,6 +209,7 @@ class QualityManager(QObject):
|
|||||||
# (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 +236,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 +260,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 +338,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 +368,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 +442,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 +540,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 "",
|
||||||
}
|
}
|
||||||
return self.parseTokenResponse(requests.post(self._token_url, data = data)) # type: ignore
|
try:
|
||||||
|
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 "",
|
||||||
}
|
}
|
||||||
return self.parseTokenResponse(requests.post(self._token_url, data = data)) # type: ignore
|
try:
|
||||||
|
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"]:
|
||||||
token_request = requests.get("{}/check-token".format(self._settings.OAUTH_SERVER_URL), headers = {
|
try:
|
||||||
"Authorization": "Bearer {}".format(access_token)
|
token_request = requests.get("{}/check-token".format(self._settings.OAUTH_SERVER_URL), headers = {
|
||||||
})
|
"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,28 +1,33 @@
|
|||||||
# 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()
|
||||||
|
|
||||||
@ -38,31 +43,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.
|
||||||
self._user_profile = self._parseJWT()
|
try:
|
||||||
if not self._user_profile:
|
self._user_profile = self._parseJWT()
|
||||||
|
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 +92,51 @@ 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
|
||||||
|
|
||||||
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:
|
||||||
|
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 +152,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 +163,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,11 +181,22 @@ 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:
|
||||||
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!")
|
||||||
@ -166,3 +209,7 @@ 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)
|
||||||
|
|
||||||
|
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.
|
||||||
|
@ -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):
|
||||||
|
@ -9,7 +9,7 @@ from typing import Union
|
|||||||
|
|
||||||
MYPY = False
|
MYPY = False
|
||||||
if MYPY:
|
if MYPY:
|
||||||
from cura.PrinterOutputDevice import PrinterOutputDevice
|
from cura.PrinterOutput.PrinterOutputDevice import PrinterOutputDevice
|
||||||
|
|
||||||
class FirmwareUpdater(QObject):
|
class FirmwareUpdater(QObject):
|
||||||
firmwareProgressChanged = pyqtSignal()
|
firmwareProgressChanged = pyqtSignal()
|
||||||
|
@ -3,14 +3,15 @@
|
|||||||
|
|
||||||
from typing import TYPE_CHECKING, Set, Union, Optional
|
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.PrinterOutputDevice import PrinterOutputDevice
|
from .PrinterOutputDevice import PrinterOutputDevice
|
||||||
from cura.PrinterOutput.ExtruderOutputModel import ExtruderOutputModel
|
from .Models.ExtruderOutputModel import ExtruderOutputModel
|
||||||
|
|
||||||
|
|
||||||
class GenericOutputController(PrinterOutputController):
|
class GenericOutputController(PrinterOutputController):
|
||||||
@ -81,8 +82,8 @@ class GenericOutputController(PrinterOutputController):
|
|||||||
self._output_device.cancelPrint()
|
self._output_device.cancelPrint()
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def setTargetBedTemperature(self, printer: "PrinterOutputModel", temperature: int) -> None:
|
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) -> None:
|
def _onTargetBedTemperatureChanged(self) -> None:
|
||||||
if self._preheat_bed_timer.isActive() and self._preheat_printer and self._preheat_printer.targetBedTemperature == 0:
|
if self._preheat_bed_timer.isActive() and self._preheat_printer and self._preheat_printer.targetBedTemperature == 0:
|
||||||
@ -96,14 +97,14 @@ class GenericOutputController(PrinterOutputController):
|
|||||||
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") -> None:
|
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)
|
||||||
|
|
||||||
|
@ -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):
|
@ -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
|
@ -1,16 +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 PyQt5.QtCore import pyqtSignal, pyqtProperty, QObject, pyqtSlot
|
|
||||||
from typing import Optional, TYPE_CHECKING, List
|
from typing import Optional, TYPE_CHECKING, List
|
||||||
|
|
||||||
from PyQt5.QtCore import QUrl
|
from PyQt5.QtCore import pyqtSignal, pyqtProperty, QObject, pyqtSlot, QUrl
|
||||||
from PyQt5.QtGui import QImage
|
from PyQt5.QtGui import QImage
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from cura.PrinterOutput.PrinterOutputController import PrinterOutputController
|
from cura.PrinterOutput.PrinterOutputController import PrinterOutputController
|
||||||
from cura.PrinterOutput.PrinterOutputModel import PrinterOutputModel
|
from cura.PrinterOutput.Models.PrinterOutputModel import PrinterOutputModel
|
||||||
from cura.PrinterOutput.ConfigurationModel import ConfigurationModel
|
from cura.PrinterOutput.Models.PrinterConfigurationModel import PrinterConfigurationModel
|
||||||
|
|
||||||
|
|
||||||
class PrintJobOutputModel(QObject):
|
class PrintJobOutputModel(QObject):
|
||||||
stateChanged = pyqtSignal()
|
stateChanged = pyqtSignal()
|
||||||
@ -24,7 +24,7 @@ class PrintJobOutputModel(QObject):
|
|||||||
previewImageChanged = pyqtSignal()
|
previewImageChanged = pyqtSignal()
|
||||||
compatibleMachineFamiliesChanged = pyqtSignal()
|
compatibleMachineFamiliesChanged = pyqtSignal()
|
||||||
|
|
||||||
def __init__(self, output_controller: "PrinterOutputController", key: str = "", name: str = "", parent=None) -> None:
|
def __init__(self, output_controller: "PrinterOutputController", key: str = "", name: str = "", parent = None) -> None:
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
self._output_controller = output_controller
|
self._output_controller = output_controller
|
||||||
self._state = ""
|
self._state = ""
|
||||||
@ -35,7 +35,7 @@ class PrintJobOutputModel(QObject):
|
|||||||
self._assigned_printer = None # type: Optional[PrinterOutputModel]
|
self._assigned_printer = None # type: Optional[PrinterOutputModel]
|
||||||
self._owner = "" # Who started/owns the print job?
|
self._owner = "" # Who started/owns the print job?
|
||||||
|
|
||||||
self._configuration = None # type: Optional[ConfigurationModel]
|
self._configuration = None # type: Optional[PrinterConfigurationModel]
|
||||||
self._compatible_machine_families = [] # type: List[str]
|
self._compatible_machine_families = [] # type: List[str]
|
||||||
self._preview_image_id = 0
|
self._preview_image_id = 0
|
||||||
|
|
||||||
@ -44,7 +44,7 @@ class PrintJobOutputModel(QObject):
|
|||||||
@pyqtProperty("QStringList", notify=compatibleMachineFamiliesChanged)
|
@pyqtProperty("QStringList", notify=compatibleMachineFamiliesChanged)
|
||||||
def compatibleMachineFamilies(self):
|
def compatibleMachineFamilies(self):
|
||||||
# Hack; Some versions of cluster will return a family more than once...
|
# Hack; Some versions of cluster will return a family more than once...
|
||||||
return set(self._compatible_machine_families)
|
return list(set(self._compatible_machine_families))
|
||||||
|
|
||||||
def setCompatibleMachineFamilies(self, compatible_machine_families: List[str]) -> None:
|
def setCompatibleMachineFamilies(self, compatible_machine_families: List[str]) -> None:
|
||||||
if self._compatible_machine_families != compatible_machine_families:
|
if self._compatible_machine_families != compatible_machine_families:
|
||||||
@ -54,7 +54,7 @@ class PrintJobOutputModel(QObject):
|
|||||||
@pyqtProperty(QUrl, notify=previewImageChanged)
|
@pyqtProperty(QUrl, notify=previewImageChanged)
|
||||||
def previewImageUrl(self):
|
def previewImageUrl(self):
|
||||||
self._preview_image_id += 1
|
self._preview_image_id += 1
|
||||||
# There is an image provider that is called "camera". In order to ensure that the image qml object, that
|
# 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
|
# 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.
|
# 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
|
temp = "image://print_job_preview/" + str(self._preview_image_id) + "/" + self._key
|
||||||
@ -69,10 +69,10 @@ class PrintJobOutputModel(QObject):
|
|||||||
self.previewImageChanged.emit()
|
self.previewImageChanged.emit()
|
||||||
|
|
||||||
@pyqtProperty(QObject, notify=configurationChanged)
|
@pyqtProperty(QObject, notify=configurationChanged)
|
||||||
def configuration(self) -> Optional["ConfigurationModel"]:
|
def configuration(self) -> Optional["PrinterConfigurationModel"]:
|
||||||
return self._configuration
|
return self._configuration
|
||||||
|
|
||||||
def updateConfiguration(self, configuration: Optional["ConfigurationModel"]) -> None:
|
def updateConfiguration(self, configuration: Optional["PrinterConfigurationModel"]) -> None:
|
||||||
if self._configuration != configuration:
|
if self._configuration != configuration:
|
||||||
self._configuration = configuration
|
self._configuration = configuration
|
||||||
self.configurationChanged.emit()
|
self.configurationChanged.emit()
|
||||||
@ -118,17 +118,39 @@ class PrintJobOutputModel(QObject):
|
|||||||
self.nameChanged.emit()
|
self.nameChanged.emit()
|
||||||
|
|
||||||
@pyqtProperty(int, notify = timeTotalChanged)
|
@pyqtProperty(int, notify = timeTotalChanged)
|
||||||
def timeTotal(self):
|
def timeTotal(self) -> int:
|
||||||
return self._time_total
|
return self._time_total
|
||||||
|
|
||||||
@pyqtProperty(int, notify = timeElapsedChanged)
|
@pyqtProperty(int, notify = timeElapsedChanged)
|
||||||
def timeElapsed(self):
|
def timeElapsed(self) -> int:
|
||||||
return self._time_elapsed
|
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)
|
@pyqtProperty(str, notify=stateChanged)
|
||||||
def state(self):
|
def state(self) -> str:
|
||||||
return self._state
|
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):
|
def updateTimeTotal(self, new_time_total):
|
||||||
if self._time_total != new_time_total:
|
if self._time_total != new_time_total:
|
||||||
self._time_total = new_time_total
|
self._time_total = new_time_total
|
@ -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:
|
||||||
self._buildplate_configuration = buildplate_configuration
|
if 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 = []
|
@ -1,17 +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.
|
||||||
|
|
||||||
from PyQt5.QtCore import pyqtSignal, pyqtProperty, QObject, QVariant, pyqtSlot
|
from PyQt5.QtCore import pyqtSignal, pyqtProperty, QObject, QVariant, pyqtSlot, QUrl
|
||||||
from typing import List, Dict, Optional
|
from typing import List, Dict, Optional
|
||||||
from UM.Math.Vector import Vector
|
from UM.Math.Vector import Vector
|
||||||
from cura.PrinterOutput.ConfigurationModel import ConfigurationModel
|
from cura.PrinterOutput.Models.PrinterConfigurationModel import PrinterConfigurationModel
|
||||||
from cura.PrinterOutput.ExtruderOutputModel import ExtruderOutputModel
|
from cura.PrinterOutput.Models.ExtruderOutputModel import ExtruderOutputModel
|
||||||
|
|
||||||
MYPY = False
|
MYPY = False
|
||||||
if MYPY:
|
if MYPY:
|
||||||
from cura.PrinterOutput.PrintJobOutputModel import PrintJobOutputModel
|
from cura.PrinterOutput.Models.PrintJobOutputModel import PrintJobOutputModel
|
||||||
from cura.PrinterOutput.PrinterOutputController import PrinterOutputController
|
from cura.PrinterOutput.PrinterOutputController import PrinterOutputController
|
||||||
from cura.PrinterOutput.NetworkCamera import NetworkCamera
|
|
||||||
|
|
||||||
|
|
||||||
class PrinterOutputModel(QObject):
|
class PrinterOutputModel(QObject):
|
||||||
@ -23,43 +22,47 @@ class PrinterOutputModel(QObject):
|
|||||||
nameChanged = pyqtSignal()
|
nameChanged = pyqtSignal()
|
||||||
headPositionChanged = pyqtSignal()
|
headPositionChanged = pyqtSignal()
|
||||||
keyChanged = pyqtSignal()
|
keyChanged = pyqtSignal()
|
||||||
printerTypeChanged = pyqtSignal()
|
typeChanged = pyqtSignal()
|
||||||
buildplateChanged = pyqtSignal()
|
buildplateChanged = pyqtSignal()
|
||||||
cameraChanged = pyqtSignal()
|
cameraUrlChanged = pyqtSignal()
|
||||||
configurationChanged = pyqtSignal()
|
configurationChanged = pyqtSignal()
|
||||||
canUpdateFirmwareChanged = pyqtSignal()
|
canUpdateFirmwareChanged = pyqtSignal()
|
||||||
|
|
||||||
def __init__(self, output_controller: "PrinterOutputController", number_of_extruders: int = 1, parent=None, firmware_version = "") -> None:
|
def __init__(self, output_controller: "PrinterOutputController", number_of_extruders: int = 1, parent=None, firmware_version = "") -> None:
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
self._bed_temperature = -1 # Use -1 for no heated bed.
|
self._bed_temperature = -1 # type: float # Use -1 for no heated bed.
|
||||||
self._target_bed_temperature = 0
|
self._target_bed_temperature = 0 # type: float
|
||||||
self._name = ""
|
self._name = ""
|
||||||
self._key = "" # Unique identifier
|
self._key = "" # Unique identifier
|
||||||
self._controller = output_controller
|
self._controller = output_controller
|
||||||
self._controller.canUpdateFirmwareChanged.connect(self._onControllerCanUpdateFirmwareChanged)
|
self._controller.canUpdateFirmwareChanged.connect(self._onControllerCanUpdateFirmwareChanged)
|
||||||
self._extruders = [ExtruderOutputModel(printer = self, position = i) for i in range(number_of_extruders)]
|
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._printer_configuration = PrinterConfigurationModel() # Indicates the current configuration setup in this printer
|
||||||
self._head_position = Vector(0, 0, 0)
|
self._head_position = Vector(0, 0, 0)
|
||||||
self._active_print_job = None # type: Optional[PrintJobOutputModel]
|
self._active_print_job = None # type: Optional[PrintJobOutputModel]
|
||||||
self._firmware_version = firmware_version
|
self._firmware_version = firmware_version
|
||||||
self._printer_state = "unknown"
|
self._printer_state = "unknown"
|
||||||
self._is_preheating = False
|
self._is_preheating = False
|
||||||
self._printer_type = ""
|
self._printer_type = ""
|
||||||
self._buildplate_name = ""
|
self._buildplate = ""
|
||||||
|
|
||||||
self._printer_configuration.extruderConfigurations = [extruder.extruderConfiguration for extruder in
|
self._printer_configuration.extruderConfigurations = [extruder.extruderConfiguration for extruder in
|
||||||
self._extruders]
|
self._extruders]
|
||||||
|
|
||||||
self._camera = None # type: Optional[NetworkCamera]
|
self._camera_url = QUrl() # type: QUrl
|
||||||
|
|
||||||
@pyqtProperty(str, constant = True)
|
@pyqtProperty(str, constant = True)
|
||||||
def firmwareVersion(self) -> str:
|
def firmwareVersion(self) -> str:
|
||||||
return self._firmware_version
|
return self._firmware_version
|
||||||
|
|
||||||
def setCamera(self, camera: Optional["NetworkCamera"]) -> None:
|
def setCameraUrl(self, camera_url: "QUrl") -> None:
|
||||||
if self._camera is not camera:
|
if self._camera_url != camera_url:
|
||||||
self._camera = camera
|
self._camera_url = camera_url
|
||||||
self.cameraChanged.emit()
|
self.cameraUrlChanged.emit()
|
||||||
|
|
||||||
|
@pyqtProperty(QUrl, fset = setCameraUrl, notify = cameraUrlChanged)
|
||||||
|
def cameraUrl(self) -> "QUrl":
|
||||||
|
return self._camera_url
|
||||||
|
|
||||||
def updateIsPreheating(self, pre_heating: bool) -> None:
|
def updateIsPreheating(self, pre_heating: bool) -> None:
|
||||||
if self._is_preheating != pre_heating:
|
if self._is_preheating != pre_heating:
|
||||||
@ -70,11 +73,7 @@ class PrinterOutputModel(QObject):
|
|||||||
def isPreheating(self) -> bool:
|
def isPreheating(self) -> bool:
|
||||||
return self._is_preheating
|
return self._is_preheating
|
||||||
|
|
||||||
@pyqtProperty(QObject, notify=cameraChanged)
|
@pyqtProperty(str, notify = typeChanged)
|
||||||
def camera(self) -> Optional["NetworkCamera"]:
|
|
||||||
return self._camera
|
|
||||||
|
|
||||||
@pyqtProperty(str, notify = printerTypeChanged)
|
|
||||||
def type(self) -> str:
|
def type(self) -> str:
|
||||||
return self._printer_type
|
return self._printer_type
|
||||||
|
|
||||||
@ -82,17 +81,17 @@ class PrinterOutputModel(QObject):
|
|||||||
if self._printer_type != printer_type:
|
if self._printer_type != printer_type:
|
||||||
self._printer_type = printer_type
|
self._printer_type = printer_type
|
||||||
self._printer_configuration.printerType = self._printer_type
|
self._printer_configuration.printerType = self._printer_type
|
||||||
self.printerTypeChanged.emit()
|
self.typeChanged.emit()
|
||||||
self.configurationChanged.emit()
|
self.configurationChanged.emit()
|
||||||
|
|
||||||
@pyqtProperty(str, notify = buildplateChanged)
|
@pyqtProperty(str, notify = buildplateChanged)
|
||||||
def buildplate(self) -> str:
|
def buildplate(self) -> str:
|
||||||
return self._buildplate_name
|
return self._buildplate
|
||||||
|
|
||||||
def updateBuildplateName(self, buildplate_name: str) -> None:
|
def updateBuildplate(self, buildplate: str) -> None:
|
||||||
if self._buildplate_name != buildplate_name:
|
if self._buildplate != buildplate:
|
||||||
self._buildplate_name = buildplate_name
|
self._buildplate = buildplate
|
||||||
self._printer_configuration.buildplateConfiguration = self._buildplate_name
|
self._printer_configuration.buildplateConfiguration = self._buildplate
|
||||||
self.buildplateChanged.emit()
|
self.buildplateChanged.emit()
|
||||||
self.configurationChanged.emit()
|
self.configurationChanged.emit()
|
||||||
|
|
||||||
@ -180,7 +179,6 @@ class PrinterOutputModel(QObject):
|
|||||||
return self._name
|
return self._name
|
||||||
|
|
||||||
def setName(self, name: str) -> None:
|
def setName(self, name: str) -> None:
|
||||||
self._setName(name)
|
|
||||||
self.updateName(name)
|
self.updateName(name)
|
||||||
|
|
||||||
def updateName(self, name: str) -> None:
|
def updateName(self, name: str) -> None:
|
||||||
@ -189,19 +187,19 @@ class PrinterOutputModel(QObject):
|
|||||||
self.nameChanged.emit()
|
self.nameChanged.emit()
|
||||||
|
|
||||||
## Update the bed temperature. This only changes it locally.
|
## Update the bed temperature. This only changes it locally.
|
||||||
def updateBedTemperature(self, temperature: int) -> None:
|
def updateBedTemperature(self, temperature: float) -> None:
|
||||||
if self._bed_temperature != temperature:
|
if self._bed_temperature != temperature:
|
||||||
self._bed_temperature = temperature
|
self._bed_temperature = temperature
|
||||||
self.bedTemperatureChanged.emit()
|
self.bedTemperatureChanged.emit()
|
||||||
|
|
||||||
def updateTargetBedTemperature(self, temperature: int) -> None:
|
def updateTargetBedTemperature(self, temperature: float) -> None:
|
||||||
if self._target_bed_temperature != temperature:
|
if self._target_bed_temperature != temperature:
|
||||||
self._target_bed_temperature = temperature
|
self._target_bed_temperature = temperature
|
||||||
self.targetBedTemperatureChanged.emit()
|
self.targetBedTemperatureChanged.emit()
|
||||||
|
|
||||||
## Set the target bed temperature. This ensures that it's actually sent to the remote.
|
## Set the target bed temperature. This ensures that it's actually sent to the remote.
|
||||||
@pyqtSlot(int)
|
@pyqtSlot(float)
|
||||||
def setTargetBedTemperature(self, temperature: int) -> None:
|
def setTargetBedTemperature(self, temperature: float) -> None:
|
||||||
self._controller.setTargetBedTemperature(self, temperature)
|
self._controller.setTargetBedTemperature(self, temperature)
|
||||||
self.updateTargetBedTemperature(temperature)
|
self.updateTargetBedTemperature(temperature)
|
||||||
|
|
||||||
@ -226,55 +224,55 @@ class PrinterOutputModel(QObject):
|
|||||||
def activePrintJob(self) -> Optional["PrintJobOutputModel"]:
|
def activePrintJob(self) -> Optional["PrintJobOutputModel"]:
|
||||||
return self._active_print_job
|
return self._active_print_job
|
||||||
|
|
||||||
@pyqtProperty(str, notify=stateChanged)
|
@pyqtProperty(str, notify = stateChanged)
|
||||||
def state(self) -> str:
|
def state(self) -> str:
|
||||||
return self._printer_state
|
return self._printer_state
|
||||||
|
|
||||||
@pyqtProperty(int, notify=bedTemperatureChanged)
|
@pyqtProperty(float, notify = bedTemperatureChanged)
|
||||||
def bedTemperature(self) -> int:
|
def bedTemperature(self) -> float:
|
||||||
return self._bed_temperature
|
return self._bed_temperature
|
||||||
|
|
||||||
@pyqtProperty(int, notify=targetBedTemperatureChanged)
|
@pyqtProperty(float, notify = targetBedTemperatureChanged)
|
||||||
def targetBedTemperature(self) -> int:
|
def targetBedTemperature(self) -> float:
|
||||||
return self._target_bed_temperature
|
return self._target_bed_temperature
|
||||||
|
|
||||||
# Does the printer support pre-heating the bed at all
|
# Does the printer support pre-heating the bed at all
|
||||||
@pyqtProperty(bool, constant=True)
|
@pyqtProperty(bool, constant = True)
|
||||||
def canPreHeatBed(self) -> bool:
|
def canPreHeatBed(self) -> bool:
|
||||||
if self._controller:
|
if self._controller:
|
||||||
return self._controller.can_pre_heat_bed
|
return self._controller.can_pre_heat_bed
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Does the printer support pre-heating the bed at all
|
# Does the printer support pre-heating the bed at all
|
||||||
@pyqtProperty(bool, constant=True)
|
@pyqtProperty(bool, constant = True)
|
||||||
def canPreHeatHotends(self) -> bool:
|
def canPreHeatHotends(self) -> bool:
|
||||||
if self._controller:
|
if self._controller:
|
||||||
return self._controller.can_pre_heat_hotends
|
return self._controller.can_pre_heat_hotends
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Does the printer support sending raw G-code at all
|
# Does the printer support sending raw G-code at all
|
||||||
@pyqtProperty(bool, constant=True)
|
@pyqtProperty(bool, constant = True)
|
||||||
def canSendRawGcode(self) -> bool:
|
def canSendRawGcode(self) -> bool:
|
||||||
if self._controller:
|
if self._controller:
|
||||||
return self._controller.can_send_raw_gcode
|
return self._controller.can_send_raw_gcode
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Does the printer support pause at all
|
# Does the printer support pause at all
|
||||||
@pyqtProperty(bool, constant=True)
|
@pyqtProperty(bool, constant = True)
|
||||||
def canPause(self) -> bool:
|
def canPause(self) -> bool:
|
||||||
if self._controller:
|
if self._controller:
|
||||||
return self._controller.can_pause
|
return self._controller.can_pause
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Does the printer support abort at all
|
# Does the printer support abort at all
|
||||||
@pyqtProperty(bool, constant=True)
|
@pyqtProperty(bool, constant = True)
|
||||||
def canAbort(self) -> bool:
|
def canAbort(self) -> bool:
|
||||||
if self._controller:
|
if self._controller:
|
||||||
return self._controller.can_abort
|
return self._controller.can_abort
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Does the printer support manual control at all
|
# Does the printer support manual control at all
|
||||||
@pyqtProperty(bool, constant=True)
|
@pyqtProperty(bool, constant = True)
|
||||||
def canControlManually(self) -> bool:
|
def canControlManually(self) -> bool:
|
||||||
if self._controller:
|
if self._controller:
|
||||||
return self._controller.can_control_manually
|
return self._controller.can_control_manually
|
||||||
@ -293,7 +291,7 @@ class PrinterOutputModel(QObject):
|
|||||||
|
|
||||||
# Returns the configuration (material, variant and buildplate) of the current printer
|
# Returns the configuration (material, variant and buildplate) of the current printer
|
||||||
@pyqtProperty(QObject, notify = configurationChanged)
|
@pyqtProperty(QObject, notify = configurationChanged)
|
||||||
def printerConfiguration(self) -> Optional[ConfigurationModel]:
|
def printerConfiguration(self) -> Optional[PrinterConfigurationModel]:
|
||||||
if self._printer_configuration.isValid():
|
if self._printer_configuration.isValid():
|
||||||
return self._printer_configuration
|
return self._printer_configuration
|
||||||
return None
|
return None
|
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, **kwargs: str) -> 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,7 +131,7 @@ 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.
|
||||||
@ -133,7 +139,7 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice):
|
|||||||
if self._last_manager_create_time is None or time() - self._last_manager_create_time > self._recreate_network_manager_time:
|
if self._last_manager_create_time is None or 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)
|
||||||
@ -143,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()
|
||||||
|
|
||||||
@ -160,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:
|
||||||
@ -178,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._last_request_time = time()
|
||||||
|
|
||||||
|
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()
|
self._validateManager()
|
||||||
request = self._createEmptyRequest(target)
|
|
||||||
self._last_request_time = time()
|
|
||||||
if self._manager is not None:
|
|
||||||
reply = self._manager.deleteResource(request)
|
|
||||||
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:
|
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)
|
||||||
|
self._registerOnFinishedCallback(reply, on_finished)
|
||||||
|
|
||||||
|
## 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)
|
|
||||||
self._last_request_time = time()
|
|
||||||
if self._manager is not None:
|
|
||||||
reply = self._manager.get(request)
|
|
||||||
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:
|
request = self._createEmptyRequest(url)
|
||||||
|
self._last_request_time = time()
|
||||||
|
|
||||||
|
if not self._manager:
|
||||||
|
Logger.log("e", "No network manager was created to execute the GET call with.")
|
||||||
|
return
|
||||||
|
|
||||||
|
reply = self._manager.get(request)
|
||||||
|
self._registerOnFinishedCallback(reply, on_finished)
|
||||||
|
|
||||||
|
## 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)
|
|
||||||
self._last_request_time = time()
|
|
||||||
if self._manager is not None:
|
|
||||||
reply = self._manager.post(request, data)
|
|
||||||
if on_progress is not None:
|
|
||||||
reply.uploadProgress.connect(on_progress)
|
|
||||||
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:
|
request = self._createEmptyRequest(url)
|
||||||
|
self._last_request_time = time()
|
||||||
|
|
||||||
|
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:
|
||||||
|
reply.uploadProgress.connect(on_progress)
|
||||||
|
self._registerOnFinishedCallback(reply, on_finished)
|
||||||
|
|
||||||
|
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)
|
||||||
@ -255,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:
|
||||||
@ -282,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:
|
||||||
|
@ -1,17 +1,15 @@
|
|||||||
# 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.Logger import Logger
|
from UM.Logger import Logger
|
||||||
from UM.Signal import Signal
|
from UM.Signal import Signal
|
||||||
|
|
||||||
from typing import Union
|
|
||||||
|
|
||||||
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 cura.PrinterOutput.PrinterOutputDevice import PrinterOutputDevice
|
from .PrinterOutputDevice import PrinterOutputDevice
|
||||||
|
|
||||||
|
|
||||||
class PrinterOutputController:
|
class PrinterOutputController:
|
||||||
@ -25,10 +23,10 @@ class PrinterOutputController:
|
|||||||
self.can_update_firmware = False
|
self.can_update_firmware = False
|
||||||
self._output_device = output_device
|
self._output_device = output_device
|
||||||
|
|
||||||
def setTargetHotendTemperature(self, printer: "PrinterOutputModel", position: int, temperature: Union[int, float]) -> None:
|
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) -> None:
|
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) -> None:
|
def setJobState(self, job: "PrintJobOutputModel", state: str) -> None:
|
||||||
|
@ -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 enum import IntEnum
|
||||||
|
from typing import Callable, List, Optional, Union
|
||||||
|
|
||||||
from UM.Decorators import deprecated
|
|
||||||
from UM.i18n import i18nCatalog
|
|
||||||
from UM.OutputDevice.OutputDevice import OutputDevice
|
|
||||||
from PyQt5.QtCore import pyqtProperty, pyqtSignal, QObject, QTimer, QUrl
|
from PyQt5.QtCore import pyqtProperty, pyqtSignal, QObject, QTimer, QUrl
|
||||||
from PyQt5.QtWidgets import QMessageBox
|
from PyQt5.QtWidgets import QMessageBox
|
||||||
|
|
||||||
@ -11,28 +10,35 @@ from UM.Logger import Logger
|
|||||||
from UM.Signal import signalemitter
|
from UM.Signal import signalemitter
|
||||||
from UM.Qt.QtApplication import QtApplication
|
from UM.Qt.QtApplication import QtApplication
|
||||||
from UM.FlameProfiler import pyqtSlot
|
from UM.FlameProfiler import pyqtSlot
|
||||||
|
from UM.Decorators import deprecated
|
||||||
from enum import IntEnum # For the connection state tracking.
|
from UM.i18n import i18nCatalog
|
||||||
from typing import Callable, List, Optional, Union
|
from UM.OutputDevice.OutputDevice import OutputDevice
|
||||||
|
|
||||||
MYPY = False
|
MYPY = False
|
||||||
if MYPY:
|
if MYPY:
|
||||||
from cura.PrinterOutput.PrinterOutputModel import PrinterOutputModel
|
|
||||||
from cura.PrinterOutput.ConfigurationModel import ConfigurationModel
|
|
||||||
from cura.PrinterOutput.FirmwareUpdater import FirmwareUpdater
|
|
||||||
from UM.FileHandler.FileHandler import FileHandler
|
from UM.FileHandler.FileHandler import FileHandler
|
||||||
from UM.Scene.SceneNode import SceneNode
|
from UM.Scene.SceneNode import SceneNode
|
||||||
|
from .Models.PrinterOutputModel import PrinterOutputModel
|
||||||
|
from .Models.PrinterConfigurationModel import PrinterConfigurationModel
|
||||||
|
from .FirmwareUpdater import FirmwareUpdater
|
||||||
|
|
||||||
i18n_catalog = i18nCatalog("cura")
|
i18n_catalog = i18nCatalog("cura")
|
||||||
|
|
||||||
|
|
||||||
## The current processing state of the backend.
|
## The current processing state of the backend.
|
||||||
class ConnectionState(IntEnum):
|
class ConnectionState(IntEnum):
|
||||||
closed = 0
|
Closed = 0
|
||||||
connecting = 1
|
Connecting = 1
|
||||||
connected = 2
|
Connected = 2
|
||||||
busy = 3
|
Busy = 3
|
||||||
error = 4
|
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.
|
## Printer output device adds extra interface options on top of output device.
|
||||||
@ -46,6 +52,7 @@ class ConnectionState(IntEnum):
|
|||||||
# For all other uses it should be used in the same way as a "regular" OutputDevice.
|
# For all other uses it should be used in the same way as a "regular" OutputDevice.
|
||||||
@signalemitter
|
@signalemitter
|
||||||
class PrinterOutputDevice(QObject, OutputDevice):
|
class PrinterOutputDevice(QObject, OutputDevice):
|
||||||
|
|
||||||
printersChanged = pyqtSignal()
|
printersChanged = pyqtSignal()
|
||||||
connectionStateChanged = pyqtSignal(str)
|
connectionStateChanged = pyqtSignal(str)
|
||||||
acceptsCommandsChanged = pyqtSignal()
|
acceptsCommandsChanged = pyqtSignal()
|
||||||
@ -62,33 +69,34 @@ class PrinterOutputDevice(QObject, OutputDevice):
|
|||||||
# Signal to indicate that the configuration of one of the printers has changed.
|
# Signal to indicate that the configuration of one of the printers has changed.
|
||||||
uniqueConfigurationsChanged = pyqtSignal()
|
uniqueConfigurationsChanged = pyqtSignal()
|
||||||
|
|
||||||
def __init__(self, device_id: str, parent: QObject = None) -> None:
|
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
|
super().__init__(device_id = device_id, parent = parent) # type: ignore # MyPy complains with the multiple inheritance
|
||||||
|
|
||||||
self._printers = [] # type: List[PrinterOutputModel]
|
self._printers = [] # type: List[PrinterOutputModel]
|
||||||
self._unique_configurations = [] # type: List[ConfigurationModel]
|
self._unique_configurations = [] # type: List[PrinterConfigurationModel]
|
||||||
|
|
||||||
self._monitor_view_qml_path = "" #type: str
|
self._monitor_view_qml_path = "" # type: str
|
||||||
self._monitor_component = None #type: Optional[QObject]
|
self._monitor_component = None # type: Optional[QObject]
|
||||||
self._monitor_item = None #type: Optional[QObject]
|
self._monitor_item = None # type: Optional[QObject]
|
||||||
|
|
||||||
self._control_view_qml_path = "" #type: str
|
self._control_view_qml_path = "" # type: str
|
||||||
self._control_component = None #type: Optional[QObject]
|
self._control_component = None # type: Optional[QObject]
|
||||||
self._control_item = None #type: Optional[QObject]
|
self._control_item = None # type: Optional[QObject]
|
||||||
|
|
||||||
self._accepts_commands = False #type: bool
|
self._accepts_commands = False # type: bool
|
||||||
|
|
||||||
self._update_timer = QTimer() #type: QTimer
|
self._update_timer = QTimer() # type: QTimer
|
||||||
self._update_timer.setInterval(2000) # TODO; Add preference for update interval
|
self._update_timer.setInterval(2000) # TODO; Add preference for update interval
|
||||||
self._update_timer.setSingleShot(False)
|
self._update_timer.setSingleShot(False)
|
||||||
self._update_timer.timeout.connect(self._update)
|
self._update_timer.timeout.connect(self._update)
|
||||||
|
|
||||||
self._connection_state = ConnectionState.closed #type: ConnectionState
|
self._connection_state = ConnectionState.Closed # type: ConnectionState
|
||||||
|
self._connection_type = connection_type # type: ConnectionType
|
||||||
|
|
||||||
self._firmware_updater = None #type: Optional[FirmwareUpdater]
|
self._firmware_updater = None # type: Optional[FirmwareUpdater]
|
||||||
self._firmware_name = None #type: Optional[str]
|
self._firmware_name = None # type: Optional[str]
|
||||||
self._address = "" #type: str
|
self._address = "" # type: str
|
||||||
self._connection_text = "" #type: str
|
self._connection_text = "" # type: str
|
||||||
self.printersChanged.connect(self._onPrintersChanged)
|
self.printersChanged.connect(self._onPrintersChanged)
|
||||||
QtApplication.getInstance().getOutputDeviceManager().outputDevicesChanged.connect(self._updateUniqueConfigurations)
|
QtApplication.getInstance().getOutputDeviceManager().outputDevicesChanged.connect(self._updateUniqueConfigurations)
|
||||||
|
|
||||||
@ -110,15 +118,19 @@ class PrinterOutputDevice(QObject, OutputDevice):
|
|||||||
callback(QMessageBox.Yes)
|
callback(QMessageBox.Yes)
|
||||||
|
|
||||||
def isConnected(self) -> bool:
|
def isConnected(self) -> bool:
|
||||||
return self._connection_state != ConnectionState.closed and self._connection_state != ConnectionState.error
|
return self._connection_state != ConnectionState.Closed and self._connection_state != ConnectionState.Error
|
||||||
|
|
||||||
def setConnectionState(self, connection_state: ConnectionState) -> None:
|
def setConnectionState(self, connection_state: "ConnectionState") -> None:
|
||||||
if self._connection_state != connection_state:
|
if self._connection_state != connection_state:
|
||||||
self._connection_state = connection_state
|
self._connection_state = connection_state
|
||||||
self.connectionStateChanged.emit(self._id)
|
self.connectionStateChanged.emit(self._id)
|
||||||
|
|
||||||
@pyqtProperty(str, notify = connectionStateChanged)
|
@pyqtProperty(int, constant = True)
|
||||||
def connectionState(self) -> ConnectionState:
|
def connectionType(self) -> "ConnectionType":
|
||||||
|
return self._connection_type
|
||||||
|
|
||||||
|
@pyqtProperty(int, notify = connectionStateChanged)
|
||||||
|
def connectionState(self) -> "ConnectionState":
|
||||||
return self._connection_state
|
return self._connection_state
|
||||||
|
|
||||||
def _update(self) -> None:
|
def _update(self) -> None:
|
||||||
@ -131,7 +143,8 @@ class PrinterOutputDevice(QObject, OutputDevice):
|
|||||||
|
|
||||||
return None
|
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:
|
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")
|
raise NotImplementedError("requestWrite needs to be implemented")
|
||||||
|
|
||||||
@pyqtProperty(QObject, notify = printersChanged)
|
@pyqtProperty(QObject, notify = printersChanged)
|
||||||
@ -174,13 +187,13 @@ class PrinterOutputDevice(QObject, OutputDevice):
|
|||||||
|
|
||||||
## Attempt to establish connection
|
## Attempt to establish connection
|
||||||
def connect(self) -> None:
|
def connect(self) -> None:
|
||||||
self.setConnectionState(ConnectionState.connecting)
|
self.setConnectionState(ConnectionState.Connecting)
|
||||||
self._update_timer.start()
|
self._update_timer.start()
|
||||||
|
|
||||||
## Attempt to close the connection
|
## Attempt to close the connection
|
||||||
def close(self) -> None:
|
def close(self) -> None:
|
||||||
self._update_timer.stop()
|
self._update_timer.stop()
|
||||||
self.setConnectionState(ConnectionState.closed)
|
self.setConnectionState(ConnectionState.Closed)
|
||||||
|
|
||||||
## Ensure that close gets called when object is destroyed
|
## Ensure that close gets called when object is destroyed
|
||||||
def __del__(self) -> None:
|
def __del__(self) -> None:
|
||||||
@ -203,14 +216,21 @@ class PrinterOutputDevice(QObject, OutputDevice):
|
|||||||
|
|
||||||
# Returns the unique configurations of the printers within this output device
|
# Returns the unique configurations of the printers within this output device
|
||||||
@pyqtProperty("QVariantList", notify = uniqueConfigurationsChanged)
|
@pyqtProperty("QVariantList", notify = uniqueConfigurationsChanged)
|
||||||
def uniqueConfigurations(self) -> List["ConfigurationModel"]:
|
def uniqueConfigurations(self) -> List["PrinterConfigurationModel"]:
|
||||||
return self._unique_configurations
|
return self._unique_configurations
|
||||||
|
|
||||||
def _updateUniqueConfigurations(self) -> None:
|
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 = sorted(
|
||||||
self._unique_configurations.sort(key = lambda k: k.printerType)
|
{printer.printerConfiguration for printer in self._printers if printer.printerConfiguration is not None},
|
||||||
|
key=lambda config: config.printerType,
|
||||||
|
)
|
||||||
self.uniqueConfigurationsChanged.emit()
|
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:
|
def _onPrintersChanged(self) -> None:
|
||||||
for printer in self._printers:
|
for printer in self._printers:
|
||||||
printer.configurationChanged.connect(self._updateUniqueConfigurations)
|
printer.configurationChanged.connect(self._updateUniqueConfigurations)
|
@ -60,13 +60,11 @@ 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)
|
|
||||||
|
|
||||||
self._onChanged()
|
self._onChanged()
|
||||||
|
|
||||||
@ -142,6 +140,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 +185,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:
|
||||||
points = numpy.append(points, child_hull.getPoints(), axis = 0)
|
try:
|
||||||
|
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 +240,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 +273,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 +287,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?")
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -112,21 +112,21 @@ 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)
|
|
||||||
position = self.getWorldPosition()
|
|
||||||
aabb = AxisAlignedBox(minimum = position, maximum = position)
|
|
||||||
|
|
||||||
for child in self._children:
|
for child in self._children:
|
||||||
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._mesh_data:
|
||||||
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")
|
||||||
@ -161,20 +161,20 @@ 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()
|
plugin_registry = PluginRegistry.getInstance()
|
||||||
extension = file_name.split(".")[-1]
|
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):
|
||||||
@ -183,7 +183,7 @@ class CuraContainerRegistry(ContainerRegistry):
|
|||||||
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 +221,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,18 +267,19 @@ 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)
|
||||||
new_instance = SettingInstance(setting_definition, profile)
|
if setting_definition is not None:
|
||||||
new_instance.setProperty("value", setting_value)
|
new_instance = SettingInstance(setting_definition, profile)
|
||||||
new_instance.resetState() # Ensure that the state is not seen as a user state.
|
new_instance.setProperty("value", setting_value)
|
||||||
profile.addInstance(new_instance)
|
new_instance.resetState() # Ensure that the state is not seen as a user state.
|
||||||
profile.setDirty(True)
|
profile.addInstance(new_instance)
|
||||||
|
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)
|
||||||
@ -290,7 +291,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 +303,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 +327,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 +403,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 +514,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 +576,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 +594,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 +635,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 +649,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 +683,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 +727,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":
|
||||||
|
@ -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
|
||||||
extruder_stack = global_stack.extruders[str(extruder_position)]
|
try:
|
||||||
|
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,7 +125,12 @@ 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)]
|
||||||
extruder_definition = registry.findDefinitionContainers(id = extruder_definition_id)[0]
|
try:
|
||||||
|
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
|
||||||
|
@ -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,8 +83,9 @@ 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:
|
||||||
self._active_extruder_index = index
|
if self._active_extruder_index != index:
|
||||||
self.activeExtruderChanged.emit()
|
self._active_extruder_index = index
|
||||||
|
self.activeExtruderChanged.emit()
|
||||||
|
|
||||||
@pyqtProperty(int, notify = activeExtruderChanged)
|
@pyqtProperty(int, notify = activeExtruderChanged)
|
||||||
def activeExtruderIndex(self) -> int:
|
def activeExtruderIndex(self) -> int:
|
||||||
@ -144,7 +145,7 @@ class ExtruderManager(QObject):
|
|||||||
|
|
||||||
@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"]:
|
||||||
@ -223,7 +224,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,7 +273,9 @@ 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
|
||||||
@ -300,12 +312,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 +347,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")
|
||||||
|
@ -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()
|
||||||
|
|
||||||
|
@ -3,8 +3,8 @@
|
|||||||
|
|
||||||
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, pyqtSlot
|
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
|
||||||
@ -42,17 +42,75 @@ 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"))
|
||||||
|
|
||||||
@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)
|
||||||
@ -87,6 +145,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
|
||||||
@ -153,7 +212,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
|
||||||
|
|
||||||
@ -199,6 +258,9 @@ class GlobalStack(CuraContainerStack):
|
|||||||
def getHasVariants(self) -> bool:
|
def getHasVariants(self) -> bool:
|
||||||
return parseBool(self.getMetaDataEntry("has_variants", False))
|
return parseBool(self.getMetaDataEntry("has_variants", False))
|
||||||
|
|
||||||
|
def getHasVariantsBuildPlates(self) -> bool:
|
||||||
|
return parseBool(self.getMetaDataEntry("has_variant_buildplates", False))
|
||||||
|
|
||||||
def getHasMachineQuality(self) -> bool:
|
def getHasMachineQuality(self) -> bool:
|
||||||
return parseBool(self.getMetaDataEntry("has_machine_quality", False))
|
return parseBool(self.getMetaDataEntry("has_machine_quality", False))
|
||||||
|
|
||||||
|
@ -1,17 +1,19 @@
|
|||||||
# 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.
|
||||||
|
|
||||||
import collections
|
|
||||||
import time
|
import time
|
||||||
from typing import Any, Callable, List, Dict, TYPE_CHECKING, Optional, cast
|
import re
|
||||||
|
import unicodedata
|
||||||
|
from typing import Any, List, Dict, TYPE_CHECKING, Optional, cast
|
||||||
|
|
||||||
|
from PyQt5.QtCore import QObject, pyqtProperty, pyqtSignal, QTimer
|
||||||
|
|
||||||
from UM.ConfigurationErrorMessage import ConfigurationErrorMessage
|
from UM.ConfigurationErrorMessage import ConfigurationErrorMessage
|
||||||
|
from UM.Decorators import deprecated
|
||||||
from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
|
from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
|
||||||
from UM.Settings.InstanceContainer import InstanceContainer
|
from UM.Settings.InstanceContainer import InstanceContainer
|
||||||
from UM.Settings.Interfaces import ContainerInterface
|
from UM.Settings.Interfaces import ContainerInterface
|
||||||
from UM.Signal import Signal
|
from UM.Signal import Signal
|
||||||
|
|
||||||
from PyQt5.QtCore import QObject, pyqtProperty, pyqtSignal, QTimer
|
|
||||||
from UM.FlameProfiler import pyqtSlot
|
from UM.FlameProfiler import pyqtSlot
|
||||||
from UM import Util
|
from UM import Util
|
||||||
from UM.Logger import Logger
|
from UM.Logger import Logger
|
||||||
@ -21,10 +23,10 @@ from UM.Settings.SettingFunction import SettingFunction
|
|||||||
from UM.Signal import postponeSignals, CompressTechnique
|
from UM.Signal import postponeSignals, CompressTechnique
|
||||||
|
|
||||||
from cura.Machines.QualityManager import getMachineDefinitionIDForQualitySearch
|
from cura.Machines.QualityManager import getMachineDefinitionIDForQualitySearch
|
||||||
from cura.PrinterOutputDevice import PrinterOutputDevice
|
from cura.PrinterOutput.PrinterOutputDevice import PrinterOutputDevice, ConnectionType
|
||||||
from cura.PrinterOutput.ConfigurationModel import ConfigurationModel
|
from cura.PrinterOutput.Models.PrinterConfigurationModel import PrinterConfigurationModel
|
||||||
from cura.PrinterOutput.ExtruderConfigurationModel import ExtruderConfigurationModel
|
from cura.PrinterOutput.Models.ExtruderConfigurationModel import ExtruderConfigurationModel
|
||||||
from cura.PrinterOutput.MaterialOutputModel import MaterialOutputModel
|
from cura.PrinterOutput.Models.MaterialOutputModel import MaterialOutputModel
|
||||||
from cura.Settings.CuraContainerRegistry import CuraContainerRegistry
|
from cura.Settings.CuraContainerRegistry import CuraContainerRegistry
|
||||||
from cura.Settings.ExtruderManager import ExtruderManager
|
from cura.Settings.ExtruderManager import ExtruderManager
|
||||||
from cura.Settings.ExtruderStack import ExtruderStack
|
from cura.Settings.ExtruderStack import ExtruderStack
|
||||||
@ -62,9 +64,7 @@ class MachineManager(QObject):
|
|||||||
|
|
||||||
self._default_extruder_position = "0" # to be updated when extruders are switched on and off
|
self._default_extruder_position = "0" # to be updated when extruders are switched on and off
|
||||||
|
|
||||||
self.machine_extruder_material_update_dict = collections.defaultdict(list) #type: Dict[str, List[Callable[[], None]]]
|
self._instance_container_timer = QTimer() # type: QTimer
|
||||||
|
|
||||||
self._instance_container_timer = QTimer() #type: QTimer
|
|
||||||
self._instance_container_timer.setInterval(250)
|
self._instance_container_timer.setInterval(250)
|
||||||
self._instance_container_timer.setSingleShot(True)
|
self._instance_container_timer.setSingleShot(True)
|
||||||
self._instance_container_timer.timeout.connect(self.__emitChangedSignals)
|
self._instance_container_timer.timeout.connect(self.__emitChangedSignals)
|
||||||
@ -74,7 +74,7 @@ class MachineManager(QObject):
|
|||||||
self._application.globalContainerStackChanged.connect(self._onGlobalContainerChanged)
|
self._application.globalContainerStackChanged.connect(self._onGlobalContainerChanged)
|
||||||
self._container_registry.containerLoadComplete.connect(self._onContainersChanged)
|
self._container_registry.containerLoadComplete.connect(self._onContainersChanged)
|
||||||
|
|
||||||
## When the global container is changed, active material probably needs to be updated.
|
# When the global container is changed, active material probably needs to be updated.
|
||||||
self.globalContainerChanged.connect(self.activeMaterialChanged)
|
self.globalContainerChanged.connect(self.activeMaterialChanged)
|
||||||
self.globalContainerChanged.connect(self.activeVariantChanged)
|
self.globalContainerChanged.connect(self.activeVariantChanged)
|
||||||
self.globalContainerChanged.connect(self.activeQualityChanged)
|
self.globalContainerChanged.connect(self.activeQualityChanged)
|
||||||
@ -86,12 +86,14 @@ class MachineManager(QObject):
|
|||||||
|
|
||||||
self._onGlobalContainerChanged()
|
self._onGlobalContainerChanged()
|
||||||
|
|
||||||
ExtruderManager.getInstance().activeExtruderChanged.connect(self._onActiveExtruderStackChanged)
|
extruder_manager = self._application.getExtruderManager()
|
||||||
|
|
||||||
|
extruder_manager.activeExtruderChanged.connect(self._onActiveExtruderStackChanged)
|
||||||
self._onActiveExtruderStackChanged()
|
self._onActiveExtruderStackChanged()
|
||||||
|
|
||||||
ExtruderManager.getInstance().activeExtruderChanged.connect(self.activeMaterialChanged)
|
extruder_manager.activeExtruderChanged.connect(self.activeMaterialChanged)
|
||||||
ExtruderManager.getInstance().activeExtruderChanged.connect(self.activeVariantChanged)
|
extruder_manager.activeExtruderChanged.connect(self.activeVariantChanged)
|
||||||
ExtruderManager.getInstance().activeExtruderChanged.connect(self.activeQualityChanged)
|
extruder_manager.activeExtruderChanged.connect(self.activeQualityChanged)
|
||||||
|
|
||||||
self.globalContainerChanged.connect(self.activeStackChanged)
|
self.globalContainerChanged.connect(self.activeStackChanged)
|
||||||
self.globalValueChanged.connect(self.activeStackValueChanged)
|
self.globalValueChanged.connect(self.activeStackValueChanged)
|
||||||
@ -105,7 +107,7 @@ class MachineManager(QObject):
|
|||||||
# There might already be some output devices by the time the signal is connected
|
# There might already be some output devices by the time the signal is connected
|
||||||
self._onOutputDevicesChanged()
|
self._onOutputDevicesChanged()
|
||||||
|
|
||||||
self._current_printer_configuration = ConfigurationModel() # Indicates the current configuration setup in this printer
|
self._current_printer_configuration = PrinterConfigurationModel() # Indicates the current configuration setup in this printer
|
||||||
self.activeMaterialChanged.connect(self._onCurrentConfigurationChanged)
|
self.activeMaterialChanged.connect(self._onCurrentConfigurationChanged)
|
||||||
self.activeVariantChanged.connect(self._onCurrentConfigurationChanged)
|
self.activeVariantChanged.connect(self._onCurrentConfigurationChanged)
|
||||||
# Force to compute the current configuration
|
# Force to compute the current configuration
|
||||||
@ -113,17 +115,13 @@ class MachineManager(QObject):
|
|||||||
|
|
||||||
self._application.callLater(self.setInitialActiveMachine)
|
self._application.callLater(self.setInitialActiveMachine)
|
||||||
|
|
||||||
self._material_incompatible_message = Message(catalog.i18nc("@info:status",
|
containers = CuraContainerRegistry.getInstance().findInstanceContainers(id = self.activeMaterialId) # type: List[InstanceContainer]
|
||||||
"The selected material is incompatible with the selected machine or configuration."),
|
|
||||||
title = catalog.i18nc("@info:title", "Incompatible Material")) #type: Message
|
|
||||||
|
|
||||||
containers = CuraContainerRegistry.getInstance().findInstanceContainers(id = self.activeMaterialId) #type: List[InstanceContainer]
|
|
||||||
if containers:
|
if containers:
|
||||||
containers[0].nameChanged.connect(self._onMaterialNameChanged)
|
containers[0].nameChanged.connect(self._onMaterialNameChanged)
|
||||||
|
|
||||||
self._material_manager = self._application.getMaterialManager() #type: MaterialManager
|
self._material_manager = self._application.getMaterialManager() # type: MaterialManager
|
||||||
self._variant_manager = self._application.getVariantManager() #type: VariantManager
|
self._variant_manager = self._application.getVariantManager() # type: VariantManager
|
||||||
self._quality_manager = self._application.getQualityManager() #type: QualityManager
|
self._quality_manager = self._application.getQualityManager() # type: QualityManager
|
||||||
|
|
||||||
# When the materials lookup table gets updated, it can mean that a material has its name changed, which should
|
# When the materials lookup table gets updated, it can mean that a material has its name changed, which should
|
||||||
# be reflected on the GUI. This signal emission makes sure that it happens.
|
# be reflected on the GUI. This signal emission makes sure that it happens.
|
||||||
@ -156,10 +154,11 @@ class MachineManager(QObject):
|
|||||||
blurSettings = pyqtSignal() # Emitted to force fields in the advanced sidebar to un-focus, so they update properly
|
blurSettings = pyqtSignal() # Emitted to force fields in the advanced sidebar to un-focus, so they update properly
|
||||||
|
|
||||||
outputDevicesChanged = pyqtSignal()
|
outputDevicesChanged = pyqtSignal()
|
||||||
currentConfigurationChanged = pyqtSignal() # Emitted every time the current configurations of the machine changes
|
currentConfigurationChanged = pyqtSignal() # Emitted every time the current configurations of the machine changes
|
||||||
printerConnectedStatusChanged = pyqtSignal() # Emitted every time the active machine change or the outputdevices change
|
printerConnectedStatusChanged = pyqtSignal() # Emitted every time the active machine change or the outputdevices change
|
||||||
|
|
||||||
rootMaterialChanged = pyqtSignal()
|
rootMaterialChanged = pyqtSignal()
|
||||||
|
discoveredPrintersChanged = pyqtSignal()
|
||||||
|
|
||||||
def setInitialActiveMachine(self) -> None:
|
def setInitialActiveMachine(self) -> None:
|
||||||
active_machine_id = self._application.getPreferences().getValue("cura/active_machine")
|
active_machine_id = self._application.getPreferences().getValue("cura/active_machine")
|
||||||
@ -176,7 +175,7 @@ class MachineManager(QObject):
|
|||||||
self.outputDevicesChanged.emit()
|
self.outputDevicesChanged.emit()
|
||||||
|
|
||||||
@pyqtProperty(QObject, notify = currentConfigurationChanged)
|
@pyqtProperty(QObject, notify = currentConfigurationChanged)
|
||||||
def currentConfiguration(self) -> ConfigurationModel:
|
def currentConfiguration(self) -> PrinterConfigurationModel:
|
||||||
return self._current_printer_configuration
|
return self._current_printer_configuration
|
||||||
|
|
||||||
def _onCurrentConfigurationChanged(self) -> None:
|
def _onCurrentConfigurationChanged(self) -> None:
|
||||||
@ -201,13 +200,13 @@ class MachineManager(QObject):
|
|||||||
extruder_configuration.hotendID = extruder.variant.getName() if extruder.variant != empty_variant_container else None
|
extruder_configuration.hotendID = extruder.variant.getName() if extruder.variant != empty_variant_container else None
|
||||||
self._current_printer_configuration.extruderConfigurations.append(extruder_configuration)
|
self._current_printer_configuration.extruderConfigurations.append(extruder_configuration)
|
||||||
|
|
||||||
# an empty build plate configuration from the network printer is presented as an empty string, so use "" for an
|
# An empty build plate configuration from the network printer is presented as an empty string, so use "" for an
|
||||||
# empty build plate.
|
# empty build plate.
|
||||||
self._current_printer_configuration.buildplateConfiguration = self._global_container_stack.getProperty("machine_buildplate_type", "value") if self._global_container_stack.variant != empty_variant_container else ""
|
self._current_printer_configuration.buildplateConfiguration = self._global_container_stack.getProperty("machine_buildplate_type", "value") if self._global_container_stack.variant != empty_variant_container else ""
|
||||||
self.currentConfigurationChanged.emit()
|
self.currentConfigurationChanged.emit()
|
||||||
|
|
||||||
@pyqtSlot(QObject, result = bool)
|
@pyqtSlot(QObject, result = bool)
|
||||||
def matchesConfiguration(self, configuration: ConfigurationModel) -> bool:
|
def matchesConfiguration(self, configuration: PrinterConfigurationModel) -> bool:
|
||||||
return self._current_printer_configuration == configuration
|
return self._current_printer_configuration == configuration
|
||||||
|
|
||||||
@pyqtProperty("QVariantList", notify = outputDevicesChanged)
|
@pyqtProperty("QVariantList", notify = outputDevicesChanged)
|
||||||
@ -247,7 +246,7 @@ class MachineManager(QObject):
|
|||||||
self.updateNumberExtrudersEnabled()
|
self.updateNumberExtrudersEnabled()
|
||||||
self.globalContainerChanged.emit()
|
self.globalContainerChanged.emit()
|
||||||
|
|
||||||
# after switching the global stack we reconnect all the signals and set the variant and material references
|
# After switching the global stack we reconnect all the signals and set the variant and material references
|
||||||
if self._global_container_stack:
|
if self._global_container_stack:
|
||||||
self._application.getPreferences().setValue("cura/active_machine", self._global_container_stack.getId())
|
self._application.getPreferences().setValue("cura/active_machine", self._global_container_stack.getId())
|
||||||
|
|
||||||
@ -261,7 +260,7 @@ class MachineManager(QObject):
|
|||||||
if global_variant.getMetaDataEntry("hardware_type") != "buildplate":
|
if global_variant.getMetaDataEntry("hardware_type") != "buildplate":
|
||||||
self._global_container_stack.setVariant(empty_variant_container)
|
self._global_container_stack.setVariant(empty_variant_container)
|
||||||
|
|
||||||
# set the global material to empty as we now use the extruder stack at all times - CURA-4482
|
# Set the global material to empty as we now use the extruder stack at all times - CURA-4482
|
||||||
global_material = self._global_container_stack.material
|
global_material = self._global_container_stack.material
|
||||||
if global_material != empty_material_container:
|
if global_material != empty_material_container:
|
||||||
self._global_container_stack.setMaterial(empty_material_container)
|
self._global_container_stack.setMaterial(empty_material_container)
|
||||||
@ -271,30 +270,19 @@ class MachineManager(QObject):
|
|||||||
extruder_stack.propertyChanged.connect(self._onPropertyChanged)
|
extruder_stack.propertyChanged.connect(self._onPropertyChanged)
|
||||||
extruder_stack.containersChanged.connect(self._onContainersChanged)
|
extruder_stack.containersChanged.connect(self._onContainersChanged)
|
||||||
|
|
||||||
if self._global_container_stack.getId() in self.machine_extruder_material_update_dict:
|
|
||||||
for func in self.machine_extruder_material_update_dict[self._global_container_stack.getId()]:
|
|
||||||
self._application.callLater(func)
|
|
||||||
del self.machine_extruder_material_update_dict[self._global_container_stack.getId()]
|
|
||||||
|
|
||||||
self.activeQualityGroupChanged.emit()
|
self.activeQualityGroupChanged.emit()
|
||||||
|
|
||||||
def _onActiveExtruderStackChanged(self) -> None:
|
def _onActiveExtruderStackChanged(self) -> None:
|
||||||
self.blurSettings.emit() # Ensure no-one has focus.
|
self.blurSettings.emit() # Ensure no-one has focus.
|
||||||
old_active_container_stack = self._active_container_stack
|
|
||||||
|
|
||||||
self._active_container_stack = ExtruderManager.getInstance().getActiveExtruderStack()
|
self._active_container_stack = ExtruderManager.getInstance().getActiveExtruderStack()
|
||||||
|
|
||||||
if old_active_container_stack != self._active_container_stack:
|
|
||||||
# Many methods and properties related to the active quality actually depend
|
|
||||||
# on _active_container_stack. If it changes, then the properties change.
|
|
||||||
self.activeQualityChanged.emit()
|
|
||||||
|
|
||||||
def __emitChangedSignals(self) -> None:
|
def __emitChangedSignals(self) -> None:
|
||||||
self.activeQualityChanged.emit()
|
self.activeQualityChanged.emit()
|
||||||
self.activeVariantChanged.emit()
|
self.activeVariantChanged.emit()
|
||||||
self.activeMaterialChanged.emit()
|
self.activeMaterialChanged.emit()
|
||||||
|
|
||||||
self.rootMaterialChanged.emit()
|
self.rootMaterialChanged.emit()
|
||||||
|
self.numberExtrudersEnabledChanged.emit()
|
||||||
|
|
||||||
def _onContainersChanged(self, container: ContainerInterface) -> None:
|
def _onContainersChanged(self, container: ContainerInterface) -> None:
|
||||||
self._instance_container_timer.start()
|
self._instance_container_timer.start()
|
||||||
@ -370,18 +358,23 @@ class MachineManager(QObject):
|
|||||||
# Make sure that the default machine actions for this machine have been added
|
# Make sure that the default machine actions for this machine have been added
|
||||||
self._application.getMachineActionManager().addDefaultMachineActions(global_stack)
|
self._application.getMachineActionManager().addDefaultMachineActions(global_stack)
|
||||||
|
|
||||||
ExtruderManager.getInstance()._fixSingleExtrusionMachineExtruderDefinition(global_stack)
|
ExtruderManager.getInstance().fixSingleExtrusionMachineExtruderDefinition(global_stack)
|
||||||
if not global_stack.isValid():
|
if not global_stack.isValid():
|
||||||
# Mark global stack as invalid
|
# Mark global stack as invalid
|
||||||
ConfigurationErrorMessage.getInstance().addFaultyContainers(global_stack.getId())
|
ConfigurationErrorMessage.getInstance().addFaultyContainers(global_stack.getId())
|
||||||
return # We're done here
|
return # We're done here
|
||||||
ExtruderManager.getInstance().setActiveExtruderIndex(0) # Switch to first extruder
|
|
||||||
self._global_container_stack = global_stack
|
self._global_container_stack = global_stack
|
||||||
self._application.setGlobalContainerStack(global_stack)
|
self._application.setGlobalContainerStack(global_stack)
|
||||||
ExtruderManager.getInstance()._globalContainerStackChanged()
|
ExtruderManager.getInstance()._globalContainerStackChanged()
|
||||||
self._initMachineState(global_stack)
|
self._initMachineState(global_stack)
|
||||||
self._onGlobalContainerChanged()
|
self._onGlobalContainerChanged()
|
||||||
|
|
||||||
|
# Switch to the first enabled extruder
|
||||||
|
self.updateDefaultExtruder()
|
||||||
|
default_extruder_position = int(self.defaultExtruderPosition)
|
||||||
|
ExtruderManager.getInstance().setActiveExtruderIndex(default_extruder_position)
|
||||||
|
|
||||||
self.__emitChangedSignals()
|
self.__emitChangedSignals()
|
||||||
|
|
||||||
## Given a definition id, return the machine with this id.
|
## Given a definition id, return the machine with this id.
|
||||||
@ -398,9 +391,17 @@ class MachineManager(QObject):
|
|||||||
return machine
|
return machine
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
@pyqtSlot(str)
|
||||||
@pyqtSlot(str, str)
|
@pyqtSlot(str, str)
|
||||||
def addMachine(self, name: str, definition_id: str) -> None:
|
def addMachine(self, definition_id: str, name: Optional[str] = None) -> None:
|
||||||
new_stack = CuraStackBuilder.createMachine(name, definition_id)
|
if name is None:
|
||||||
|
definitions = CuraContainerRegistry.getInstance().findDefinitionContainers(id = definition_id)
|
||||||
|
if definitions:
|
||||||
|
name = definitions[0].getName()
|
||||||
|
else:
|
||||||
|
name = definition_id
|
||||||
|
|
||||||
|
new_stack = CuraStackBuilder.createMachine(cast(str, name), definition_id)
|
||||||
if new_stack:
|
if new_stack:
|
||||||
# Instead of setting the global container stack here, we set the active machine and so the signals are emitted
|
# Instead of setting the global container stack here, we set the active machine and so the signals are emitted
|
||||||
self.setActiveMachine(new_stack.getId())
|
self.setActiveMachine(new_stack.getId())
|
||||||
@ -419,7 +420,7 @@ class MachineManager(QObject):
|
|||||||
# Not a very pretty solution, but the extruder manager doesn't really know how many extruders there are
|
# Not a very pretty solution, but the extruder manager doesn't really know how many extruders there are
|
||||||
machine_extruder_count = self._global_container_stack.getProperty("machine_extruder_count", "value")
|
machine_extruder_count = self._global_container_stack.getProperty("machine_extruder_count", "value")
|
||||||
extruder_stacks = ExtruderManager.getInstance().getActiveExtruderStacks()
|
extruder_stacks = ExtruderManager.getInstance().getActiveExtruderStacks()
|
||||||
count = 1 # we start with the global stack
|
count = 1 # We start with the global stack
|
||||||
for stack in extruder_stacks:
|
for stack in extruder_stacks:
|
||||||
md = stack.getMetaData()
|
md = stack.getMetaData()
|
||||||
if "position" in md and int(md["position"]) >= machine_extruder_count:
|
if "position" in md and int(md["position"]) >= machine_extruder_count:
|
||||||
@ -438,12 +439,12 @@ class MachineManager(QObject):
|
|||||||
if not self._global_container_stack:
|
if not self._global_container_stack:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if self._global_container_stack.getTop().findInstances():
|
if self._global_container_stack.getTop().getNumInstances() != 0:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
stacks = ExtruderManager.getInstance().getActiveExtruderStacks()
|
stacks = ExtruderManager.getInstance().getActiveExtruderStacks()
|
||||||
for stack in stacks:
|
for stack in stacks:
|
||||||
if stack.getTop().findInstances():
|
if stack.getTop().getNumInstances() != 0:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
return False
|
return False
|
||||||
@ -453,10 +454,10 @@ class MachineManager(QObject):
|
|||||||
if not self._global_container_stack:
|
if not self._global_container_stack:
|
||||||
return 0
|
return 0
|
||||||
num_user_settings = 0
|
num_user_settings = 0
|
||||||
num_user_settings += len(self._global_container_stack.getTop().findInstances())
|
num_user_settings += self._global_container_stack.getTop().getNumInstances()
|
||||||
stacks = ExtruderManager.getInstance().getActiveExtruderStacks()
|
stacks = self._global_container_stack.extruderList
|
||||||
for stack in stacks:
|
for stack in stacks:
|
||||||
num_user_settings += len(stack.getTop().findInstances())
|
num_user_settings += stack.getTop().getNumInstances()
|
||||||
return num_user_settings
|
return num_user_settings
|
||||||
|
|
||||||
## Delete a user setting from the global stack and all extruder stacks.
|
## Delete a user setting from the global stack and all extruder stacks.
|
||||||
@ -498,28 +499,78 @@ class MachineManager(QObject):
|
|||||||
return bool(self._stacks_have_errors)
|
return bool(self._stacks_have_errors)
|
||||||
|
|
||||||
@pyqtProperty(str, notify = globalContainerChanged)
|
@pyqtProperty(str, notify = globalContainerChanged)
|
||||||
|
@deprecated("use Cura.MachineManager.activeMachine.definition.name instead", "4.1")
|
||||||
def activeMachineDefinitionName(self) -> str:
|
def activeMachineDefinitionName(self) -> str:
|
||||||
if self._global_container_stack:
|
if self._global_container_stack:
|
||||||
return self._global_container_stack.definition.getName()
|
return self._global_container_stack.definition.getName()
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
@pyqtProperty(str, notify = globalContainerChanged)
|
@pyqtProperty(str, notify = globalContainerChanged)
|
||||||
|
@deprecated("use Cura.MachineManager.activeMachine.name instead", "4.1")
|
||||||
def activeMachineName(self) -> str:
|
def activeMachineName(self) -> str:
|
||||||
if self._global_container_stack:
|
if self._global_container_stack:
|
||||||
return self._global_container_stack.getName()
|
return self._global_container_stack.getMetaDataEntry("group_name", self._global_container_stack.getName())
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
@pyqtProperty(str, notify = globalContainerChanged)
|
@pyqtProperty(str, notify = globalContainerChanged)
|
||||||
|
@deprecated("use Cura.MachineManager.activeMachine.id instead", "4.1")
|
||||||
def activeMachineId(self) -> str:
|
def activeMachineId(self) -> str:
|
||||||
if self._global_container_stack:
|
if self._global_container_stack:
|
||||||
return self._global_container_stack.getId()
|
return self._global_container_stack.getId()
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
@pyqtProperty(str, notify = globalContainerChanged)
|
||||||
|
def activeMachineFirmwareVersion(self) -> str:
|
||||||
|
if not self._printer_output_devices:
|
||||||
|
return ""
|
||||||
|
return self._printer_output_devices[0].firmwareVersion
|
||||||
|
|
||||||
|
@pyqtProperty(str, notify = globalContainerChanged)
|
||||||
|
def activeMachineAddress(self) -> str:
|
||||||
|
if not self._printer_output_devices:
|
||||||
|
return ""
|
||||||
|
return self._printer_output_devices[0].address
|
||||||
|
|
||||||
@pyqtProperty(bool, notify = printerConnectedStatusChanged)
|
@pyqtProperty(bool, notify = printerConnectedStatusChanged)
|
||||||
def printerConnected(self):
|
def printerConnected(self) -> bool:
|
||||||
return bool(self._printer_output_devices)
|
return bool(self._printer_output_devices)
|
||||||
|
|
||||||
@pyqtProperty(str, notify = printerConnectedStatusChanged)
|
@pyqtProperty(bool, notify = printerConnectedStatusChanged)
|
||||||
|
def activeMachineHasRemoteConnection(self) -> bool:
|
||||||
|
if self._global_container_stack:
|
||||||
|
has_remote_connection = False
|
||||||
|
|
||||||
|
for connection_type in self._global_container_stack.configuredConnectionTypes:
|
||||||
|
has_remote_connection |= connection_type in [ConnectionType.NetworkConnection.value,
|
||||||
|
ConnectionType.CloudConnection.value]
|
||||||
|
return has_remote_connection
|
||||||
|
return False
|
||||||
|
|
||||||
|
@pyqtProperty("QVariantList", notify=globalContainerChanged)
|
||||||
|
@deprecated("use Cura.MachineManager.activeMachine.configuredConnectionTypes instead", "4.1")
|
||||||
|
def activeMachineConfiguredConnectionTypes(self):
|
||||||
|
if self._global_container_stack:
|
||||||
|
return self._global_container_stack.configuredConnectionTypes
|
||||||
|
return []
|
||||||
|
|
||||||
|
@pyqtProperty(bool, notify = printerConnectedStatusChanged)
|
||||||
|
def activeMachineIsGroup(self) -> bool:
|
||||||
|
return bool(self._printer_output_devices) and len(self._printer_output_devices[0].printers) > 1
|
||||||
|
|
||||||
|
@pyqtProperty(bool, notify = printerConnectedStatusChanged)
|
||||||
|
def activeMachineHasNetworkConnection(self) -> bool:
|
||||||
|
# A network connection is only available if any output device is actually a network connected device.
|
||||||
|
return any(d.connectionType == ConnectionType.NetworkConnection for d in self._printer_output_devices)
|
||||||
|
|
||||||
|
@pyqtProperty(bool, notify = printerConnectedStatusChanged)
|
||||||
|
def activeMachineHasCloudConnection(self) -> bool:
|
||||||
|
# A cloud connection is only available if any output device actually is a cloud connected device.
|
||||||
|
return any(d.connectionType == ConnectionType.CloudConnection for d in self._printer_output_devices)
|
||||||
|
|
||||||
|
@pyqtProperty(bool, notify = printerConnectedStatusChanged)
|
||||||
|
def activeMachineIsUsingCloudConnection(self) -> bool:
|
||||||
|
return self.activeMachineHasCloudConnection and not self.activeMachineHasNetworkConnection
|
||||||
|
|
||||||
def activeMachineNetworkKey(self) -> str:
|
def activeMachineNetworkKey(self) -> str:
|
||||||
if self._global_container_stack:
|
if self._global_container_stack:
|
||||||
return self._global_container_stack.getMetaDataEntry("um_network_key", "")
|
return self._global_container_stack.getMetaDataEntry("um_network_key", "")
|
||||||
@ -528,7 +579,7 @@ class MachineManager(QObject):
|
|||||||
@pyqtProperty(str, notify = printerConnectedStatusChanged)
|
@pyqtProperty(str, notify = printerConnectedStatusChanged)
|
||||||
def activeMachineNetworkGroupName(self) -> str:
|
def activeMachineNetworkGroupName(self) -> str:
|
||||||
if self._global_container_stack:
|
if self._global_container_stack:
|
||||||
return self._global_container_stack.getMetaDataEntry("connect_group_name", "")
|
return self._global_container_stack.getMetaDataEntry("group_name", "")
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
@pyqtProperty(QObject, notify = globalContainerChanged)
|
@pyqtProperty(QObject, notify = globalContainerChanged)
|
||||||
@ -616,6 +667,14 @@ class MachineManager(QObject):
|
|||||||
is_supported = self._current_quality_group.is_available
|
is_supported = self._current_quality_group.is_available
|
||||||
return is_supported
|
return is_supported
|
||||||
|
|
||||||
|
@pyqtProperty(bool, notify = activeQualityGroupChanged)
|
||||||
|
def isActiveQualityExperimental(self) -> bool:
|
||||||
|
is_experimental = False
|
||||||
|
if self._global_container_stack:
|
||||||
|
if self._current_quality_group:
|
||||||
|
is_experimental = self._current_quality_group.is_experimental
|
||||||
|
return is_experimental
|
||||||
|
|
||||||
## Returns whether there is anything unsupported in the current set-up.
|
## Returns whether there is anything unsupported in the current set-up.
|
||||||
#
|
#
|
||||||
# The current set-up signifies the global stack and all extruder stacks,
|
# The current set-up signifies the global stack and all extruder stacks,
|
||||||
@ -633,11 +692,6 @@ class MachineManager(QObject):
|
|||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
## Check if a container is read_only
|
|
||||||
@pyqtSlot(str, result = bool)
|
|
||||||
def isReadOnly(self, container_id: str) -> bool:
|
|
||||||
return CuraContainerRegistry.getInstance().isReadOnly(container_id)
|
|
||||||
|
|
||||||
## Copy the value of the setting of the current extruder to all other extruders as well as the global container.
|
## Copy the value of the setting of the current extruder to all other extruders as well as the global container.
|
||||||
@pyqtSlot(str)
|
@pyqtSlot(str)
|
||||||
def copyValueToExtruders(self, key: str) -> None:
|
def copyValueToExtruders(self, key: str) -> None:
|
||||||
@ -646,7 +700,7 @@ class MachineManager(QObject):
|
|||||||
new_value = self._active_container_stack.getProperty(key, "value")
|
new_value = self._active_container_stack.getProperty(key, "value")
|
||||||
extruder_stacks = [stack for stack in ExtruderManager.getInstance().getActiveExtruderStacks()]
|
extruder_stacks = [stack for stack in ExtruderManager.getInstance().getActiveExtruderStacks()]
|
||||||
|
|
||||||
# check in which stack the value has to be replaced
|
# Check in which stack the value has to be replaced
|
||||||
for extruder_stack in extruder_stacks:
|
for extruder_stack in extruder_stacks:
|
||||||
if extruder_stack != self._active_container_stack and extruder_stack.getProperty(key, "value") != new_value:
|
if extruder_stack != self._active_container_stack and extruder_stack.getProperty(key, "value") != new_value:
|
||||||
extruder_stack.userChanges.setProperty(key, "value", new_value) # TODO: nested property access, should be improved
|
extruder_stack.userChanges.setProperty(key, "value", new_value) # TODO: nested property access, should be improved
|
||||||
@ -662,10 +716,11 @@ class MachineManager(QObject):
|
|||||||
for key in self._active_container_stack.userChanges.getAllKeys():
|
for key in self._active_container_stack.userChanges.getAllKeys():
|
||||||
new_value = self._active_container_stack.getProperty(key, "value")
|
new_value = self._active_container_stack.getProperty(key, "value")
|
||||||
|
|
||||||
# check if the value has to be replaced
|
# Check if the value has to be replaced
|
||||||
extruder_stack.userChanges.setProperty(key, "value", new_value)
|
extruder_stack.userChanges.setProperty(key, "value", new_value)
|
||||||
|
|
||||||
@pyqtProperty(str, notify = activeVariantChanged)
|
@pyqtProperty(str, notify = activeVariantChanged)
|
||||||
|
@deprecated("use Cura.activeStack.variant.name instead", "4.1")
|
||||||
def activeVariantName(self) -> str:
|
def activeVariantName(self) -> str:
|
||||||
if self._active_container_stack:
|
if self._active_container_stack:
|
||||||
variant = self._active_container_stack.variant
|
variant = self._active_container_stack.variant
|
||||||
@ -675,6 +730,7 @@ class MachineManager(QObject):
|
|||||||
return ""
|
return ""
|
||||||
|
|
||||||
@pyqtProperty(str, notify = activeVariantChanged)
|
@pyqtProperty(str, notify = activeVariantChanged)
|
||||||
|
@deprecated("use Cura.activeStack.variant.id instead", "4.1")
|
||||||
def activeVariantId(self) -> str:
|
def activeVariantId(self) -> str:
|
||||||
if self._active_container_stack:
|
if self._active_container_stack:
|
||||||
variant = self._active_container_stack.variant
|
variant = self._active_container_stack.variant
|
||||||
@ -684,6 +740,7 @@ class MachineManager(QObject):
|
|||||||
return ""
|
return ""
|
||||||
|
|
||||||
@pyqtProperty(str, notify = activeVariantChanged)
|
@pyqtProperty(str, notify = activeVariantChanged)
|
||||||
|
@deprecated("use Cura.activeMachine.variant.name instead", "4.1")
|
||||||
def activeVariantBuildplateName(self) -> str:
|
def activeVariantBuildplateName(self) -> str:
|
||||||
if self._global_container_stack:
|
if self._global_container_stack:
|
||||||
variant = self._global_container_stack.variant
|
variant = self._global_container_stack.variant
|
||||||
@ -693,6 +750,7 @@ class MachineManager(QObject):
|
|||||||
return ""
|
return ""
|
||||||
|
|
||||||
@pyqtProperty(str, notify = globalContainerChanged)
|
@pyqtProperty(str, notify = globalContainerChanged)
|
||||||
|
@deprecated("use Cura.activeMachine.definition.id instead", "4.1")
|
||||||
def activeDefinitionId(self) -> str:
|
def activeDefinitionId(self) -> str:
|
||||||
if self._global_container_stack:
|
if self._global_container_stack:
|
||||||
return self._global_container_stack.definition.id
|
return self._global_container_stack.definition.id
|
||||||
@ -731,7 +789,7 @@ class MachineManager(QObject):
|
|||||||
# If the machine that is being removed is the currently active machine, set another machine as the active machine.
|
# If the machine that is being removed is the currently active machine, set another machine as the active machine.
|
||||||
activate_new_machine = (self._global_container_stack and self._global_container_stack.getId() == machine_id)
|
activate_new_machine = (self._global_container_stack and self._global_container_stack.getId() == machine_id)
|
||||||
|
|
||||||
# activate a new machine before removing a machine because this is safer
|
# Activate a new machine before removing a machine because this is safer
|
||||||
if activate_new_machine:
|
if activate_new_machine:
|
||||||
machine_stacks = CuraContainerRegistry.getInstance().findContainerStacksMetadata(type = "machine")
|
machine_stacks = CuraContainerRegistry.getInstance().findContainerStacksMetadata(type = "machine")
|
||||||
other_machine_stacks = [s for s in machine_stacks if s["id"] != machine_id]
|
other_machine_stacks = [s for s in machine_stacks if s["id"] != machine_id]
|
||||||
@ -739,7 +797,7 @@ class MachineManager(QObject):
|
|||||||
self.setActiveMachine(other_machine_stacks[0]["id"])
|
self.setActiveMachine(other_machine_stacks[0]["id"])
|
||||||
|
|
||||||
metadata = CuraContainerRegistry.getInstance().findContainerStacksMetadata(id = machine_id)[0]
|
metadata = CuraContainerRegistry.getInstance().findContainerStacksMetadata(id = machine_id)[0]
|
||||||
network_key = metadata["um_network_key"] if "um_network_key" in metadata else None
|
network_key = metadata.get("um_network_key", None)
|
||||||
ExtruderManager.getInstance().removeMachineExtruders(machine_id)
|
ExtruderManager.getInstance().removeMachineExtruders(machine_id)
|
||||||
containers = CuraContainerRegistry.getInstance().findInstanceContainersMetadata(type = "user", machine = machine_id)
|
containers = CuraContainerRegistry.getInstance().findInstanceContainersMetadata(type = "user", machine = machine_id)
|
||||||
for container in containers:
|
for container in containers:
|
||||||
@ -757,19 +815,19 @@ class MachineManager(QObject):
|
|||||||
@pyqtProperty(bool, notify = globalContainerChanged)
|
@pyqtProperty(bool, notify = globalContainerChanged)
|
||||||
def hasMaterials(self) -> bool:
|
def hasMaterials(self) -> bool:
|
||||||
if self._global_container_stack:
|
if self._global_container_stack:
|
||||||
return Util.parseBool(self._global_container_stack.getMetaDataEntry("has_materials", False))
|
return self._global_container_stack.getHasMaterials()
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@pyqtProperty(bool, notify = globalContainerChanged)
|
@pyqtProperty(bool, notify = globalContainerChanged)
|
||||||
def hasVariants(self) -> bool:
|
def hasVariants(self) -> bool:
|
||||||
if self._global_container_stack:
|
if self._global_container_stack:
|
||||||
return Util.parseBool(self._global_container_stack.getMetaDataEntry("has_variants", False))
|
return self._global_container_stack.getHasVariants()
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@pyqtProperty(bool, notify = globalContainerChanged)
|
@pyqtProperty(bool, notify = globalContainerChanged)
|
||||||
def hasVariantBuildplates(self) -> bool:
|
def hasVariantBuildplates(self) -> bool:
|
||||||
if self._global_container_stack:
|
if self._global_container_stack:
|
||||||
return Util.parseBool(self._global_container_stack.getMetaDataEntry("has_variant_buildplates", False))
|
return self._global_container_stack.getHasVariantsBuildPlates()
|
||||||
return False
|
return False
|
||||||
|
|
||||||
## The selected buildplate is compatible if it is compatible with all the materials in all the extruders
|
## The selected buildplate is compatible if it is compatible with all the materials in all the extruders
|
||||||
@ -864,7 +922,7 @@ class MachineManager(QObject):
|
|||||||
caution_message = Message(catalog.i18nc(
|
caution_message = Message(catalog.i18nc(
|
||||||
"@info:generic",
|
"@info:generic",
|
||||||
"Settings have been changed to match the current availability of extruders: [%s]" % ", ".join(add_user_changes)),
|
"Settings have been changed to match the current availability of extruders: [%s]" % ", ".join(add_user_changes)),
|
||||||
lifetime=0,
|
lifetime = 0,
|
||||||
title = catalog.i18nc("@info:title", "Settings updated"))
|
title = catalog.i18nc("@info:title", "Settings updated"))
|
||||||
caution_message.show()
|
caution_message.show()
|
||||||
|
|
||||||
@ -909,21 +967,18 @@ class MachineManager(QObject):
|
|||||||
# After CURA-4482 this should not be the case anymore, but we still want to support older project files.
|
# After CURA-4482 this should not be the case anymore, but we still want to support older project files.
|
||||||
global_user_container = self._global_container_stack.userChanges
|
global_user_container = self._global_container_stack.userChanges
|
||||||
|
|
||||||
# Make sure extruder_stacks exists
|
|
||||||
extruder_stacks = [] #type: List[ExtruderStack]
|
|
||||||
|
|
||||||
if previous_extruder_count == 1:
|
|
||||||
extruder_stacks = ExtruderManager.getInstance().getActiveExtruderStacks()
|
|
||||||
global_user_container = self._global_container_stack.userChanges
|
|
||||||
|
|
||||||
for setting_instance in global_user_container.findInstances():
|
for setting_instance in global_user_container.findInstances():
|
||||||
setting_key = setting_instance.definition.key
|
setting_key = setting_instance.definition.key
|
||||||
settable_per_extruder = self._global_container_stack.getProperty(setting_key, "settable_per_extruder")
|
settable_per_extruder = self._global_container_stack.getProperty(setting_key, "settable_per_extruder")
|
||||||
|
|
||||||
if settable_per_extruder:
|
if settable_per_extruder:
|
||||||
limit_to_extruder = int(self._global_container_stack.getProperty(setting_key, "limit_to_extruder"))
|
limit_to_extruder = int(self._global_container_stack.getProperty(setting_key, "limit_to_extruder"))
|
||||||
extruder_stack = extruder_stacks[max(0, limit_to_extruder)]
|
extruder_position = max(0, limit_to_extruder)
|
||||||
extruder_stack.userChanges.setProperty(setting_key, "value", global_user_container.getProperty(setting_key, "value"))
|
extruder_stack = self.getExtruder(extruder_position)
|
||||||
|
if extruder_stack:
|
||||||
|
extruder_stack.userChanges.setProperty(setting_key, "value", global_user_container.getProperty(setting_key, "value"))
|
||||||
|
else:
|
||||||
|
Logger.log("e", "Unable to find extruder on position %s", extruder_position)
|
||||||
global_user_container.removeInstance(setting_key)
|
global_user_container.removeInstance(setting_key)
|
||||||
|
|
||||||
# Signal that the global stack has changed
|
# Signal that the global stack has changed
|
||||||
@ -932,10 +987,9 @@ class MachineManager(QObject):
|
|||||||
|
|
||||||
@pyqtSlot(int, result = QObject)
|
@pyqtSlot(int, result = QObject)
|
||||||
def getExtruder(self, position: int) -> Optional[ExtruderStack]:
|
def getExtruder(self, position: int) -> Optional[ExtruderStack]:
|
||||||
extruder = None
|
|
||||||
if self._global_container_stack:
|
if self._global_container_stack:
|
||||||
extruder = self._global_container_stack.extruders.get(str(position))
|
return self._global_container_stack.extruders.get(str(position))
|
||||||
return extruder
|
return None
|
||||||
|
|
||||||
def updateDefaultExtruder(self) -> None:
|
def updateDefaultExtruder(self) -> None:
|
||||||
if self._global_container_stack is None:
|
if self._global_container_stack is None:
|
||||||
@ -1001,12 +1055,12 @@ class MachineManager(QObject):
|
|||||||
if not enabled and position == ExtruderManager.getInstance().activeExtruderIndex:
|
if not enabled and position == ExtruderManager.getInstance().activeExtruderIndex:
|
||||||
ExtruderManager.getInstance().setActiveExtruderIndex(int(self._default_extruder_position))
|
ExtruderManager.getInstance().setActiveExtruderIndex(int(self._default_extruder_position))
|
||||||
|
|
||||||
# ensure that the quality profile is compatible with current combination, or choose a compatible one if available
|
# Ensure that the quality profile is compatible with current combination, or choose a compatible one if available
|
||||||
self._updateQualityWithMaterial()
|
self._updateQualityWithMaterial()
|
||||||
self.extruderChanged.emit()
|
self.extruderChanged.emit()
|
||||||
# update material compatibility color
|
# Update material compatibility color
|
||||||
self.activeQualityGroupChanged.emit()
|
self.activeQualityGroupChanged.emit()
|
||||||
# update items in SettingExtruder
|
# Update items in SettingExtruder
|
||||||
ExtruderManager.getInstance().extrudersChanged.emit(self._global_container_stack.getId())
|
ExtruderManager.getInstance().extrudersChanged.emit(self._global_container_stack.getId())
|
||||||
# Make sure the front end reflects changes
|
# Make sure the front end reflects changes
|
||||||
self.forceUpdateAllSettings()
|
self.forceUpdateAllSettings()
|
||||||
@ -1019,9 +1073,6 @@ class MachineManager(QObject):
|
|||||||
def _onMaterialNameChanged(self) -> None:
|
def _onMaterialNameChanged(self) -> None:
|
||||||
self.activeMaterialChanged.emit()
|
self.activeMaterialChanged.emit()
|
||||||
|
|
||||||
def _onQualityNameChanged(self) -> None:
|
|
||||||
self.activeQualityChanged.emit()
|
|
||||||
|
|
||||||
def _getContainerChangedSignals(self) -> List[Signal]:
|
def _getContainerChangedSignals(self) -> List[Signal]:
|
||||||
if self._global_container_stack is None:
|
if self._global_container_stack is None:
|
||||||
return []
|
return []
|
||||||
@ -1080,7 +1131,6 @@ class MachineManager(QObject):
|
|||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
#
|
|
||||||
# Sets all quality and quality_changes containers to empty_quality and empty_quality_changes containers
|
# Sets all quality and quality_changes containers to empty_quality and empty_quality_changes containers
|
||||||
# for all stacks in the currently active machine.
|
# for all stacks in the currently active machine.
|
||||||
#
|
#
|
||||||
@ -1139,7 +1189,7 @@ class MachineManager(QObject):
|
|||||||
|
|
||||||
def _setQualityChangesGroup(self, quality_changes_group: "QualityChangesGroup") -> None:
|
def _setQualityChangesGroup(self, quality_changes_group: "QualityChangesGroup") -> None:
|
||||||
if self._global_container_stack is None:
|
if self._global_container_stack is None:
|
||||||
return #Can't change that.
|
return # Can't change that.
|
||||||
quality_type = quality_changes_group.quality_type
|
quality_type = quality_changes_group.quality_type
|
||||||
# A custom quality can be created based on "not supported".
|
# A custom quality can be created based on "not supported".
|
||||||
# In that case, do not set quality containers to empty.
|
# In that case, do not set quality containers to empty.
|
||||||
@ -1209,7 +1259,7 @@ class MachineManager(QObject):
|
|||||||
self.rootMaterialChanged.emit()
|
self.rootMaterialChanged.emit()
|
||||||
|
|
||||||
def activeMaterialsCompatible(self) -> bool:
|
def activeMaterialsCompatible(self) -> bool:
|
||||||
# check material - variant compatibility
|
# Check material - variant compatibility
|
||||||
if self._global_container_stack is not None:
|
if self._global_container_stack is not None:
|
||||||
if Util.parseBool(self._global_container_stack.getMetaDataEntry("has_materials", False)):
|
if Util.parseBool(self._global_container_stack.getMetaDataEntry("has_materials", False)):
|
||||||
for position, extruder in self._global_container_stack.extruders.items():
|
for position, extruder in self._global_container_stack.extruders.items():
|
||||||
@ -1310,17 +1360,18 @@ class MachineManager(QObject):
|
|||||||
# Get the definition id corresponding to this machine name
|
# Get the definition id corresponding to this machine name
|
||||||
machine_definition_id = CuraContainerRegistry.getInstance().findDefinitionContainers(name = machine_name)[0].getId()
|
machine_definition_id = CuraContainerRegistry.getInstance().findDefinitionContainers(name = machine_name)[0].getId()
|
||||||
# Try to find a machine with the same network key
|
# Try to find a machine with the same network key
|
||||||
new_machine = self.getMachine(machine_definition_id, metadata_filter = {"um_network_key": self.activeMachineNetworkKey})
|
new_machine = self.getMachine(machine_definition_id, metadata_filter = {"um_network_key": self.activeMachineNetworkKey()})
|
||||||
# If there is no machine, then create a new one and set it to the non-hidden instance
|
# If there is no machine, then create a new one and set it to the non-hidden instance
|
||||||
if not new_machine:
|
if not new_machine:
|
||||||
new_machine = CuraStackBuilder.createMachine(machine_definition_id + "_sync", machine_definition_id)
|
new_machine = CuraStackBuilder.createMachine(machine_definition_id + "_sync", machine_definition_id)
|
||||||
if not new_machine:
|
if not new_machine:
|
||||||
return
|
return
|
||||||
new_machine.setMetaDataEntry("um_network_key", self.activeMachineNetworkKey)
|
new_machine.setMetaDataEntry("um_network_key", self.activeMachineNetworkKey())
|
||||||
new_machine.setMetaDataEntry("connect_group_name", self.activeMachineNetworkGroupName)
|
new_machine.setMetaDataEntry("group_name", self.activeMachineNetworkGroupName)
|
||||||
new_machine.setMetaDataEntry("hidden", False)
|
new_machine.setMetaDataEntry("hidden", False)
|
||||||
|
new_machine.setMetaDataEntry("connection_type", self._global_container_stack.getMetaDataEntry("connection_type"))
|
||||||
else:
|
else:
|
||||||
Logger.log("i", "Found a %s with the key %s. Let's use it!", machine_name, self.activeMachineNetworkKey)
|
Logger.log("i", "Found a %s with the key %s. Let's use it!", machine_name, self.activeMachineNetworkKey())
|
||||||
new_machine.setMetaDataEntry("hidden", False)
|
new_machine.setMetaDataEntry("hidden", False)
|
||||||
|
|
||||||
# Set the current printer instance to hidden (the metadata entry must exist)
|
# Set the current printer instance to hidden (the metadata entry must exist)
|
||||||
@ -1329,31 +1380,65 @@ class MachineManager(QObject):
|
|||||||
self.setActiveMachine(new_machine.getId())
|
self.setActiveMachine(new_machine.getId())
|
||||||
|
|
||||||
@pyqtSlot(QObject)
|
@pyqtSlot(QObject)
|
||||||
def applyRemoteConfiguration(self, configuration: ConfigurationModel) -> None:
|
def applyRemoteConfiguration(self, configuration: PrinterConfigurationModel) -> None:
|
||||||
if self._global_container_stack is None:
|
if self._global_container_stack is None:
|
||||||
return
|
return
|
||||||
self.blurSettings.emit()
|
self.blurSettings.emit()
|
||||||
with postponeSignals(*self._getContainerChangedSignals(), compress = CompressTechnique.CompressPerParameterValue):
|
with postponeSignals(*self._getContainerChangedSignals(), compress = CompressTechnique.CompressPerParameterValue):
|
||||||
self.switchPrinterType(configuration.printerType)
|
self.switchPrinterType(configuration.printerType)
|
||||||
|
|
||||||
|
disabled_used_extruder_position_set = set()
|
||||||
|
extruders_to_disable = set()
|
||||||
|
|
||||||
|
# If an extruder that's currently used to print a model gets disabled due to the syncing, we need to show
|
||||||
|
# a message explaining why.
|
||||||
|
need_to_show_message = False
|
||||||
|
|
||||||
|
for extruder_configuration in configuration.extruderConfigurations:
|
||||||
|
# We support "" or None, since the cloud uses None instead of empty strings
|
||||||
|
extruder_has_hotend = extruder_configuration.hotendID and extruder_configuration.hotendID != ""
|
||||||
|
extruder_has_material = extruder_configuration.material.guid and extruder_configuration.material.guid != ""
|
||||||
|
|
||||||
|
# If the machine doesn't have a hotend or material, disable this extruder
|
||||||
|
if not extruder_has_hotend or not extruder_has_material:
|
||||||
|
extruders_to_disable.add(extruder_configuration.position)
|
||||||
|
|
||||||
|
# If there's no material and/or nozzle on the printer, enable the first extruder and disable the rest.
|
||||||
|
if len(extruders_to_disable) == len(self._global_container_stack.extruders):
|
||||||
|
extruders_to_disable.remove(min(extruders_to_disable))
|
||||||
|
|
||||||
for extruder_configuration in configuration.extruderConfigurations:
|
for extruder_configuration in configuration.extruderConfigurations:
|
||||||
position = str(extruder_configuration.position)
|
position = str(extruder_configuration.position)
|
||||||
variant_container_node = self._variant_manager.getVariantNode(self._global_container_stack.definition.getId(), extruder_configuration.hotendID)
|
|
||||||
material_container_node = self._material_manager.getMaterialNodeByType(self._global_container_stack,
|
|
||||||
position,
|
|
||||||
extruder_configuration.hotendID,
|
|
||||||
configuration.buildplateConfiguration,
|
|
||||||
extruder_configuration.material.guid)
|
|
||||||
|
|
||||||
if variant_container_node:
|
# If the machine doesn't have a hotend or material, disable this extruder
|
||||||
self._setVariantNode(position, variant_container_node)
|
if int(position) in extruders_to_disable:
|
||||||
else:
|
self._global_container_stack.extruders[position].setEnabled(False)
|
||||||
self._global_container_stack.extruders[position].variant = empty_variant_container
|
|
||||||
|
need_to_show_message = True
|
||||||
|
disabled_used_extruder_position_set.add(int(position))
|
||||||
|
|
||||||
if material_container_node:
|
|
||||||
self._setMaterial(position, material_container_node)
|
|
||||||
else:
|
else:
|
||||||
self._global_container_stack.extruders[position].material = empty_material_container
|
variant_container_node = self._variant_manager.getVariantNode(self._global_container_stack.definition.getId(),
|
||||||
self.updateMaterialWithVariant(position)
|
extruder_configuration.hotendID)
|
||||||
|
material_container_node = self._material_manager.getMaterialNodeByType(self._global_container_stack,
|
||||||
|
position,
|
||||||
|
extruder_configuration.hotendID,
|
||||||
|
configuration.buildplateConfiguration,
|
||||||
|
extruder_configuration.material.guid)
|
||||||
|
if variant_container_node:
|
||||||
|
self._setVariantNode(position, variant_container_node)
|
||||||
|
else:
|
||||||
|
self._global_container_stack.extruders[position].variant = empty_variant_container
|
||||||
|
|
||||||
|
if material_container_node:
|
||||||
|
self._setMaterial(position, material_container_node)
|
||||||
|
else:
|
||||||
|
self._global_container_stack.extruders[position].material = empty_material_container
|
||||||
|
self._global_container_stack.extruders[position].setEnabled(True)
|
||||||
|
self.updateMaterialWithVariant(position)
|
||||||
|
|
||||||
|
self.updateDefaultExtruder()
|
||||||
|
self.updateNumberExtrudersEnabled()
|
||||||
|
|
||||||
if configuration.buildplateConfiguration is not None:
|
if configuration.buildplateConfiguration is not None:
|
||||||
global_variant_container_node = self._variant_manager.getBuildplateVariantNode(self._global_container_stack.definition.getId(), configuration.buildplateConfiguration)
|
global_variant_container_node = self._variant_manager.getBuildplateVariantNode(self._global_container_stack.definition.getId(), configuration.buildplateConfiguration)
|
||||||
@ -1365,35 +1450,25 @@ class MachineManager(QObject):
|
|||||||
self._global_container_stack.variant = empty_variant_container
|
self._global_container_stack.variant = empty_variant_container
|
||||||
self._updateQualityWithMaterial()
|
self._updateQualityWithMaterial()
|
||||||
|
|
||||||
|
if need_to_show_message:
|
||||||
|
msg_str = "{extruders} is disabled because there is no material loaded. Please load a material or use custom configurations."
|
||||||
|
|
||||||
|
# Show human-readable extruder names such as "Extruder Left", "Extruder Front" instead of "Extruder 1, 2, 3".
|
||||||
|
extruder_names = []
|
||||||
|
for extruder_position in sorted(disabled_used_extruder_position_set):
|
||||||
|
extruder_stack = self._global_container_stack.extruders[str(extruder_position)]
|
||||||
|
extruder_name = extruder_stack.definition.getName()
|
||||||
|
extruder_names.append(extruder_name)
|
||||||
|
extruders_str = ", ".join(extruder_names)
|
||||||
|
msg_str = msg_str.format(extruders = extruders_str)
|
||||||
|
message = Message(catalog.i18nc("@info:status", msg_str),
|
||||||
|
title = catalog.i18nc("@info:title", "Extruder(s) Disabled"))
|
||||||
|
message.show()
|
||||||
|
|
||||||
# See if we need to show the Discard or Keep changes screen
|
# See if we need to show the Discard or Keep changes screen
|
||||||
if self.hasUserSettings and self._application.getPreferences().getValue("cura/active_mode") == 1:
|
if self.hasUserSettings and self._application.getPreferences().getValue("cura/active_mode") == 1:
|
||||||
self._application.discardOrKeepProfileChanges()
|
self._application.discardOrKeepProfileChanges()
|
||||||
|
|
||||||
## Find all container stacks that has the pair 'key = value' in its metadata and replaces the value with 'new_value'
|
|
||||||
def replaceContainersMetadata(self, key: str, value: str, new_value: str) -> None:
|
|
||||||
machines = CuraContainerRegistry.getInstance().findContainerStacks(type = "machine")
|
|
||||||
for machine in machines:
|
|
||||||
if machine.getMetaDataEntry(key) == value:
|
|
||||||
machine.setMetaDataEntry(key, new_value)
|
|
||||||
|
|
||||||
## 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:
|
|
||||||
if self._global_container_stack and device_id == self.activeMachineNetworkKey:
|
|
||||||
# Check if the connect_group_name is correct. If not, update all the containers connected to the same printer
|
|
||||||
if self.activeMachineNetworkGroupName != group_name:
|
|
||||||
metadata_filter = {"um_network_key": self.activeMachineNetworkKey}
|
|
||||||
containers = CuraContainerRegistry.getInstance().findContainerStacks(type = "machine", **metadata_filter)
|
|
||||||
for container in containers:
|
|
||||||
container.setMetaDataEntry("connect_group_name", group_name)
|
|
||||||
|
|
||||||
## This method checks if there is an instance connected to the given network_key
|
|
||||||
def existNetworkInstances(self, network_key: str) -> bool:
|
|
||||||
metadata_filter = {"um_network_key": network_key}
|
|
||||||
containers = CuraContainerRegistry.getInstance().findContainerStacks(type = "machine", **metadata_filter)
|
|
||||||
return bool(containers)
|
|
||||||
|
|
||||||
@pyqtSlot("QVariant")
|
@pyqtSlot("QVariant")
|
||||||
def setGlobalVariant(self, container_node: "ContainerNode") -> None:
|
def setGlobalVariant(self, container_node: "ContainerNode") -> None:
|
||||||
self.blurSettings.emit()
|
self.blurSettings.emit()
|
||||||
@ -1419,7 +1494,7 @@ class MachineManager(QObject):
|
|||||||
material_diameter, root_material_id)
|
material_diameter, root_material_id)
|
||||||
self.setMaterial(position, material_node)
|
self.setMaterial(position, material_node)
|
||||||
|
|
||||||
## global_stack: if you want to provide your own global_stack instead of the current active one
|
## Global_stack: if you want to provide your own global_stack instead of the current active one
|
||||||
# if you update an active machine, special measures have to be taken.
|
# if you update an active machine, special measures have to be taken.
|
||||||
@pyqtSlot(str, "QVariant")
|
@pyqtSlot(str, "QVariant")
|
||||||
def setMaterial(self, position: str, container_node, global_stack: Optional["GlobalStack"] = None) -> None:
|
def setMaterial(self, position: str, container_node, global_stack: Optional["GlobalStack"] = None) -> None:
|
||||||
@ -1522,6 +1597,10 @@ class MachineManager(QObject):
|
|||||||
def activeQualityChangesGroup(self) -> Optional["QualityChangesGroup"]:
|
def activeQualityChangesGroup(self) -> Optional["QualityChangesGroup"]:
|
||||||
return self._current_quality_changes_group
|
return self._current_quality_changes_group
|
||||||
|
|
||||||
|
@pyqtProperty(bool, notify = activeQualityChangesGroupChanged)
|
||||||
|
def hasCustomQuality(self) -> bool:
|
||||||
|
return self._current_quality_changes_group is not None
|
||||||
|
|
||||||
@pyqtProperty(str, notify = activeQualityGroupChanged)
|
@pyqtProperty(str, notify = activeQualityGroupChanged)
|
||||||
def activeQualityOrQualityChangesName(self) -> str:
|
def activeQualityOrQualityChangesName(self) -> str:
|
||||||
name = empty_quality_container.getName()
|
name = empty_quality_container.getName()
|
||||||
@ -1531,9 +1610,40 @@ class MachineManager(QObject):
|
|||||||
name = self._current_quality_group.name
|
name = self._current_quality_group.name
|
||||||
return name
|
return name
|
||||||
|
|
||||||
|
@pyqtProperty(bool, notify = activeQualityGroupChanged)
|
||||||
|
def hasNotSupportedQuality(self) -> bool:
|
||||||
|
return self._current_quality_group is None and self._current_quality_changes_group is None
|
||||||
|
|
||||||
def _updateUponMaterialMetadataChange(self) -> None:
|
def _updateUponMaterialMetadataChange(self) -> None:
|
||||||
if self._global_container_stack is None:
|
if self._global_container_stack is None:
|
||||||
return
|
return
|
||||||
with postponeSignals(*self._getContainerChangedSignals(), compress = CompressTechnique.CompressPerParameterValue):
|
with postponeSignals(*self._getContainerChangedSignals(), compress = CompressTechnique.CompressPerParameterValue):
|
||||||
self.updateMaterialWithVariant(None)
|
self.updateMaterialWithVariant(None)
|
||||||
self._updateQualityWithMaterial()
|
self._updateQualityWithMaterial()
|
||||||
|
|
||||||
|
## This function will translate any printer type name to an abbreviated printer type name
|
||||||
|
@pyqtSlot(str, result = str)
|
||||||
|
def getAbbreviatedMachineName(self, machine_type_name: str) -> str:
|
||||||
|
abbr_machine = ""
|
||||||
|
for word in re.findall(r"[\w']+", machine_type_name):
|
||||||
|
if word.lower() == "ultimaker":
|
||||||
|
abbr_machine += "UM"
|
||||||
|
elif word.isdigit():
|
||||||
|
abbr_machine += word
|
||||||
|
else:
|
||||||
|
stripped_word = "".join(char for char in unicodedata.normalize("NFD", word.upper()) if unicodedata.category(char) != "Mn")
|
||||||
|
# - use only the first character if the word is too long (> 3 characters)
|
||||||
|
# - use the whole word if it's not too long (<= 3 characters)
|
||||||
|
if len(stripped_word) > 3:
|
||||||
|
stripped_word = stripped_word[0]
|
||||||
|
abbr_machine += stripped_word
|
||||||
|
|
||||||
|
return abbr_machine
|
||||||
|
|
||||||
|
@pyqtSlot(str, result = str)
|
||||||
|
def getMachineTypeNameFromId(self, machine_type_id: str) -> str:
|
||||||
|
machine_type_name = ""
|
||||||
|
results = self._container_registry.findDefinitionContainersMetadata(id = machine_type_id)
|
||||||
|
if results:
|
||||||
|
machine_type_name = results[0]["name"]
|
||||||
|
return machine_type_name
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -73,8 +73,8 @@ class SettingOverrideDecorator(SceneNodeDecorator):
|
|||||||
|
|
||||||
# use value from the stack because there can be a delay in signal triggering and "_is_non_printing_mesh"
|
# use value from the stack because there can be a delay in signal triggering and "_is_non_printing_mesh"
|
||||||
# has not been updated yet.
|
# has not been updated yet.
|
||||||
deep_copy._is_non_printing_mesh = self.evaluateIsNonPrintingMesh()
|
deep_copy._is_non_printing_mesh = self._evaluateIsNonPrintingMesh()
|
||||||
deep_copy._is_non_thumbnail_visible_mesh = self.evaluateIsNonThumbnailVisibleMesh()
|
deep_copy._is_non_thumbnail_visible_mesh = self._evaluateIsNonThumbnailVisibleMesh()
|
||||||
|
|
||||||
return deep_copy
|
return deep_copy
|
||||||
|
|
||||||
@ -102,23 +102,32 @@ class SettingOverrideDecorator(SceneNodeDecorator):
|
|||||||
def isNonPrintingMesh(self):
|
def isNonPrintingMesh(self):
|
||||||
return self._is_non_printing_mesh
|
return self._is_non_printing_mesh
|
||||||
|
|
||||||
def evaluateIsNonPrintingMesh(self):
|
def _evaluateIsNonPrintingMesh(self):
|
||||||
return any(bool(self._stack.getProperty(setting, "value")) for setting in self._non_printing_mesh_settings)
|
return any(bool(self._stack.getProperty(setting, "value")) for setting in self._non_printing_mesh_settings)
|
||||||
|
|
||||||
def isNonThumbnailVisibleMesh(self):
|
def isNonThumbnailVisibleMesh(self):
|
||||||
return self._is_non_thumbnail_visible_mesh
|
return self._is_non_thumbnail_visible_mesh
|
||||||
|
|
||||||
def evaluateIsNonThumbnailVisibleMesh(self):
|
def _evaluateIsNonThumbnailVisibleMesh(self):
|
||||||
return any(bool(self._stack.getProperty(setting, "value")) for setting in self._non_thumbnail_visible_settings)
|
return any(bool(self._stack.getProperty(setting, "value")) for setting in self._non_thumbnail_visible_settings)
|
||||||
|
|
||||||
def _onSettingChanged(self, instance, property_name): # Reminder: 'property' is a built-in function
|
def _onSettingChanged(self, setting_key, property_name): # Reminder: 'property' is a built-in function
|
||||||
if property_name == "value":
|
# We're only interested in a few settings and only if it's value changed.
|
||||||
|
if property_name == "value" and (setting_key in self._non_printing_mesh_settings or setting_key in self._non_thumbnail_visible_settings):
|
||||||
# Trigger slice/need slicing if the value has changed.
|
# Trigger slice/need slicing if the value has changed.
|
||||||
self._is_non_printing_mesh = self.evaluateIsNonPrintingMesh()
|
new_is_non_printing_mesh = self._evaluateIsNonPrintingMesh()
|
||||||
self._is_non_thumbnail_visible_mesh = self.evaluateIsNonThumbnailVisibleMesh()
|
new_is_non_thumbnail_visible_mesh = self._evaluateIsNonThumbnailVisibleMesh()
|
||||||
|
changed = False
|
||||||
|
if self._is_non_printing_mesh != new_is_non_printing_mesh:
|
||||||
|
self._is_non_printing_mesh = new_is_non_printing_mesh
|
||||||
|
self._node.setCalculateBoundingBox(not self._is_non_printing_mesh)
|
||||||
|
changed = True
|
||||||
|
if self._is_non_thumbnail_visible_mesh != new_is_non_thumbnail_visible_mesh:
|
||||||
|
changed = True
|
||||||
|
|
||||||
Application.getInstance().getBackend().needsSlicing()
|
if changed:
|
||||||
Application.getInstance().getBackend().tickle()
|
Application.getInstance().getBackend().needsSlicing()
|
||||||
|
Application.getInstance().getBackend().tickle()
|
||||||
|
|
||||||
## Makes sure that the stack upon which the container stack is placed is
|
## Makes sure that the stack upon which the container stack is placed is
|
||||||
# kept up to date.
|
# kept up to date.
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
# Copyright (c) 2017 Ultimaker B.V.
|
# Copyright (c) 2017 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 Set
|
||||||
|
|
||||||
from PyQt5.QtCore import QObject, pyqtSignal, pyqtProperty
|
from PyQt5.QtCore import QObject, pyqtSignal, pyqtProperty, pyqtSlot
|
||||||
|
|
||||||
from UM.Application import Application
|
from UM.Application import Application
|
||||||
|
|
||||||
@ -16,15 +17,11 @@ class SimpleModeSettingsManager(QObject):
|
|||||||
self._is_profile_user_created = False # True when profile was custom created by user
|
self._is_profile_user_created = False # True when profile was custom created by user
|
||||||
|
|
||||||
self._machine_manager.activeStackValueChanged.connect(self._updateIsProfileCustomized)
|
self._machine_manager.activeStackValueChanged.connect(self._updateIsProfileCustomized)
|
||||||
self._machine_manager.activeQualityGroupChanged.connect(self._updateIsProfileUserCreated)
|
|
||||||
self._machine_manager.activeQualityChangesGroupChanged.connect(self._updateIsProfileUserCreated)
|
|
||||||
|
|
||||||
# update on create as the activeQualityChanged signal is emitted before this manager is created when Cura starts
|
# update on create as the activeQualityChanged signal is emitted before this manager is created when Cura starts
|
||||||
self._updateIsProfileCustomized()
|
self._updateIsProfileCustomized()
|
||||||
self._updateIsProfileUserCreated()
|
|
||||||
|
|
||||||
isProfileCustomizedChanged = pyqtSignal()
|
isProfileCustomizedChanged = pyqtSignal()
|
||||||
isProfileUserCreatedChanged = pyqtSignal()
|
|
||||||
|
|
||||||
@pyqtProperty(bool, notify = isProfileCustomizedChanged)
|
@pyqtProperty(bool, notify = isProfileCustomizedChanged)
|
||||||
def isProfileCustomized(self):
|
def isProfileCustomized(self):
|
||||||
@ -57,33 +54,6 @@ class SimpleModeSettingsManager(QObject):
|
|||||||
self._is_profile_customized = has_customized_user_settings
|
self._is_profile_customized = has_customized_user_settings
|
||||||
self.isProfileCustomizedChanged.emit()
|
self.isProfileCustomizedChanged.emit()
|
||||||
|
|
||||||
@pyqtProperty(bool, notify = isProfileUserCreatedChanged)
|
|
||||||
def isProfileUserCreated(self):
|
|
||||||
return self._is_profile_user_created
|
|
||||||
|
|
||||||
def _updateIsProfileUserCreated(self):
|
|
||||||
quality_changes_keys = set()
|
|
||||||
|
|
||||||
if not self._machine_manager.activeMachine:
|
|
||||||
return False
|
|
||||||
|
|
||||||
global_stack = self._machine_manager.activeMachine
|
|
||||||
|
|
||||||
# check quality changes settings in the global stack
|
|
||||||
quality_changes_keys.update(global_stack.qualityChanges.getAllKeys())
|
|
||||||
|
|
||||||
# check quality changes settings in the extruder stacks
|
|
||||||
if global_stack.extruders:
|
|
||||||
for extruder_stack in global_stack.extruders.values():
|
|
||||||
quality_changes_keys.update(extruder_stack.qualityChanges.getAllKeys())
|
|
||||||
|
|
||||||
# check if the qualityChanges container is not empty (meaning it is a user created profile)
|
|
||||||
has_quality_changes = len(quality_changes_keys) > 0
|
|
||||||
|
|
||||||
if has_quality_changes != self._is_profile_user_created:
|
|
||||||
self._is_profile_user_created = has_quality_changes
|
|
||||||
self.isProfileUserCreatedChanged.emit()
|
|
||||||
|
|
||||||
# These are the settings included in the Simple ("Recommended") Mode, so only when the other settings have been
|
# These are the settings included in the Simple ("Recommended") Mode, so only when the other settings have been
|
||||||
# changed, we consider it as a user customized profile in the Simple ("Recommended") Mode.
|
# changed, we consider it as a user customized profile in the Simple ("Recommended") Mode.
|
||||||
__ignored_custom_setting_keys = ["support_enable",
|
__ignored_custom_setting_keys = ["support_enable",
|
||||||
|
@ -41,6 +41,22 @@ empty_quality_changes_container.setMetaDataEntry("type", "quality_changes")
|
|||||||
empty_quality_changes_container.setMetaDataEntry("quality_type", "not_supported")
|
empty_quality_changes_container.setMetaDataEntry("quality_type", "not_supported")
|
||||||
|
|
||||||
|
|
||||||
|
# All empty container IDs set
|
||||||
|
ALL_EMPTY_CONTAINER_ID_SET = {
|
||||||
|
EMPTY_CONTAINER_ID,
|
||||||
|
EMPTY_DEFINITION_CHANGES_CONTAINER_ID,
|
||||||
|
EMPTY_VARIANT_CONTAINER_ID,
|
||||||
|
EMPTY_MATERIAL_CONTAINER_ID,
|
||||||
|
EMPTY_QUALITY_CONTAINER_ID,
|
||||||
|
EMPTY_QUALITY_CHANGES_CONTAINER_ID,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# Convenience function to check if a container ID represents an empty container.
|
||||||
|
def isEmptyContainer(container_id: str) -> bool:
|
||||||
|
return container_id in ALL_EMPTY_CONTAINER_ID_SET
|
||||||
|
|
||||||
|
|
||||||
__all__ = ["EMPTY_CONTAINER_ID",
|
__all__ = ["EMPTY_CONTAINER_ID",
|
||||||
"empty_container", # For convenience
|
"empty_container", # For convenience
|
||||||
"EMPTY_DEFINITION_CHANGES_CONTAINER_ID",
|
"EMPTY_DEFINITION_CHANGES_CONTAINER_ID",
|
||||||
@ -52,5 +68,7 @@ __all__ = ["EMPTY_CONTAINER_ID",
|
|||||||
"EMPTY_QUALITY_CHANGES_CONTAINER_ID",
|
"EMPTY_QUALITY_CHANGES_CONTAINER_ID",
|
||||||
"empty_quality_changes_container",
|
"empty_quality_changes_container",
|
||||||
"EMPTY_QUALITY_CONTAINER_ID",
|
"EMPTY_QUALITY_CONTAINER_ID",
|
||||||
"empty_quality_container"
|
"empty_quality_container",
|
||||||
|
"ALL_EMPTY_CONTAINER_ID_SET",
|
||||||
|
"isEmptyContainer",
|
||||||
]
|
]
|
||||||
|
@ -1,23 +1,32 @@
|
|||||||
# 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 pyqtProperty, QUrl
|
from PyQt5.QtCore import pyqtProperty, QUrl
|
||||||
|
|
||||||
from UM.Stage import Stage
|
from UM.Stage import Stage
|
||||||
|
|
||||||
|
|
||||||
|
# Since Cura has a few pre-defined "space claims" for the locations of certain components, we've provided some structure
|
||||||
|
# to indicate this.
|
||||||
|
# * The StageMenuComponent is the horizontal area below the stage bar. This should be used to show stage specific
|
||||||
|
# buttons and elements. This component will be drawn over the bar & main component.
|
||||||
|
# * The MainComponent is the component that will be drawn starting from the bottom of the stageBar and fills the rest
|
||||||
|
# of the screen.
|
||||||
class CuraStage(Stage):
|
class CuraStage(Stage):
|
||||||
|
def __init__(self, parent = None) -> None:
|
||||||
def __init__(self, parent = None):
|
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
|
|
||||||
@pyqtProperty(str, constant = True)
|
@pyqtProperty(str, constant = True)
|
||||||
def stageId(self):
|
def stageId(self) -> str:
|
||||||
return self.getPluginId()
|
return self.getPluginId()
|
||||||
|
|
||||||
@pyqtProperty(QUrl, constant = True)
|
@pyqtProperty(QUrl, constant = True)
|
||||||
def mainComponent(self):
|
def mainComponent(self) -> QUrl:
|
||||||
return self.getDisplayComponent("main")
|
return self.getDisplayComponent("main")
|
||||||
|
|
||||||
@pyqtProperty(QUrl, constant = True)
|
@pyqtProperty(QUrl, constant = True)
|
||||||
def sidebarComponent(self):
|
def stageMenuComponent(self) -> QUrl:
|
||||||
return self.getDisplayComponent("sidebar")
|
return self.getDisplayComponent("menu")
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = ["CuraStage"]
|
||||||
|
@ -1,2 +0,0 @@
|
|||||||
# Copyright (c) 2017 Ultimaker B.V.
|
|
||||||
# Cura is released under the terms of the LGPLv3 or higher.
|
|
30
cura/UI/AddPrinterPagesModel.py
Normal file
30
cura/UI/AddPrinterPagesModel.py
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
# Copyright (c) 2019 Ultimaker B.V.
|
||||||
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
|
from .WelcomePagesModel import WelcomePagesModel
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# This Qt ListModel is more or less the same the WelcomePagesModel, except that this model is only for adding a printer,
|
||||||
|
# so only the steps for adding a printer is included.
|
||||||
|
#
|
||||||
|
class AddPrinterPagesModel(WelcomePagesModel):
|
||||||
|
|
||||||
|
def initialize(self) -> None:
|
||||||
|
self._pages.append({"id": "add_network_or_local_printer",
|
||||||
|
"page_url": self._getBuiltinWelcomePagePath("AddNetworkOrLocalPrinterContent.qml"),
|
||||||
|
"next_page_id": "machine_actions",
|
||||||
|
"next_page_button_text": self._catalog.i18nc("@action:button", "Add"),
|
||||||
|
})
|
||||||
|
self._pages.append({"id": "add_printer_by_ip",
|
||||||
|
"page_url": self._getBuiltinWelcomePagePath("AddPrinterByIpContent.qml"),
|
||||||
|
"next_page_id": "machine_actions",
|
||||||
|
})
|
||||||
|
self._pages.append({"id": "machine_actions",
|
||||||
|
"page_url": self._getBuiltinWelcomePagePath("FirstStartMachineActionsContent.qml"),
|
||||||
|
"should_show_function": self.shouldShowMachineActions,
|
||||||
|
})
|
||||||
|
self.setItems(self._pages)
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = ["AddPrinterPagesModel"]
|
@ -12,7 +12,7 @@ from UM.PluginRegistry import PluginRegistry # So MachineAction can be added as
|
|||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from cura.CuraApplication import CuraApplication
|
from cura.CuraApplication import CuraApplication
|
||||||
from cura.Settings.GlobalStack import GlobalStack
|
from cura.Settings.GlobalStack import GlobalStack
|
||||||
from .MachineAction import MachineAction
|
from cura.MachineAction import MachineAction
|
||||||
|
|
||||||
|
|
||||||
## Raised when trying to add an unknown machine action as a required action
|
## Raised when trying to add an unknown machine action as a required action
|
||||||
@ -136,7 +136,7 @@ class MachineActionManager(QObject):
|
|||||||
# action multiple times).
|
# action multiple times).
|
||||||
# \param definition_id The ID of the definition that you want to get the "on added" actions for.
|
# \param definition_id The ID of the definition that you want to get the "on added" actions for.
|
||||||
# \returns List of actions.
|
# \returns List of actions.
|
||||||
@pyqtSlot(str, result="QVariantList")
|
@pyqtSlot(str, result = "QVariantList")
|
||||||
def getFirstStartActions(self, definition_id: str) -> List["MachineAction"]:
|
def getFirstStartActions(self, definition_id: str) -> List["MachineAction"]:
|
||||||
if definition_id in self._first_start_actions:
|
if definition_id in self._first_start_actions:
|
||||||
return self._first_start_actions[definition_id]
|
return self._first_start_actions[definition_id]
|
82
cura/UI/MachineSettingsManager.py
Normal file
82
cura/UI/MachineSettingsManager.py
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
# Copyright (c) 2019 Ultimaker B.V.
|
||||||
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
|
from typing import Optional, TYPE_CHECKING
|
||||||
|
|
||||||
|
from PyQt5.QtCore import QObject, pyqtSlot
|
||||||
|
|
||||||
|
from UM.i18n import i18nCatalog
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from cura.CuraApplication import CuraApplication
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# This manager provides (convenience) functions to the Machine Settings Dialog QML to update certain machine settings.
|
||||||
|
#
|
||||||
|
class MachineSettingsManager(QObject):
|
||||||
|
|
||||||
|
def __init__(self, application: "CuraApplication", parent: Optional["QObject"] = None) -> None:
|
||||||
|
super().__init__(parent)
|
||||||
|
self._i18n_catalog = i18nCatalog("cura")
|
||||||
|
|
||||||
|
self._application = application
|
||||||
|
|
||||||
|
# Force rebuilding the build volume by reloading the global container stack. This is a bit of a hack, but it seems
|
||||||
|
# quite enough.
|
||||||
|
@pyqtSlot()
|
||||||
|
def forceUpdate(self) -> None:
|
||||||
|
self._application.getMachineManager().globalContainerChanged.emit()
|
||||||
|
|
||||||
|
# Function for the Machine Settings panel (QML) to update the compatible material diameter after a user has changed
|
||||||
|
# an extruder's compatible material diameter. This ensures that after the modification, changes can be notified
|
||||||
|
# and updated right away.
|
||||||
|
@pyqtSlot(int)
|
||||||
|
def updateMaterialForDiameter(self, extruder_position: int) -> None:
|
||||||
|
# Updates the material container to a material that matches the material diameter set for the printer
|
||||||
|
self._application.getMachineManager().updateMaterialWithVariant(str(extruder_position))
|
||||||
|
|
||||||
|
@pyqtSlot(int)
|
||||||
|
def setMachineExtruderCount(self, extruder_count: int) -> None:
|
||||||
|
# Note: this method was in this class before, but since it's quite generic and other plugins also need it
|
||||||
|
# it was moved to the machine manager instead. Now this method just calls the machine manager.
|
||||||
|
self._application.getMachineManager().setActiveMachineExtruderCount(extruder_count)
|
||||||
|
|
||||||
|
# Function for the Machine Settings panel (QML) to update after the usre changes "Number of Extruders".
|
||||||
|
#
|
||||||
|
# fieldOfView: The Ultimaker 2 family (not 2+) does not have materials in Cura by default, because the material is
|
||||||
|
# to be set on the printer. But when switching to Marlin flavor, the printer firmware can not change/insert material
|
||||||
|
# settings on the fly so they need to be configured in Cura. So when switching between gcode flavors, materials may
|
||||||
|
# need to be enabled/disabled.
|
||||||
|
@pyqtSlot()
|
||||||
|
def updateHasMaterialsMetadata(self):
|
||||||
|
machine_manager = self._application.getMachineManager()
|
||||||
|
material_manager = self._application.getMaterialManager()
|
||||||
|
|
||||||
|
global_stack = machine_manager.activeMachine
|
||||||
|
|
||||||
|
definition = global_stack.definition
|
||||||
|
if definition.getProperty("machine_gcode_flavor", "value") != "UltiGCode" or definition.getMetaDataEntry(
|
||||||
|
"has_materials", False):
|
||||||
|
# In other words: only continue for the UM2 (extended), but not for the UM2+
|
||||||
|
return
|
||||||
|
|
||||||
|
extruder_positions = list(global_stack.extruders.keys())
|
||||||
|
has_materials = global_stack.getProperty("machine_gcode_flavor", "value") != "UltiGCode"
|
||||||
|
|
||||||
|
material_node = None
|
||||||
|
if has_materials:
|
||||||
|
global_stack.setMetaDataEntry("has_materials", True)
|
||||||
|
else:
|
||||||
|
# The metadata entry is stored in an ini, and ini files are parsed as strings only.
|
||||||
|
# Because any non-empty string evaluates to a boolean True, we have to remove the entry to make it False.
|
||||||
|
if "has_materials" in global_stack.getMetaData():
|
||||||
|
global_stack.removeMetaDataEntry("has_materials")
|
||||||
|
|
||||||
|
# set materials
|
||||||
|
for position in extruder_positions:
|
||||||
|
if has_materials:
|
||||||
|
material_node = material_manager.getDefaultMaterial(global_stack, position, None)
|
||||||
|
machine_manager.setMaterial(position, material_node)
|
||||||
|
|
||||||
|
self.forceUpdate()
|
@ -1,54 +1,64 @@
|
|||||||
# 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 collections import defaultdict
|
||||||
|
from typing import Dict
|
||||||
|
|
||||||
from PyQt5.QtCore import QTimer
|
from PyQt5.QtCore import QTimer
|
||||||
|
|
||||||
from UM.Application import Application
|
from UM.Application import Application
|
||||||
from UM.Qt.ListModel import ListModel
|
from UM.Qt.ListModel import ListModel
|
||||||
|
from UM.Scene.Camera import Camera
|
||||||
from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
|
from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
|
||||||
from UM.Scene.SceneNode import SceneNode
|
from UM.Scene.SceneNode import SceneNode
|
||||||
from UM.Scene.Selection import Selection
|
from UM.Scene.Selection import Selection
|
||||||
from UM.i18n import i18nCatalog
|
from UM.i18n import i18nCatalog
|
||||||
from collections import defaultdict
|
|
||||||
|
|
||||||
catalog = i18nCatalog("cura")
|
catalog = i18nCatalog("cura")
|
||||||
|
|
||||||
|
|
||||||
## Keep track of all objects in the project
|
## Keep track of all objects in the project
|
||||||
class ObjectsModel(ListModel):
|
class ObjectsModel(ListModel):
|
||||||
def __init__(self):
|
def __init__(self) -> None:
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
|
||||||
Application.getInstance().getController().getScene().sceneChanged.connect(self._updateDelayed)
|
Application.getInstance().getController().getScene().sceneChanged.connect(self._updateSceneDelayed)
|
||||||
Application.getInstance().getPreferences().preferenceChanged.connect(self._updateDelayed)
|
Application.getInstance().getPreferences().preferenceChanged.connect(self._updateDelayed)
|
||||||
|
|
||||||
self._update_timer = QTimer()
|
self._update_timer = QTimer()
|
||||||
self._update_timer.setInterval(100)
|
self._update_timer.setInterval(200)
|
||||||
self._update_timer.setSingleShot(True)
|
self._update_timer.setSingleShot(True)
|
||||||
self._update_timer.timeout.connect(self._update)
|
self._update_timer.timeout.connect(self._update)
|
||||||
|
|
||||||
self._build_plate_number = -1
|
self._build_plate_number = -1
|
||||||
|
|
||||||
def setActiveBuildPlate(self, nr):
|
def setActiveBuildPlate(self, nr: int) -> None:
|
||||||
self._build_plate_number = nr
|
if self._build_plate_number != nr:
|
||||||
self._update()
|
self._build_plate_number = nr
|
||||||
|
self._update()
|
||||||
|
|
||||||
def _updateDelayed(self, *args):
|
def _updateSceneDelayed(self, source) -> None:
|
||||||
|
if not isinstance(source, Camera):
|
||||||
|
self._update_timer.start()
|
||||||
|
|
||||||
|
def _updateDelayed(self, *args) -> None:
|
||||||
self._update_timer.start()
|
self._update_timer.start()
|
||||||
|
|
||||||
def _update(self, *args):
|
def _update(self, *args) -> None:
|
||||||
nodes = []
|
nodes = []
|
||||||
filter_current_build_plate = Application.getInstance().getPreferences().getValue("view/filter_current_build_plate")
|
filter_current_build_plate = Application.getInstance().getPreferences().getValue("view/filter_current_build_plate")
|
||||||
active_build_plate_number = self._build_plate_number
|
active_build_plate_number = self._build_plate_number
|
||||||
group_nr = 1
|
group_nr = 1
|
||||||
name_count_dict = defaultdict(int)
|
name_count_dict = defaultdict(int) # type: Dict[str, int]
|
||||||
|
|
||||||
for node in DepthFirstIterator(Application.getInstance().getController().getScene().getRoot()):
|
for node in DepthFirstIterator(Application.getInstance().getController().getScene().getRoot()): # type: ignore
|
||||||
if not isinstance(node, SceneNode):
|
if not isinstance(node, SceneNode):
|
||||||
continue
|
continue
|
||||||
if (not node.getMeshData() and not node.callDecoration("getLayerData")) and not node.callDecoration("isGroup"):
|
if (not node.getMeshData() and not node.callDecoration("getLayerData")) and not node.callDecoration("isGroup"):
|
||||||
continue
|
continue
|
||||||
if node.getParent() and node.getParent().callDecoration("isGroup"):
|
|
||||||
|
parent = node.getParent()
|
||||||
|
if parent and parent.callDecoration("isGroup"):
|
||||||
continue # Grouped nodes don't need resetting as their parent (the group) is resetted)
|
continue # Grouped nodes don't need resetting as their parent (the group) is resetted)
|
||||||
if not node.callDecoration("isSliceable") and not node.callDecoration("isGroup"):
|
if not node.callDecoration("isSliceable") and not node.callDecoration("isGroup"):
|
||||||
continue
|
continue
|
||||||
@ -64,7 +74,7 @@ class ObjectsModel(ListModel):
|
|||||||
group_nr += 1
|
group_nr += 1
|
||||||
|
|
||||||
if hasattr(node, "isOutsideBuildArea"):
|
if hasattr(node, "isOutsideBuildArea"):
|
||||||
is_outside_build_area = node.isOutsideBuildArea()
|
is_outside_build_area = node.isOutsideBuildArea() # type: ignore
|
||||||
else:
|
else:
|
||||||
is_outside_build_area = False
|
is_outside_build_area = False
|
||||||
|
|
@ -5,8 +5,7 @@ import json
|
|||||||
import math
|
import math
|
||||||
import os
|
import os
|
||||||
import unicodedata
|
import unicodedata
|
||||||
import re # To create abbreviations for printer names.
|
from typing import Dict, List, Optional, TYPE_CHECKING
|
||||||
from typing import Dict, List, Optional
|
|
||||||
|
|
||||||
from PyQt5.QtCore import QObject, pyqtSignal, pyqtProperty, pyqtSlot
|
from PyQt5.QtCore import QObject, pyqtSignal, pyqtProperty, pyqtSlot
|
||||||
|
|
||||||
@ -14,10 +13,7 @@ from UM.Logger import Logger
|
|||||||
from UM.Qt.Duration import Duration
|
from UM.Qt.Duration import Duration
|
||||||
from UM.Scene.SceneNode import SceneNode
|
from UM.Scene.SceneNode import SceneNode
|
||||||
from UM.i18n import i18nCatalog
|
from UM.i18n import i18nCatalog
|
||||||
from UM.MimeTypeDatabase import MimeTypeDatabase
|
from UM.MimeTypeDatabase import MimeTypeDatabase, MimeTypeNotFoundError
|
||||||
|
|
||||||
|
|
||||||
from typing import TYPE_CHECKING
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from cura.CuraApplication import CuraApplication
|
from cura.CuraApplication import CuraApplication
|
||||||
@ -70,10 +66,8 @@ class PrintInformation(QObject):
|
|||||||
self._application.getInstance().getPreferences().preferenceChanged.connect(self._onPreferencesChanged)
|
self._application.getInstance().getPreferences().preferenceChanged.connect(self._onPreferencesChanged)
|
||||||
|
|
||||||
self._multi_build_plate_model.activeBuildPlateChanged.connect(self._onActiveBuildPlateChanged)
|
self._multi_build_plate_model.activeBuildPlateChanged.connect(self._onActiveBuildPlateChanged)
|
||||||
|
|
||||||
self._onActiveMaterialsChanged()
|
|
||||||
|
|
||||||
self._material_amounts = [] # type: List[float]
|
self._material_amounts = [] # type: List[float]
|
||||||
|
self._onActiveMaterialsChanged()
|
||||||
|
|
||||||
def initializeCuraMessagePrintTimeProperties(self) -> None:
|
def initializeCuraMessagePrintTimeProperties(self) -> None:
|
||||||
self._current_print_time = {} # type: Dict[int, Duration]
|
self._current_print_time = {} # type: Dict[int, Duration]
|
||||||
@ -221,6 +215,7 @@ class PrintInformation(QObject):
|
|||||||
|
|
||||||
material_guid = material.getMetaDataEntry("GUID")
|
material_guid = material.getMetaDataEntry("GUID")
|
||||||
material_name = material.getName()
|
material_name = material.getName()
|
||||||
|
|
||||||
if material_guid in material_preference_values:
|
if material_guid in material_preference_values:
|
||||||
material_values = material_preference_values[material_guid]
|
material_values = material_preference_values[material_guid]
|
||||||
|
|
||||||
@ -361,7 +356,7 @@ class PrintInformation(QObject):
|
|||||||
try:
|
try:
|
||||||
mime_type = MimeTypeDatabase.getMimeTypeForFile(name)
|
mime_type = MimeTypeDatabase.getMimeTypeForFile(name)
|
||||||
data = mime_type.stripExtension(name)
|
data = mime_type.stripExtension(name)
|
||||||
except:
|
except MimeTypeNotFoundError:
|
||||||
Logger.log("w", "Unsupported Mime Type Database file extension %s", name)
|
Logger.log("w", "Unsupported Mime Type Database file extension %s", name)
|
||||||
|
|
||||||
if data is not None and check_name is not None:
|
if data is not None and check_name is not None:
|
||||||
@ -369,6 +364,16 @@ class PrintInformation(QObject):
|
|||||||
else:
|
else:
|
||||||
self._base_name = ""
|
self._base_name = ""
|
||||||
|
|
||||||
|
# Strip the old "curaproject" extension from the name
|
||||||
|
OLD_CURA_PROJECT_EXT = ".curaproject"
|
||||||
|
if self._base_name.lower().endswith(OLD_CURA_PROJECT_EXT):
|
||||||
|
self._base_name = self._base_name[:len(self._base_name) - len(OLD_CURA_PROJECT_EXT)]
|
||||||
|
|
||||||
|
# CURA-5896 Try to strip extra extensions with an infinite amount of ".curaproject.3mf".
|
||||||
|
OLD_CURA_PROJECT_3MF_EXT = ".curaproject.3mf"
|
||||||
|
while self._base_name.lower().endswith(OLD_CURA_PROJECT_3MF_EXT):
|
||||||
|
self._base_name = self._base_name[:len(self._base_name) - len(OLD_CURA_PROJECT_3MF_EXT)]
|
||||||
|
|
||||||
self._updateJobName()
|
self._updateJobName()
|
||||||
|
|
||||||
@pyqtProperty(str, fset = setBaseName, notify = baseNameChanged)
|
@pyqtProperty(str, fset = setBaseName, notify = baseNameChanged)
|
||||||
@ -385,28 +390,14 @@ class PrintInformation(QObject):
|
|||||||
return
|
return
|
||||||
active_machine_type_name = global_container_stack.definition.getName()
|
active_machine_type_name = global_container_stack.definition.getName()
|
||||||
|
|
||||||
abbr_machine = ""
|
self._abbr_machine = self._application.getMachineManager().getAbbreviatedMachineName(active_machine_type_name)
|
||||||
for word in re.findall(r"[\w']+", active_machine_type_name):
|
|
||||||
if word.lower() == "ultimaker":
|
|
||||||
abbr_machine += "UM"
|
|
||||||
elif word.isdigit():
|
|
||||||
abbr_machine += word
|
|
||||||
else:
|
|
||||||
stripped_word = self._stripAccents(word.upper())
|
|
||||||
# - use only the first character if the word is too long (> 3 characters)
|
|
||||||
# - use the whole word if it's not too long (<= 3 characters)
|
|
||||||
if len(stripped_word) > 3:
|
|
||||||
stripped_word = stripped_word[0]
|
|
||||||
abbr_machine += stripped_word
|
|
||||||
|
|
||||||
self._abbr_machine = abbr_machine
|
|
||||||
|
|
||||||
## Utility method that strips accents from characters (eg: â -> a)
|
## Utility method that strips accents from characters (eg: â -> a)
|
||||||
def _stripAccents(self, to_strip: str) -> str:
|
def _stripAccents(self, to_strip: str) -> str:
|
||||||
return ''.join(char for char in unicodedata.normalize('NFD', to_strip) if unicodedata.category(char) != 'Mn')
|
return ''.join(char for char in unicodedata.normalize('NFD', to_strip) if unicodedata.category(char) != 'Mn')
|
||||||
|
|
||||||
@pyqtSlot(result = "QVariantMap")
|
@pyqtSlot(result = "QVariantMap")
|
||||||
def getFeaturePrintTimes(self):
|
def getFeaturePrintTimes(self) -> Dict[str, Duration]:
|
||||||
result = {}
|
result = {}
|
||||||
if self._active_build_plate not in self._print_times_per_feature:
|
if self._active_build_plate not in self._print_times_per_feature:
|
||||||
self._initPrintTimesPerFeature(self._active_build_plate)
|
self._initPrintTimesPerFeature(self._active_build_plate)
|
69
cura/UI/TextManager.py
Normal file
69
cura/UI/TextManager.py
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
# Copyright (c) 2019 Ultimaker B.V.
|
||||||
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
|
import collections
|
||||||
|
from typing import Optional, Dict, List, cast
|
||||||
|
|
||||||
|
from PyQt5.QtCore import QObject, pyqtSlot
|
||||||
|
|
||||||
|
from UM.Resources import Resources
|
||||||
|
from UM.Version import Version
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# This manager provides means to load texts to QML.
|
||||||
|
#
|
||||||
|
class TextManager(QObject):
|
||||||
|
|
||||||
|
def __init__(self, parent: Optional["QObject"] = None) -> None:
|
||||||
|
super().__init__(parent)
|
||||||
|
|
||||||
|
self._change_log_text = ""
|
||||||
|
|
||||||
|
@pyqtSlot(result = str)
|
||||||
|
def getChangeLogText(self) -> str:
|
||||||
|
if not self._change_log_text:
|
||||||
|
self._change_log_text = self._loadChangeLogText()
|
||||||
|
return self._change_log_text
|
||||||
|
|
||||||
|
def _loadChangeLogText(self) -> str:
|
||||||
|
# Load change log texts and organize them with a dict
|
||||||
|
file_path = Resources.getPath(Resources.Texts, "change_log.txt")
|
||||||
|
change_logs_dict = {} # type: Dict[Version, Dict[str, List[str]]]
|
||||||
|
with open(file_path, "r", encoding = "utf-8") as f:
|
||||||
|
open_version = None # type: Optional[Version]
|
||||||
|
open_header = "" # Initialise to an empty header in case there is no "*" in the first line of the changelog
|
||||||
|
for line in f:
|
||||||
|
line = line.replace("\n", "")
|
||||||
|
if "[" in line and "]" in line:
|
||||||
|
line = line.replace("[", "")
|
||||||
|
line = line.replace("]", "")
|
||||||
|
open_version = Version(line)
|
||||||
|
if open_version > Version([14, 99, 99]): # Bit of a hack: We released the 15.x.x versions before 2.x
|
||||||
|
open_version = Version([0, open_version.getMinor(), open_version.getRevision(), open_version.getPostfixVersion()])
|
||||||
|
open_header = ""
|
||||||
|
change_logs_dict[open_version] = collections.OrderedDict()
|
||||||
|
elif line.startswith("*"):
|
||||||
|
open_header = line.replace("*", "")
|
||||||
|
change_logs_dict[cast(Version, open_version)][open_header] = []
|
||||||
|
elif line != "":
|
||||||
|
if open_header not in change_logs_dict[cast(Version, open_version)]:
|
||||||
|
change_logs_dict[cast(Version, open_version)][open_header] = []
|
||||||
|
change_logs_dict[cast(Version, open_version)][open_header].append(line)
|
||||||
|
|
||||||
|
# Format changelog text
|
||||||
|
content = ""
|
||||||
|
for version in sorted(change_logs_dict.keys(), reverse = True):
|
||||||
|
text_version = version
|
||||||
|
if version < Version([1, 0, 0]): # Bit of a hack: We released the 15.x.x versions before 2.x
|
||||||
|
text_version = Version([15, version.getMinor(), version.getRevision(), version.getPostfixVersion()])
|
||||||
|
content += "<h1>" + str(text_version) + "</h1><br>"
|
||||||
|
content += ""
|
||||||
|
for change in change_logs_dict[version]:
|
||||||
|
if str(change) != "":
|
||||||
|
content += "<b>" + str(change) + "</b><br>"
|
||||||
|
for line in change_logs_dict[version][change]:
|
||||||
|
content += str(line) + "<br>"
|
||||||
|
content += "<br>"
|
||||||
|
|
||||||
|
return content
|
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