mirror of
https://github.com/Ultimaker/Cura.git
synced 2025-07-06 22:47:29 -06:00
Merge branch 'master' into master-1
This commit is contained in:
commit
849ac3551d
2286 changed files with 195832 additions and 76181 deletions
48
.github/ISSUE_TEMPLATE.md
vendored
48
.github/ISSUE_TEMPLATE.md
vendored
|
@ -1,33 +1,49 @@
|
|||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us fix issues.
|
||||
title: ''
|
||||
labels: 'Type: Bug'
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
<!--
|
||||
The following template is useful for filing new issues. Processing an issue will go much faster when this is filled out, and issues which do not use this template WILL BE REMOVED.
|
||||
Processing an issue will go much faster when this is filled out, and issues which do not use this template WILL BE REMOVED and no fix will be considered!
|
||||
|
||||
Before filing, PLEASE check if the issue already exists (either open or closed) by using the search bar on the issues page. If it does, comment there. Even if it's closed, we can reopen it based on your comment.
|
||||
|
||||
Also, please note the application version in the title of the issue. For example: "[3.2.1] Cannot connect to 3rd-party printer". Please do not write things like "Request:" or "[BUG]" in the title; this is what labels are for.
|
||||
|
||||
It is also helpful to attach a project (.3mf or .curaproject) file and Cura log file so we can debug issues quicker.
|
||||
Information about how to find the log file can be found at https://github.com/Ultimaker/Cura/wiki/Cura-Preferences-and-Settings-Locations. To upload a project, try changing the extension to e.g. .curaproject.3mf.zip so that github accepts uploading the file. Otherwise we recommend http://wetransfer.com, but other file hosts like Google Drive or Dropbox work well too.
|
||||
Also, please note the application version in the title of the issue. For example: "[3.2.1] Cannot connect to 3rd-party printer". Please do NOT write things like "Request:" or "[BUG]" in the title; this is what labels are for.
|
||||
|
||||
Thank you for using Cura!
|
||||
-->
|
||||
|
||||
**Application Version**
|
||||
<!-- The version of the application this issue occurs with -->
|
||||
**Application version**
|
||||
(The version of the application this issue occurs with.)
|
||||
|
||||
**Platform**
|
||||
<!-- Information about the operating system the issue occurs on -->
|
||||
(Information about the operating system the issue occurs on. Include at least the operating system and maybe GPU.)
|
||||
|
||||
**Printer**
|
||||
<!-- Which printer was selected in Cura. Please attach project file as .curaproject.3mf.zip -->
|
||||
(Which printer was selected in Cura?)
|
||||
|
||||
**Steps to Reproduce**
|
||||
<!-- Add the steps needed that lead up to the issue -->
|
||||
**Reproduction steps**
|
||||
1. (Something you did.)
|
||||
2. (Something you did next.)
|
||||
|
||||
**Actual Results**
|
||||
<!-- What happens after the above steps have been followed -->
|
||||
**Screenshot(s)**
|
||||
(Image showing the problem, perhaps before/after images.)
|
||||
|
||||
**Actual results**
|
||||
(What happens after the above steps have been followed.)
|
||||
|
||||
**Expected results**
|
||||
<!-- What should happen after the above steps have been followed -->
|
||||
(What should happen after the above steps have been followed.)
|
||||
|
||||
**Additional Information**
|
||||
<!-- Extra information relevant to the issue, like screenshots -->
|
||||
**Project file**
|
||||
(For slicing bugs, provide a project which clearly shows the bug, by going to File->Save. For big files you may need to use WeTransfer or similar file sharing sites.)
|
||||
|
||||
**Log file**
|
||||
(See https://github.com/Ultimaker/Cura#logging-issues to find the log file to upload, or copy a relevant snippet from it.)
|
||||
|
||||
**Additional information**
|
||||
(Extra information relevant to the issue.)
|
||||
|
|
49
.github/ISSUE_TEMPLATE/bug-report.md
vendored
Normal file
49
.github/ISSUE_TEMPLATE/bug-report.md
vendored
Normal file
|
@ -0,0 +1,49 @@
|
|||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us fix issues.
|
||||
title: ''
|
||||
labels: 'Type: Bug'
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
<!--
|
||||
Processing an issue will go much faster when this is filled out, and issues which do not use this template WILL BE REMOVED and no fix will be considered!
|
||||
|
||||
Before filing, PLEASE check if the issue already exists (either open or closed) by using the search bar on the issues page. If it does, comment there. Even if it's closed, we can reopen it based on your comment.
|
||||
|
||||
Also, please note the application version in the title of the issue. For example: "[3.2.1] Cannot connect to 3rd-party printer". Please do NOT write things like "Request:" or "[BUG]" in the title; this is what labels are for.
|
||||
|
||||
Thank you for using Cura!
|
||||
-->
|
||||
|
||||
**Application version**
|
||||
(The version of the application this issue occurs with.)
|
||||
|
||||
**Platform**
|
||||
(Information about the operating system the issue occurs on. Include at least the operating system and maybe GPU.)
|
||||
|
||||
**Printer**
|
||||
(Which printer was selected in Cura?)
|
||||
|
||||
**Reproduction steps**
|
||||
1. (Something you did.)
|
||||
2. (Something you did next.)
|
||||
|
||||
**Screenshot(s)**
|
||||
(Image showing the problem, perhaps before/after images.)
|
||||
|
||||
**Actual results**
|
||||
(What happens after the above steps have been followed.)
|
||||
|
||||
**Expected results**
|
||||
(What should happen after the above steps have been followed.)
|
||||
|
||||
**Project file**
|
||||
(For slicing bugs, provide a project which clearly shows the bug, by going to File->Save. For big files you may need to use WeTransfer or similar file sharing sites.)
|
||||
|
||||
**Log file**
|
||||
(See https://github.com/Ultimaker/Cura#logging-issues to find the log file to upload, or copy a relevant snippet from it.)
|
||||
|
||||
**Additional information**
|
||||
(Extra information relevant to the issue.)
|
23
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
23
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
|
@ -0,0 +1,23 @@
|
|||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: 'Type: New Feature'
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
(A clear and concise description of what the problem is. Ex. I'm always frustrated when [...])
|
||||
|
||||
**Describe the solution you'd like**
|
||||
(A clear and concise description of what you want to happen. If possible, describe why you think this is a good solution.)
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
(A clear and concise description of any alternative solutions or features you've considered. Again, if possible, think about why these alternatives are not working out.)
|
||||
|
||||
**Affected users and/or printers**
|
||||
(Who do you think will benefit from this? Is everyone going to benefit from these changes? Or specific kinds of users?)
|
||||
|
||||
**Additional context**
|
||||
(Add any other context or screenshots about the feature request here.)
|
13
.github/workflows/cicd.yml
vendored
Normal file
13
.github/workflows/cicd.yml
vendored
Normal file
|
@ -0,0 +1,13 @@
|
|||
---
|
||||
name: CI/CD
|
||||
on: [push, pull_request]
|
||||
jobs:
|
||||
build:
|
||||
name: Build and test
|
||||
runs-on: ubuntu-latest
|
||||
container: ultimaker/cura-build-environment
|
||||
steps:
|
||||
- name: Checkout master
|
||||
uses: actions/checkout@v1.2.0
|
||||
- name: Build and test
|
||||
run: docker/build.sh
|
7
.gitignore
vendored
7
.gitignore
vendored
|
@ -35,7 +35,7 @@ cura.desktop
|
|||
.pydevproject
|
||||
.settings
|
||||
|
||||
#Externally located plug-ins.
|
||||
#Externally located plug-ins commonly installed by our devs.
|
||||
plugins/cura-big-flame-graph
|
||||
plugins/cura-god-mode-plugin
|
||||
plugins/cura-siemensnx-plugin
|
||||
|
@ -52,6 +52,7 @@ plugins/FlatProfileExporter
|
|||
plugins/GodMode
|
||||
plugins/OctoPrintPlugin
|
||||
plugins/ProfileFlattener
|
||||
plugins/SettingsGuide
|
||||
plugins/X3GWriter
|
||||
|
||||
#Build stuff
|
||||
|
@ -71,3 +72,7 @@ run.sh
|
|||
.scannerwork/
|
||||
CuraEngine
|
||||
|
||||
/.coverage
|
||||
|
||||
#Prevents import failures when plugin running tests
|
||||
plugins/__init__.py
|
||||
|
|
16
.gitlab-ci.yml
Normal file
16
.gitlab-ci.yml
Normal file
|
@ -0,0 +1,16 @@
|
|||
image: registry.gitlab.com/ultimaker/cura/cura-build-environment:centos7
|
||||
|
||||
stages:
|
||||
- build
|
||||
|
||||
build and test linux:
|
||||
stage: build
|
||||
tags:
|
||||
- cura
|
||||
- docker
|
||||
- linux
|
||||
script:
|
||||
- docker/build.sh
|
||||
artifacts:
|
||||
paths:
|
||||
- build
|
|
@ -1,11 +1,10 @@
|
|||
project(cura NONE)
|
||||
cmake_minimum_required(VERSION 2.8.12)
|
||||
|
||||
set(CMAKE_MODULE_PATH ${CMAKE_SOURCE_DIR}/cmake/
|
||||
${CMAKE_MODULE_PATH})
|
||||
project(cura)
|
||||
cmake_minimum_required(VERSION 3.6)
|
||||
|
||||
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_SCRIPTS_DIR "${URANIUM_DIR}/scripts" CACHE DIRECTORY "The location of the scripts directory of the Uranium repository")
|
||||
|
||||
|
@ -21,15 +20,36 @@ set(CURA_APP_NAME "cura" CACHE STRING "Short name of Cura, used for configuratio
|
|||
set(CURA_APP_DISPLAY_NAME "Ultimaker Cura" CACHE STRING "Display name of Cura")
|
||||
set(CURA_VERSION "master" CACHE STRING "Version name of Cura")
|
||||
set(CURA_BUILDTYPE "" CACHE STRING "Build type of Cura, eg. 'PPA'")
|
||||
set(CURA_SDK_VERSION "" CACHE STRING "SDK version of Cura")
|
||||
set(CURA_CLOUD_API_ROOT "" CACHE STRING "Alternative Cura cloud API root")
|
||||
set(CURA_CLOUD_API_VERSION "" CACHE STRING "Alternative Cura cloud API version")
|
||||
set(CURA_CLOUD_ACCOUNT_API_ROOT "" CACHE STRING "Alternative Cura cloud account API version")
|
||||
|
||||
configure_file(${CMAKE_SOURCE_DIR}/cura.desktop.in ${CMAKE_BINARY_DIR}/cura.desktop @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 "")
|
||||
set(CMAKE_MODULE_PATH "${URANIUM_DIR}/cmake")
|
||||
set(CMAKE_MODULE_PATH "${CMAKE_MODULE_PATH};${URANIUM_DIR}/cmake")
|
||||
endif()
|
||||
if(NOT ${URANIUM_SCRIPTS_DIR} STREQUAL "")
|
||||
list(APPEND CMAKE_MODULE_PATH ${URANIUM_DIR}/cmake)
|
||||
|
@ -40,12 +60,12 @@ if(NOT ${URANIUM_SCRIPTS_DIR} STREQUAL "")
|
|||
CREATE_TRANSLATION_TARGETS()
|
||||
endif()
|
||||
|
||||
find_package(PythonInterp 3.5.0 REQUIRED)
|
||||
|
||||
install(DIRECTORY resources
|
||||
DESTINATION ${CMAKE_INSTALL_DATADIR}/cura)
|
||||
install(DIRECTORY plugins
|
||||
DESTINATION lib${LIB_SUFFIX}/cura)
|
||||
|
||||
include(CuraPluginInstall)
|
||||
|
||||
if(NOT APPLE AND NOT WIN32)
|
||||
install(FILES cura_app.py
|
||||
DESTINATION ${CMAKE_INSTALL_BINDIR}
|
||||
|
@ -53,16 +73,16 @@ if(NOT APPLE AND NOT WIN32)
|
|||
RENAME cura)
|
||||
if(EXISTS /etc/debian_version)
|
||||
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)
|
||||
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()
|
||||
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)
|
||||
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()
|
||||
install(FILES ${CMAKE_BINARY_DIR}/cura.desktop
|
||||
DESTINATION ${CMAKE_INSTALL_DATADIR}/applications)
|
||||
|
@ -78,8 +98,8 @@ else()
|
|||
DESTINATION ${CMAKE_INSTALL_BINDIR}
|
||||
PERMISSIONS OWNER_READ OWNER_WRITE OWNER_EXECUTE GROUP_READ GROUP_EXECUTE WORLD_READ WORLD_EXECUTE)
|
||||
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)
|
||||
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()
|
||||
|
|
99
cmake/CuraPluginInstall.cmake
Normal file
99
cmake/CuraPluginInstall.cmake
Normal file
|
@ -0,0 +1,99 @@
|
|||
# Copyright (c) 2019 Ultimaker B.V.
|
||||
# CuraPluginInstall.cmake is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
#
|
||||
# This module detects all plugins that need to be installed and adds them using the CMake install() command.
|
||||
# It detects all plugin folder in the path "plugins/*" where there's a "plugin.json" in it.
|
||||
#
|
||||
# Plugins can be configured to NOT BE INSTALLED via the variable "CURA_NO_INSTALL_PLUGINS" as a list of string in the
|
||||
# form of "a;b;c" or "a,b,c". By default all plugins will be installed.
|
||||
#
|
||||
|
||||
# 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)
|
||||
endif()
|
||||
|
||||
# Options or configuration variables
|
||||
set(CURA_NO_INSTALL_PLUGINS "" CACHE STRING "A list of plugins that should not be installed, separated with ';' or ','.")
|
||||
|
||||
file(GLOB_RECURSE _plugin_json_list ${CMAKE_SOURCE_DIR}/plugins/*/plugin.json)
|
||||
list(LENGTH _plugin_json_list _plugin_json_list_len)
|
||||
|
||||
# Sort the lists alphabetically so we can handle cases like this:
|
||||
# - plugins/my_plugin/plugin.json
|
||||
# - plugins/my_plugin/my_module/plugin.json
|
||||
# In this case, only "plugins/my_plugin" should be added via install().
|
||||
set(_no_install_plugin_list ${CURA_NO_INSTALL_PLUGINS})
|
||||
# Sanitize the string so the comparison will be case-insensitive.
|
||||
string(STRIP "${_no_install_plugin_list}" _no_install_plugin_list)
|
||||
string(TOLOWER "${_no_install_plugin_list}" _no_install_plugin_list)
|
||||
|
||||
# WORKAROUND counterpart of what's in cura-build.
|
||||
string(REPLACE "," ";" _no_install_plugin_list "${_no_install_plugin_list}")
|
||||
|
||||
list(LENGTH _no_install_plugin_list _no_install_plugin_list_len)
|
||||
|
||||
if(_no_install_plugin_list_len GREATER 0)
|
||||
list(SORT _no_install_plugin_list)
|
||||
endif()
|
||||
if(_plugin_json_list_len GREATER 0)
|
||||
list(SORT _plugin_json_list)
|
||||
endif()
|
||||
|
||||
# Check all plugin directories and add them via install() if needed.
|
||||
set(_install_plugin_list "")
|
||||
foreach(_plugin_json_path ${_plugin_json_list})
|
||||
get_filename_component(_plugin_dir ${_plugin_json_path} DIRECTORY)
|
||||
file(RELATIVE_PATH _rel_plugin_dir ${CMAKE_CURRENT_SOURCE_DIR} ${_plugin_dir})
|
||||
get_filename_component(_plugin_dir_name ${_plugin_dir} NAME)
|
||||
|
||||
# Make plugin name comparison case-insensitive
|
||||
string(TOLOWER "${_plugin_dir_name}" _plugin_dir_name_lowercase)
|
||||
|
||||
# Check if this plugin needs to be skipped for installation
|
||||
set(_add_plugin ON) # Indicates if this plugin should be added to the build or not.
|
||||
set(_is_no_install_plugin OFF) # If this plugin will not be added, this indicates if it's because the plugin is
|
||||
# specified in the NO_INSTALL_PLUGINS list.
|
||||
if(_no_install_plugin_list)
|
||||
if("${_plugin_dir_name_lowercase}" IN_LIST _no_install_plugin_list)
|
||||
set(_add_plugin OFF)
|
||||
set(_is_no_install_plugin ON)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
# Make sure this is not a subdirectory in a plugin that's already in the install list
|
||||
if(_add_plugin)
|
||||
foreach(_known_install_plugin_dir ${_install_plugin_list})
|
||||
if(_plugin_dir MATCHES "${_known_install_plugin_dir}.+")
|
||||
set(_add_plugin OFF)
|
||||
break()
|
||||
endif()
|
||||
endforeach()
|
||||
endif()
|
||||
|
||||
if(_add_plugin)
|
||||
message(STATUS "[+] PLUGIN TO INSTALL: ${_rel_plugin_dir}")
|
||||
get_filename_component(_rel_plugin_parent_dir ${_rel_plugin_dir} DIRECTORY)
|
||||
install(DIRECTORY ${_rel_plugin_dir}
|
||||
DESTINATION lib${LIB_SUFFIX}/cura/${_rel_plugin_parent_dir}
|
||||
PATTERN "__pycache__" EXCLUDE
|
||||
PATTERN "*.qmlc" EXCLUDE
|
||||
)
|
||||
list(APPEND _install_plugin_list ${_plugin_dir})
|
||||
elseif(_is_no_install_plugin)
|
||||
message(STATUS "[-] PLUGIN TO REMOVE : ${_rel_plugin_dir}")
|
||||
execute_process(COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/cmake/mod_bundled_packages_json.py
|
||||
-d ${CMAKE_CURRENT_SOURCE_DIR}/resources/bundled_packages
|
||||
${_plugin_dir_name}
|
||||
RESULT_VARIABLE _mod_json_result)
|
||||
endif()
|
||||
endforeach()
|
|
@ -1,10 +1,21 @@
|
|||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
enable_testing()
|
||||
include(CTest)
|
||||
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)
|
||||
|
||||
|
@ -36,7 +47,7 @@ function(cura_add_test)
|
|||
if (NOT ${test_exists})
|
||||
add_test(
|
||||
NAME ${_NAME}
|
||||
COMMAND ${PYTHON_EXECUTABLE} -m pytest --verbose --full-trace --capture=no --no-print-log --junitxml=${CMAKE_BINARY_DIR}/junit-${_NAME}.xml ${_DIRECTORY}
|
||||
COMMAND ${Python3_EXECUTABLE} -m pytest --junitxml=${CMAKE_BINARY_DIR}/junit-${_NAME}.xml ${_DIRECTORY}
|
||||
)
|
||||
set_tests_properties(${_NAME} PROPERTIES ENVIRONMENT LANG=C)
|
||||
set_tests_properties(${_NAME} PROPERTIES ENVIRONMENT "PYTHONPATH=${_PYTHONPATH}")
|
||||
|
@ -59,13 +70,13 @@ endforeach()
|
|||
#Add code style test.
|
||||
add_test(
|
||||
NAME "code-style"
|
||||
COMMAND ${PYTHON_EXECUTABLE} run_mypy.py
|
||||
COMMAND ${Python3_EXECUTABLE} run_mypy.py
|
||||
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
|
||||
)
|
||||
|
||||
#Add test for whether the shortcut alt-keys are unique in every translation.
|
||||
add_test(
|
||||
NAME "shortcut-keys"
|
||||
COMMAND ${PYTHON_EXECUTABLE} scripts/check_shortcut_keys.py
|
||||
COMMAND ${Python3_EXECUTABLE} scripts/check_shortcut_keys.py
|
||||
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
|
||||
)
|
69
cmake/mod_bundled_packages_json.py
Executable file
69
cmake/mod_bundled_packages_json.py
Executable file
|
@ -0,0 +1,69 @@
|
|||
#!/usr/bin/env python3
|
||||
#
|
||||
# This script removes the given package entries in the bundled_packages JSON files. This is used by the PluginInstall
|
||||
# CMake module.
|
||||
#
|
||||
|
||||
import argparse
|
||||
import collections
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
|
||||
|
||||
## Finds all JSON files in the given directory recursively and returns a list of those files in absolute paths.
|
||||
#
|
||||
# \param work_dir The directory to look for JSON files recursively.
|
||||
# \return A list of JSON files in absolute paths that are found in the given directory.
|
||||
def find_json_files(work_dir: str) -> list:
|
||||
json_file_list = []
|
||||
for root, dir_names, file_names in os.walk(work_dir):
|
||||
for file_name in file_names:
|
||||
abs_path = os.path.abspath(os.path.join(root, file_name))
|
||||
json_file_list.append(abs_path)
|
||||
return json_file_list
|
||||
|
||||
|
||||
## Removes the given entries from the given JSON file. The file will modified in-place.
|
||||
#
|
||||
# \param file_path The JSON file to modify.
|
||||
# \param entries A list of strings as entries to remove.
|
||||
# \return None
|
||||
def remove_entries_from_json_file(file_path: str, entries: list) -> None:
|
||||
try:
|
||||
with open(file_path, "r", encoding = "utf-8") as f:
|
||||
package_dict = json.load(f, object_hook = collections.OrderedDict)
|
||||
except Exception as e:
|
||||
msg = "Failed to load '{file_path}' as a JSON file. This file will be ignored Exception: {e}"\
|
||||
.format(file_path = file_path, e = e)
|
||||
sys.stderr.write(msg + os.linesep)
|
||||
return
|
||||
|
||||
for entry in entries:
|
||||
if entry in package_dict:
|
||||
del package_dict[entry]
|
||||
print("[INFO] Remove entry [{entry}] from [{file_path}]".format(file_path = file_path, entry = entry))
|
||||
|
||||
try:
|
||||
with open(file_path, "w", encoding = "utf-8", newline = "\n") as f:
|
||||
json.dump(package_dict, f, indent = 4)
|
||||
except Exception as e:
|
||||
msg = "Failed to write '{file_path}' as a JSON file. Exception: {e}".format(file_path = file_path, e = e)
|
||||
raise IOError(msg)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser("mod_bundled_packages_json")
|
||||
parser.add_argument("-d", "--dir", dest = "work_dir",
|
||||
help = "The directory to look for bundled packages JSON files, recursively.")
|
||||
parser.add_argument("entries", metavar = "ENTRIES", type = str, nargs = "+")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
json_file_list = find_json_files(args.work_dir)
|
||||
for json_file_path in json_file_list:
|
||||
remove_entries_from_json_file(json_file_path, args.entries)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
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,7 @@ TryExec=@CMAKE_INSTALL_FULL_BINDIR@/cura
|
|||
Icon=cura-icon
|
||||
Terminal=false
|
||||
Type=Application
|
||||
MimeType=model/stl;application/vnd.ms-3mfdocument;application/prs.wavefront-obj;image/bmp;image/gif;image/jpeg;image/png;model/x3d+xml;text/x-gcode;
|
||||
MimeType=model/stl;application/vnd.ms-3mfdocument;application/prs.wavefront-obj;image/bmp;image/gif;image/jpeg;image/png;text/x-gcode;application/x-amf;application/x-ply;application/x-ctm;model/vnd.collada+xml;model/gltf-binary;model/gltf+json;model/vnd.collada+xml+zip;
|
||||
Categories=Graphics;
|
||||
Keywords=3D;Printing;Slicer;
|
||||
StartupWMClass=cura.real
|
||||
|
|
|
@ -29,6 +29,7 @@ i18n_catalog = i18nCatalog("cura")
|
|||
class Account(QObject):
|
||||
# Signal emitted when user logged in or out.
|
||||
loginStateChanged = pyqtSignal(bool)
|
||||
accessTokenChanged = pyqtSignal()
|
||||
|
||||
def __init__(self, application: "CuraApplication", parent = None) -> None:
|
||||
super().__init__(parent)
|
||||
|
@ -59,8 +60,12 @@ class Account(QObject):
|
|||
self._authorization_service.initialize(self._application.getPreferences())
|
||||
self._authorization_service.onAuthStateChanged.connect(self._onLoginStateChanged)
|
||||
self._authorization_service.onAuthenticationError.connect(self._onLoginStateChanged)
|
||||
self._authorization_service.accessTokenChanged.connect(self._onAccessTokenChanged)
|
||||
self._authorization_service.loadAuthDataFromPreferences()
|
||||
|
||||
def _onAccessTokenChanged(self):
|
||||
self.accessTokenChanged.emit()
|
||||
|
||||
## Returns a boolean indicating whether the given authentication is applied against staging or not.
|
||||
@property
|
||||
def is_staging(self) -> bool:
|
||||
|
@ -76,6 +81,9 @@ class Account(QObject):
|
|||
self._error_message.hide()
|
||||
self._error_message = Message(error_message, title = i18n_catalog.i18nc("@info:title", "Login failed"))
|
||||
self._error_message.show()
|
||||
self._logged_in = False
|
||||
self.loginStateChanged.emit(False)
|
||||
return
|
||||
|
||||
if self._logged_in != logged_in:
|
||||
self._logged_in = logged_in
|
||||
|
@ -102,7 +110,7 @@ class Account(QObject):
|
|||
return None
|
||||
return user_profile.profile_image_url
|
||||
|
||||
@pyqtProperty(str, notify=loginStateChanged)
|
||||
@pyqtProperty(str, notify=accessTokenChanged)
|
||||
def accessToken(self) -> Optional[str]:
|
||||
return self._authorization_service.getAccessToken()
|
||||
|
||||
|
|
|
@ -9,7 +9,11 @@ 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"
|
||||
|
||||
# Each release has a fixed SDK version coupled with it. It doesn't make sense to make it configurable because, for
|
||||
# example Cura 3.2 with SDK version 6.1 will not work. So the SDK version is hard-coded here and left out of the
|
||||
# CuraVersion.py.in template.
|
||||
CuraSDKVersion = "7.0.0"
|
||||
|
||||
try:
|
||||
from cura.CuraVersion import CuraAppName # type: ignore
|
||||
|
@ -32,6 +36,9 @@ try:
|
|||
except ImportError:
|
||||
CuraVersion = DEFAULT_CURA_VERSION # [CodeStyle: Reflecting imported value]
|
||||
|
||||
# CURA-6569
|
||||
# This string indicates what type of version it is. For example, "enterprise". By default it's empty which indicates
|
||||
# a default/normal Cura build.
|
||||
try:
|
||||
from cura.CuraVersion import CuraBuildType # type: ignore
|
||||
except ImportError:
|
||||
|
@ -42,9 +49,7 @@ try:
|
|||
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
|
||||
# CURA-6569
|
||||
# Various convenience flags indicating what kind of Cura build it is.
|
||||
__ENTERPRISE_VERSION_TYPE = "enterprise"
|
||||
IsEnterpriseVersion = CuraBuildType.lower() == __ENTERPRISE_VERSION_TYPE
|
||||
|
|
|
@ -217,11 +217,6 @@ class Arrange:
|
|||
prio_slice = self._priority[min_y:max_y, min_x:max_x]
|
||||
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
|
||||
def isEmpty(self):
|
||||
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.
|
||||
|
||||
from UM.Application import Application
|
||||
|
@ -48,7 +48,6 @@ class ArrangeArray:
|
|||
return self._count
|
||||
|
||||
def get(self, index):
|
||||
print(self._arrange)
|
||||
return self._arrange[index]
|
||||
|
||||
def getFirstEmpty(self):
|
||||
|
|
|
@ -1,12 +1,19 @@
|
|||
# Copyright (c) 2019 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
import numpy
|
||||
import copy
|
||||
from typing import Optional, Tuple, TYPE_CHECKING
|
||||
|
||||
from UM.Math.Polygon import Polygon
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from UM.Scene.SceneNode import SceneNode
|
||||
|
||||
|
||||
## Polygon representation as an array for use with Arrange
|
||||
class ShapeArray:
|
||||
def __init__(self, arr, offset_x, offset_y, scale = 1):
|
||||
def __init__(self, arr: numpy.array, offset_x: float, offset_y: float, scale: float = 1) -> None:
|
||||
self.arr = arr
|
||||
self.offset_x = offset_x
|
||||
self.offset_y = offset_y
|
||||
|
@ -16,7 +23,7 @@ class ShapeArray:
|
|||
# \param vertices
|
||||
# \param scale scale the coordinates
|
||||
@classmethod
|
||||
def fromPolygon(cls, vertices, scale = 1):
|
||||
def fromPolygon(cls, vertices: numpy.array, scale: float = 1) -> "ShapeArray":
|
||||
# scale
|
||||
vertices = vertices * scale
|
||||
# flip y, x -> x, y
|
||||
|
@ -42,7 +49,7 @@ class ShapeArray:
|
|||
# \param min_offset offset for the offset ShapeArray
|
||||
# \param scale scale the coordinates
|
||||
@classmethod
|
||||
def fromNode(cls, node, min_offset, scale = 0.5, include_children = False):
|
||||
def fromNode(cls, node: "SceneNode", min_offset: float, scale: float = 0.5, include_children: bool = False) -> Tuple[Optional["ShapeArray"], Optional["ShapeArray"]]:
|
||||
transform = node._transformation
|
||||
transform_x = transform._data[0][3]
|
||||
transform_y = transform._data[2][3]
|
||||
|
@ -88,14 +95,16 @@ class ShapeArray:
|
|||
# \param shape numpy format shape, [x-size, y-size]
|
||||
# \param vertices
|
||||
@classmethod
|
||||
def arrayFromPolygon(cls, shape, vertices):
|
||||
def arrayFromPolygon(cls, shape: Tuple[int, int], vertices: numpy.array) -> numpy.array:
|
||||
base_array = numpy.zeros(shape, dtype = numpy.int32) # Initialize your array of zeros
|
||||
|
||||
fill = numpy.ones(base_array.shape) * True # Initialize boolean array defining shape fill
|
||||
|
||||
# Create check array for each edge segment, combine into fill array
|
||||
for k in range(vertices.shape[0]):
|
||||
fill = numpy.all([fill, cls._check(vertices[k - 1], vertices[k], base_array)], axis=0)
|
||||
check_array = cls._check(vertices[k - 1], vertices[k], base_array)
|
||||
if check_array is not None:
|
||||
fill = numpy.all([fill, check_array], axis=0)
|
||||
|
||||
# Set all values inside polygon to one
|
||||
base_array[fill] = 1
|
||||
|
@ -111,9 +120,9 @@ class ShapeArray:
|
|||
# \param p2 2-tuple with x, y for point 2
|
||||
# \param base_array boolean array to project the line on
|
||||
@classmethod
|
||||
def _check(cls, p1, p2, base_array):
|
||||
def _check(cls, p1: numpy.array, p2: numpy.array, base_array: numpy.array) -> Optional[numpy.array]:
|
||||
if p1[0] == p2[0] and p1[1] == p2[1]:
|
||||
return
|
||||
return None
|
||||
idxs = numpy.indices(base_array.shape) # Create 3D array of indices
|
||||
|
||||
p1 = p1.astype(float)
|
||||
|
@ -132,4 +141,3 @@ class ShapeArray:
|
|||
max_col_idx = (idxs[0] - p1[0]) / (p2[0] - p1[0]) * (p2[1] - p1[1]) + p1[1]
|
||||
sign = numpy.sign(p2[0] - p1[0])
|
||||
return idxs[1] * sign <= max_col_idx * sign
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
# Copyright (c) 2016 Ultimaker B.V.
|
||||
# Copyright (c) 2019 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
from PyQt5.QtCore import QTimer
|
||||
|
@ -16,9 +16,10 @@ class AutoSave:
|
|||
self._application.getPreferences().addPreference("cura/autosave_delay", 1000 * 10)
|
||||
|
||||
self._change_timer = QTimer()
|
||||
self._change_timer.setInterval(self._application.getPreferences().getValue("cura/autosave_delay"))
|
||||
self._change_timer.setInterval(int(self._application.getPreferences().getValue("cura/autosave_delay")))
|
||||
self._change_timer.setSingleShot(True)
|
||||
|
||||
self._enabled = True
|
||||
self._saving = False
|
||||
|
||||
def initialize(self):
|
||||
|
@ -32,6 +33,13 @@ class AutoSave:
|
|||
if not self._saving:
|
||||
self._change_timer.start()
|
||||
|
||||
def setEnabled(self, enabled: bool) -> None:
|
||||
self._enabled = enabled
|
||||
if self._enabled:
|
||||
self._change_timer.start()
|
||||
else:
|
||||
self._change_timer.stop()
|
||||
|
||||
def _onGlobalStackChanged(self):
|
||||
if self._global_stack:
|
||||
self._global_stack.propertyChanged.disconnect(self._triggerTimer)
|
||||
|
|
|
@ -116,12 +116,13 @@ class Backup:
|
|||
|
||||
current_version = self._application.getVersion()
|
||||
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.
|
||||
# Restoring this will cause a lot of issues so we don't allow this for now.
|
||||
|
||||
if current_version < version_to_restore:
|
||||
# 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.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
|
||||
|
||||
version_data_dir = Resources.getDataStoragePath()
|
||||
|
@ -147,5 +148,9 @@ class Backup:
|
|||
Logger.log("d", "Removing current data in location: %s", target_path)
|
||||
Resources.factoryReset()
|
||||
Logger.log("d", "Extracting backup to location: %s", target_path)
|
||||
try:
|
||||
archive.extractall(target_path)
|
||||
except PermissionError:
|
||||
Logger.logException("e", "Unable to extract the backup due to permission errors")
|
||||
return False
|
||||
return True
|
||||
|
|
|
@ -51,8 +51,18 @@ class BackupsManager:
|
|||
## Here we try to disable the auto-save plug-in as it might interfere with
|
||||
# restoring a back-up.
|
||||
def _disableAutoSave(self) -> None:
|
||||
self._application.setSaveDataEnabled(False)
|
||||
auto_save = self._application.getAutoSave()
|
||||
# The auto save is only not created if the application has not yet started.
|
||||
if auto_save:
|
||||
auto_save.setEnabled(False)
|
||||
else:
|
||||
Logger.log("e", "Unable to disable the autosave as application init has not been completed")
|
||||
|
||||
## Re-enable auto-save after we're done.
|
||||
def _enableAutoSave(self) -> None:
|
||||
self._application.setSaveDataEnabled(True)
|
||||
auto_save = self._application.getAutoSave()
|
||||
# The auto save is only not created if the application has not yet started.
|
||||
if auto_save:
|
||||
auto_save.setEnabled(True)
|
||||
else:
|
||||
Logger.log("e", "Unable to enable the autosave as application init has not been completed")
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -12,9 +12,10 @@ import json
|
|||
import ssl
|
||||
import urllib.request
|
||||
import urllib.error
|
||||
import shutil
|
||||
|
||||
from PyQt5.QtCore import QT_VERSION_STR, PYQT_VERSION_STR, Qt, QUrl
|
||||
import certifi
|
||||
|
||||
from PyQt5.QtCore import QT_VERSION_STR, PYQT_VERSION_STR, QUrl
|
||||
from PyQt5.QtWidgets import QDialog, QDialogButtonBox, QVBoxLayout, QLabel, QTextEdit, QGroupBox, QCheckBox, QPushButton
|
||||
from PyQt5.QtGui import QDesktopServices
|
||||
|
||||
|
@ -22,9 +23,10 @@ from UM.Application import Application
|
|||
from UM.Logger import Logger
|
||||
from UM.View.GL.OpenGL import OpenGL
|
||||
from UM.i18n import i18nCatalog
|
||||
from UM.Platform import Platform
|
||||
from UM.Resources import Resources
|
||||
|
||||
from cura import ApplicationMetadata
|
||||
|
||||
catalog = i18nCatalog("cura")
|
||||
|
||||
MYPY = False
|
||||
|
@ -181,6 +183,7 @@ class CrashHandler:
|
|||
self.cura_version = catalog.i18nc("@label unknown version of Cura", "Unknown")
|
||||
|
||||
crash_info = "<b>" + catalog.i18nc("@label Cura version number", "Cura version") + ":</b> " + str(self.cura_version) + "<br/>"
|
||||
crash_info += "<b>" + catalog.i18nc("@label Cura build type", "Cura build type") + ":</b> " + str(ApplicationMetadata.CuraBuildType) + "<br/>"
|
||||
crash_info += "<b>" + catalog.i18nc("@label Type of platform", "Platform") + ":</b> " + str(platform.platform()) + "<br/>"
|
||||
crash_info += "<b>" + catalog.i18nc("@label", "Qt version") + ":</b> " + str(QT_VERSION_STR) + "<br/>"
|
||||
crash_info += "<b>" + catalog.i18nc("@label", "PyQt version") + ":</b> " + str(PYQT_VERSION_STR) + "<br/>"
|
||||
|
@ -191,6 +194,7 @@ class CrashHandler:
|
|||
group.setLayout(layout)
|
||||
|
||||
self.data["cura_version"] = self.cura_version
|
||||
self.data["cura_build_type"] = ApplicationMetadata.CuraBuildType
|
||||
self.data["os"] = {"type": platform.system(), "version": platform.version()}
|
||||
self.data["qt_version"] = QT_VERSION_STR
|
||||
self.data["pyqt_version"] = PYQT_VERSION_STR
|
||||
|
@ -319,7 +323,8 @@ class CrashHandler:
|
|||
|
||||
def _userDescriptionWidget(self):
|
||||
group = QGroupBox()
|
||||
group.setTitle(catalog.i18nc("@title:groupbox", "User description"))
|
||||
group.setTitle(catalog.i18nc("@title:groupbox", "User description" +
|
||||
" (Note: Developers may not speak your language, please use English if possible)"))
|
||||
layout = QVBoxLayout()
|
||||
|
||||
# When sending the report, the user comments will be collected
|
||||
|
@ -351,11 +356,13 @@ class CrashHandler:
|
|||
# Convert data to bytes
|
||||
binary_data = json.dumps(self.data).encode("utf-8")
|
||||
|
||||
# CURA-6698 Create an SSL context and use certifi CA certificates for verification.
|
||||
context = ssl.SSLContext(protocol=ssl.PROTOCOL_TLSv1_2)
|
||||
context.load_verify_locations(cafile = certifi.where())
|
||||
# Submit data
|
||||
kwoptions = {"data": binary_data, "timeout": 5}
|
||||
|
||||
if Platform.isOSX():
|
||||
kwoptions["context"] = ssl._create_unverified_context()
|
||||
kwoptions = {"data": binary_data,
|
||||
"timeout": 5,
|
||||
"context": context}
|
||||
|
||||
Logger.log("i", "Sending crash report info to [%s]...", self.crash_url)
|
||||
if not self.has_started:
|
||||
|
|
|
@ -3,15 +3,17 @@
|
|||
|
||||
from PyQt5.QtCore import QObject, QUrl
|
||||
from PyQt5.QtGui import QDesktopServices
|
||||
from typing import List, TYPE_CHECKING, cast
|
||||
from typing import List, Optional, cast
|
||||
|
||||
from UM.Event import CallFunctionEvent
|
||||
from UM.FlameProfiler import pyqtSlot
|
||||
from UM.Math.Quaternion import Quaternion
|
||||
from UM.Math.Vector import Vector
|
||||
from UM.Scene.Selection import Selection
|
||||
from UM.Scene.Iterator.BreadthFirstIterator import BreadthFirstIterator
|
||||
from UM.Operations.GroupedOperation import GroupedOperation
|
||||
from UM.Operations.RemoveSceneNodeOperation import RemoveSceneNodeOperation
|
||||
from UM.Operations.RotateOperation import RotateOperation
|
||||
from UM.Operations.TranslateOperation import TranslateOperation
|
||||
|
||||
import cura.CuraApplication
|
||||
|
@ -23,9 +25,8 @@ from cura.Settings.ExtruderManager import ExtruderManager
|
|||
from cura.Operations.SetBuildPlateNumberOperation import SetBuildPlateNumberOperation
|
||||
|
||||
from UM.Logger import Logger
|
||||
from UM.Scene.SceneNode import SceneNode
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from UM.Scene.SceneNode import SceneNode
|
||||
|
||||
class CuraActions(QObject):
|
||||
def __init__(self, parent: QObject = None) -> None:
|
||||
|
|
|
@ -13,119 +13,130 @@ from PyQt5.QtGui import QColor, QIcon
|
|||
from PyQt5.QtWidgets import QMessageBox
|
||||
from PyQt5.QtQml import qmlRegisterUncreatableType, qmlRegisterSingletonType, qmlRegisterType
|
||||
|
||||
from UM.i18n import i18nCatalog
|
||||
from UM.Application import Application
|
||||
from UM.Decorators import override, deprecated
|
||||
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.Scene.SceneNode import SceneNode
|
||||
from UM.Scene.Camera import Camera
|
||||
from UM.Math.Vector import Vector
|
||||
from UM.Math.Quaternion import Quaternion
|
||||
from UM.Resources import Resources
|
||||
from UM.Preferences import Preferences
|
||||
from UM.Qt.QtApplication import QtApplication # The class we're inheriting from.
|
||||
import UM.Util
|
||||
from UM.View.SelectionPass import SelectionPass # For typing.
|
||||
|
||||
from UM.Math.AxisAlignedBox import AxisAlignedBox
|
||||
from UM.Math.Matrix import Matrix
|
||||
from UM.Platform import Platform
|
||||
from UM.Resources import Resources
|
||||
from UM.Scene.ToolHandle import ToolHandle
|
||||
from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
|
||||
from UM.Math.Quaternion import Quaternion
|
||||
from UM.Math.Vector import Vector
|
||||
|
||||
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.Operations.AddSceneNodeOperation import AddSceneNodeOperation
|
||||
from UM.Operations.GroupedOperation import GroupedOperation
|
||||
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.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.Arranging.Arrange import Arrange
|
||||
from cura.Arranging.ArrangeObjectsJob import ArrangeObjectsJob
|
||||
from cura.Arranging.ArrangeObjectsAllBuildPlatesJob import ArrangeObjectsAllBuildPlatesJob
|
||||
from cura.Arranging.ShapeArray import ShapeArray
|
||||
from cura.MultiplyObjectsJob import MultiplyObjectsJob
|
||||
from cura.GlobalStacksModel import GlobalStacksModel
|
||||
from cura.Scene.ConvexHullDecorator import ConvexHullDecorator
|
||||
|
||||
from cura.Operations.SetParentOperation import SetParentOperation
|
||||
from cura.Scene.SliceableObjectDecorator import SliceableObjectDecorator
|
||||
|
||||
from cura.Scene.BlockSlicingDecorator import BlockSlicingDecorator
|
||||
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.CuraSceneNode import CuraSceneNode
|
||||
from cura.Scene.GCodeListDecorator import GCodeListDecorator
|
||||
from cura.Scene.SliceableObjectDecorator import SliceableObjectDecorator
|
||||
from cura.Scene import ZOffsetDecorator
|
||||
|
||||
from UM.Settings.SettingDefinition import SettingDefinition, DefinitionPropertyType
|
||||
from UM.Settings.ContainerRegistry import ContainerRegistry
|
||||
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.ContainerTree import ContainerTree
|
||||
from cura.Machines.MachineErrorChecker import MachineErrorChecker
|
||||
|
||||
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.MaterialManagementModel import MaterialManagementModel
|
||||
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.Machines.Models.IntentModel import IntentModel
|
||||
from cura.Machines.Models.IntentCategoryModel import IntentCategoryModel
|
||||
|
||||
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.ExtruderStack import ExtruderStack
|
||||
from cura.Settings.MachineManager import MachineManager
|
||||
from cura.Settings.MachineNameValidator import MachineNameValidator
|
||||
from cura.Settings.IntentManager import IntentManager
|
||||
from cura.Settings.MaterialSettingsVisibilityHandler import MaterialSettingsVisibilityHandler
|
||||
from cura.Settings.SettingInheritanceManager import SettingInheritanceManager
|
||||
from cura.Settings.SidebarCustomMenuItemsModel import SidebarCustomMenuItemsModel
|
||||
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.RecommendedMode import RecommendedMode
|
||||
from cura.UI.WelcomePagesModel import WelcomePagesModel
|
||||
from cura.UI.WhatsNewPagesModel import WhatsNewPagesModel
|
||||
|
||||
from cura.Utils.NetworkingUtil import NetworkingUtil
|
||||
|
||||
from .SingleInstance import SingleInstance
|
||||
from .AutoSave import AutoSave
|
||||
from . import PlatformPhysics
|
||||
from . import BuildVolume
|
||||
from . import CameraAnimation
|
||||
from . import PrintInformation
|
||||
from . import CuraActions
|
||||
from cura.Scene import ZOffsetDecorator
|
||||
from . import CuraSplashScreen
|
||||
from . import PrintJobPreviewImageProvider
|
||||
from . import MachineActionManager
|
||||
|
||||
from cura.TaskManagement.OnExitCallbackManager import OnExitCallbackManager
|
||||
|
||||
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 cura.PrinterOutputDevice import PrinterOutputDevice
|
||||
from cura.PrinterOutput.NetworkMJPGImage import NetworkMJPGImage
|
||||
|
||||
from cura import ApplicationMetadata, UltimakerCloudAuthentication
|
||||
|
||||
from UM.FlameProfiler import pyqtSlot
|
||||
from UM.Decorators import override
|
||||
from cura.Settings.GlobalStack import GlobalStack
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from cura.Machines.MaterialManager import MaterialManager
|
||||
from cura.Machines.QualityManager import QualityManager
|
||||
from UM.Settings.EmptyInstanceContainer import EmptyInstanceContainer
|
||||
from cura.Settings.GlobalStack import GlobalStack
|
||||
|
||||
|
||||
numpy.seterr(all = "ignore")
|
||||
|
||||
|
@ -134,7 +145,7 @@ class CuraApplication(QtApplication):
|
|||
# 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
|
||||
# changes of the settings.
|
||||
SettingVersion = 7
|
||||
SettingVersion = 11
|
||||
|
||||
Created = False
|
||||
|
||||
|
@ -150,6 +161,7 @@ class CuraApplication(QtApplication):
|
|||
ExtruderStack = Resources.UserType + 9
|
||||
DefinitionChangesContainer = Resources.UserType + 10
|
||||
SettingVisibilityPreset = Resources.UserType + 11
|
||||
IntentInstanceContainer = Resources.UserType + 12
|
||||
|
||||
Q_ENUMS(ResourceTypes)
|
||||
|
||||
|
@ -186,13 +198,12 @@ class CuraApplication(QtApplication):
|
|||
self.empty_container = None # type: EmptyInstanceContainer
|
||||
self.empty_definition_changes_container = None # type: EmptyInstanceContainer
|
||||
self.empty_variant_container = None # type: EmptyInstanceContainer
|
||||
self.empty_intent_container = None # type: EmptyInstanceContainer
|
||||
self.empty_material_container = None # type: EmptyInstanceContainer
|
||||
self.empty_quality_container = None # type: EmptyInstanceContainer
|
||||
self.empty_quality_changes_container = None # type: EmptyInstanceContainer
|
||||
|
||||
self._variant_manager = None
|
||||
self._material_manager = None
|
||||
self._quality_manager = None
|
||||
self._machine_manager = None
|
||||
self._extruder_manager = None
|
||||
self._container_manager = None
|
||||
|
@ -208,6 +219,17 @@ class CuraApplication(QtApplication):
|
|||
self._cura_scene_controller = None
|
||||
self._machine_error_checker = None
|
||||
|
||||
self._machine_settings_manager = MachineSettingsManager(self, parent = self)
|
||||
self._material_management_model = None
|
||||
self._quality_management_model = None
|
||||
|
||||
self._discovered_printer_model = DiscoveredPrintersModel(self, parent = self)
|
||||
self._first_start_machine_actions_model = None
|
||||
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._custom_quality_profile_drop_down_menu_model = None
|
||||
self._cura_API = CuraAPI(self)
|
||||
|
@ -237,15 +259,12 @@ class CuraApplication(QtApplication):
|
|||
|
||||
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._plugins_loaded = False
|
||||
|
||||
# Backups
|
||||
self._auto_save = None
|
||||
self._save_data_enabled = True
|
||||
self._auto_save = None # type: Optional[AutoSave]
|
||||
|
||||
from cura.Settings.CuraContainerRegistry import CuraContainerRegistry
|
||||
self._container_registry_class = CuraContainerRegistry
|
||||
|
@ -329,7 +348,7 @@ class CuraApplication(QtApplication):
|
|||
# Adds expected directory names and search paths for Resources.
|
||||
def __addExpectedResourceDirsAndSearchPaths(self):
|
||||
# this list of dir names will be used by UM to detect an old cura directory
|
||||
for dir_name in ["extruders", "machine_instances", "materials", "plugins", "quality", "quality_changes", "user", "variants"]:
|
||||
for dir_name in ["extruders", "machine_instances", "materials", "plugins", "quality", "quality_changes", "user", "variants", "intent"]:
|
||||
Resources.addExpectedDirNameInData(dir_name)
|
||||
|
||||
Resources.addSearchPath(os.path.join(self._app_install_dir, "share", "cura", "resources"))
|
||||
|
@ -387,6 +406,7 @@ class CuraApplication(QtApplication):
|
|||
Resources.addStorageType(self.ResourceTypes.MachineStack, "machine_instances")
|
||||
Resources.addStorageType(self.ResourceTypes.DefinitionChangesContainer, "definition_changes")
|
||||
Resources.addStorageType(self.ResourceTypes.SettingVisibilityPreset, "setting_visibility")
|
||||
Resources.addStorageType(self.ResourceTypes.IntentInstanceContainer, "intent")
|
||||
|
||||
self._container_registry.addResourceType(self.ResourceTypes.QualityInstanceContainer, "quality")
|
||||
self._container_registry.addResourceType(self.ResourceTypes.QualityChangesInstanceContainer, "quality_changes")
|
||||
|
@ -396,6 +416,7 @@ class CuraApplication(QtApplication):
|
|||
self._container_registry.addResourceType(self.ResourceTypes.ExtruderStack, "extruder_train")
|
||||
self._container_registry.addResourceType(self.ResourceTypes.MachineStack, "machine")
|
||||
self._container_registry.addResourceType(self.ResourceTypes.DefinitionChangesContainer, "definition_changes")
|
||||
self._container_registry.addResourceType(self.ResourceTypes.IntentInstanceContainer, "intent")
|
||||
|
||||
Resources.addType(self.ResourceTypes.QmlFiles, "qml")
|
||||
Resources.addType(self.ResourceTypes.Firmware, "firmware")
|
||||
|
@ -405,7 +426,7 @@ class CuraApplication(QtApplication):
|
|||
# Add empty variant, material and quality containers.
|
||||
# Since they are empty, they should never be serialized and instead just programmatically created.
|
||||
# We need them to simplify the switching between materials.
|
||||
self.empty_container = cura.Settings.cura_empty_instance_containers.empty_container # type: EmptyInstanceContainer
|
||||
self.empty_container = cura.Settings.cura_empty_instance_containers.empty_container
|
||||
|
||||
self._container_registry.addContainer(
|
||||
cura.Settings.cura_empty_instance_containers.empty_definition_changes_container)
|
||||
|
@ -414,6 +435,9 @@ class CuraApplication(QtApplication):
|
|||
self._container_registry.addContainer(cura.Settings.cura_empty_instance_containers.empty_variant_container)
|
||||
self.empty_variant_container = cura.Settings.cura_empty_instance_containers.empty_variant_container
|
||||
|
||||
self._container_registry.addContainer(cura.Settings.cura_empty_instance_containers.empty_intent_container)
|
||||
self.empty_intent_container = cura.Settings.cura_empty_instance_containers.empty_intent_container
|
||||
|
||||
self._container_registry.addContainer(cura.Settings.cura_empty_instance_containers.empty_material_container)
|
||||
self.empty_material_container = cura.Settings.cura_empty_instance_containers.empty_material_container
|
||||
|
||||
|
@ -430,8 +454,9 @@ class CuraApplication(QtApplication):
|
|||
{
|
||||
("quality", InstanceContainer.Version * 1000000 + self.SettingVersion): (self.ResourceTypes.QualityInstanceContainer, "application/x-uranium-instancecontainer"),
|
||||
("quality_changes", InstanceContainer.Version * 1000000 + self.SettingVersion): (self.ResourceTypes.QualityChangesInstanceContainer, "application/x-uranium-instancecontainer"),
|
||||
("machine_stack", ContainerStack.Version * 1000000 + self.SettingVersion): (self.ResourceTypes.MachineStack, "application/x-cura-globalstack"),
|
||||
("extruder_train", ContainerStack.Version * 1000000 + self.SettingVersion): (self.ResourceTypes.ExtruderStack, "application/x-cura-extruderstack"),
|
||||
("intent", InstanceContainer.Version * 1000000 + self.SettingVersion): (self.ResourceTypes.IntentInstanceContainer, "application/x-uranium-instancecontainer"),
|
||||
("machine_stack", GlobalStack.Version * 1000000 + self.SettingVersion): (self.ResourceTypes.MachineStack, "application/x-cura-globalstack"),
|
||||
("extruder_train", ExtruderStack.Version * 1000000 + self.SettingVersion): (self.ResourceTypes.ExtruderStack, "application/x-cura-extruderstack"),
|
||||
("preferences", Preferences.Version * 1000000 + self.SettingVersion): (Resources.Preferences, "application/x-uranium-preferences"),
|
||||
("user", InstanceContainer.Version * 1000000 + self.SettingVersion): (self.ResourceTypes.UserInstanceContainer, "application/x-uranium-instancecontainer"),
|
||||
("definition_changes", InstanceContainer.Version * 1000000 + self.SettingVersion): (self.ResourceTypes.DefinitionChangesContainer, "application/x-uranium-instancecontainer"),
|
||||
|
@ -450,7 +475,6 @@ class CuraApplication(QtApplication):
|
|||
# Misc.:
|
||||
"ConsoleLogger", #You want to be able to read the log if something goes wrong.
|
||||
"CuraEngineBackend", #Cura is useless without this one since you can't slice.
|
||||
"UserAgreement", #Our lawyers want every user to see this at least once.
|
||||
"FileLogger", #You want to be able to read the log if something goes wrong.
|
||||
"XmlMaterialProfile", #Cura crashes without this one.
|
||||
"Toolbox", #This contains the interface to enable/disable plug-ins, so if you disable it you can't enable it back.
|
||||
|
@ -488,10 +512,13 @@ class CuraApplication(QtApplication):
|
|||
|
||||
self.showSplashMessage(self._i18n_catalog.i18nc("@info:progress", "Loading machines..."))
|
||||
|
||||
self._container_registry.allMetadataLoaded.connect(ContainerRegistry.getInstance)
|
||||
|
||||
with self._container_registry.lockFile():
|
||||
self._container_registry.loadAllMetadata()
|
||||
|
||||
# set the setting version for Preferences
|
||||
self.showSplashMessage(self._i18n_catalog.i18nc("@info:progress", "Setting up preferences..."))
|
||||
# Set the setting version for Preferences
|
||||
preferences = self.getPreferences()
|
||||
preferences.addPreference("metadata/setting_version", 0)
|
||||
preferences.setValue("metadata/setting_version", self.SettingVersion) #Don't make it equal to the default so that the setting version always gets written to the file.
|
||||
|
@ -509,8 +536,13 @@ class CuraApplication(QtApplication):
|
|||
preferences.addPreference("cura/choice_on_profile_override", "always_ask")
|
||||
preferences.addPreference("cura/choice_on_open_project", "always_ask")
|
||||
preferences.addPreference("cura/use_multi_build_plate", False)
|
||||
preferences.addPreference("cura/show_list_of_objects", False)
|
||||
preferences.addPreference("view/settings_list_height", 400)
|
||||
preferences.addPreference("view/settings_visible", False)
|
||||
preferences.addPreference("view/settings_xpos", 0)
|
||||
preferences.addPreference("view/settings_ypos", 56)
|
||||
preferences.addPreference("view/colorscheme_xpos", 0)
|
||||
preferences.addPreference("view/colorscheme_ypos", 56)
|
||||
preferences.addPreference("cura/currency", "€")
|
||||
preferences.addPreference("cura/material_settings", "{}")
|
||||
|
||||
|
@ -522,7 +554,7 @@ class CuraApplication(QtApplication):
|
|||
preferences.addPreference("cura/expanded_brands", "")
|
||||
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 [
|
||||
"dialog_load_path", # dialog_save_path is in LocalFileOutputDevicePlugin
|
||||
|
@ -545,13 +577,20 @@ class CuraApplication(QtApplication):
|
|||
|
||||
@pyqtProperty(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:
|
||||
self._need_to_show_user_agreement = set_value
|
||||
@pyqtSlot(bool)
|
||||
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
|
||||
# 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:
|
||||
Logger.log("i", "Close application")
|
||||
main_window = self.getMainWindow()
|
||||
|
@ -604,9 +643,17 @@ class CuraApplication(QtApplication):
|
|||
|
||||
## A reusable dialogbox
|
||||
#
|
||||
showMessageBox = pyqtSignal(str, str, str, str, int, int, arguments = ["title", "text", "informativeText", "detailedText", "buttons", "icon"])
|
||||
showMessageBox = pyqtSignal(str,str, str, str, int, int,
|
||||
arguments = ["title", "text", "informativeText", "detailedText","buttons", "icon"])
|
||||
|
||||
def messageBox(self, title, text, informativeText = "", detailedText = "", buttons = QMessageBox.Ok, icon = QMessageBox.NoIcon, callback = None, callback_arguments = []):
|
||||
def messageBox(self, title, text,
|
||||
informativeText = "",
|
||||
detailedText = "",
|
||||
buttons = QMessageBox.Ok,
|
||||
icon = QMessageBox.NoIcon,
|
||||
callback = None,
|
||||
callback_arguments = []
|
||||
):
|
||||
self._message_box_callback = callback
|
||||
self._message_box_callback_arguments = callback_arguments
|
||||
self.showMessageBox.emit(title, text, informativeText, detailedText, buttons, icon)
|
||||
|
@ -632,14 +679,14 @@ class CuraApplication(QtApplication):
|
|||
def discardOrKeepProfileChangesClosed(self, option: str) -> None:
|
||||
global_stack = self.getGlobalContainerStack()
|
||||
if option == "discard":
|
||||
for extruder in global_stack.extruders.values():
|
||||
for extruder in global_stack.extruderList:
|
||||
extruder.userChanges.clear()
|
||||
global_stack.userChanges.clear()
|
||||
|
||||
# if the user decided to keep settings then the user settings should be re-calculated and validated for errors
|
||||
# before slicing. To ensure that slicer uses right settings values
|
||||
elif option == "keep":
|
||||
for extruder in global_stack.extruders.values():
|
||||
for extruder in global_stack.extruderList:
|
||||
extruder.userChanges.update()
|
||||
global_stack.userChanges.update()
|
||||
|
||||
|
@ -650,12 +697,9 @@ class CuraApplication(QtApplication):
|
|||
self._message_box_callback = None
|
||||
self._message_box_callback_arguments = []
|
||||
|
||||
def setSaveDataEnabled(self, enabled: bool) -> None:
|
||||
self._save_data_enabled = enabled
|
||||
|
||||
# Cura has multiple locations where instance containers need to be saved, so we need to handle this differently.
|
||||
def saveSettings(self):
|
||||
if not self.started or not self._save_data_enabled:
|
||||
if not self.started:
|
||||
# Do not do saving during application start or when data should not be saved on quit.
|
||||
return
|
||||
ContainerRegistry.getInstance().saveDirtyContainers()
|
||||
|
@ -676,6 +720,8 @@ class CuraApplication(QtApplication):
|
|||
## Handle loading of all plugin types (and the backend explicitly)
|
||||
# \sa PluginRegistry
|
||||
def _loadPlugins(self) -> None:
|
||||
self._plugin_registry.setCheckIfTrusted(ApplicationMetadata.IsEnterpriseVersion)
|
||||
|
||||
self._plugin_registry.addType("profile_reader", self._addProfileReader)
|
||||
self._plugin_registry.addType("profile_writer", self._addProfileWriter)
|
||||
|
||||
|
@ -699,21 +745,6 @@ class CuraApplication(QtApplication):
|
|||
|
||||
def run(self):
|
||||
super().run()
|
||||
container_registry = self._container_registry
|
||||
|
||||
Logger.log("i", "Initializing variant manager")
|
||||
self._variant_manager = VariantManager(container_registry)
|
||||
self._variant_manager.initialize()
|
||||
|
||||
Logger.log("i", "Initializing material manager")
|
||||
from cura.Machines.MaterialManager import MaterialManager
|
||||
self._material_manager = MaterialManager(container_registry, parent = self)
|
||||
self._material_manager.initialize()
|
||||
|
||||
Logger.log("i", "Initializing quality manager")
|
||||
from cura.Machines.QualityManager import QualityManager
|
||||
self._quality_manager = QualityManager(self, parent = self)
|
||||
self._quality_manager.initialize()
|
||||
|
||||
Logger.log("i", "Initializing machine manager")
|
||||
self._machine_manager = MachineManager(self, parent = self)
|
||||
|
@ -745,6 +776,11 @@ class CuraApplication(QtApplication):
|
|||
# Initialize Cura API
|
||||
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
|
||||
if self._is_headless:
|
||||
self.runWithoutGUI()
|
||||
|
@ -810,7 +846,6 @@ class CuraApplication(QtApplication):
|
|||
if diagonal < 1: #No printer added yet. Set a default camera distance for normal-sized printers.
|
||||
diagonal = 375
|
||||
camera.setPosition(Vector(-80, 250, 700) * diagonal / 375)
|
||||
camera.setPerspective(True)
|
||||
camera.lookAt(Vector(0, 0, 0))
|
||||
controller.getScene().setActiveCamera("3d")
|
||||
|
||||
|
@ -839,10 +874,42 @@ class CuraApplication(QtApplication):
|
|||
# Hide the splash screen
|
||||
self.closeSplash()
|
||||
|
||||
@pyqtSlot(result = QObject)
|
||||
def getDiscoveredPrintersModel(self, *args) -> "DiscoveredPrintersModel":
|
||||
return self._discovered_printer_model
|
||||
|
||||
@pyqtSlot(result = QObject)
|
||||
def getFirstStartMachineActionsModel(self, *args) -> "FirstStartMachineActionsModel":
|
||||
if self._first_start_machine_actions_model is None:
|
||||
self._first_start_machine_actions_model = FirstStartMachineActionsModel(self, parent = self)
|
||||
if self.started:
|
||||
self._first_start_machine_actions_model.initialize()
|
||||
return self._first_start_machine_actions_model
|
||||
|
||||
@pyqtSlot(result = QObject)
|
||||
def getSettingVisibilityPresetsModel(self, *args) -> SettingVisibilityPresetsModel:
|
||||
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":
|
||||
if self._cura_formula_functions is None:
|
||||
self._cura_formula_functions = CuraFormulaFunctions(self)
|
||||
|
@ -861,20 +928,12 @@ class CuraApplication(QtApplication):
|
|||
self._extruder_manager = ExtruderManager()
|
||||
return self._extruder_manager
|
||||
|
||||
def getVariantManager(self, *args) -> VariantManager:
|
||||
return self._variant_manager
|
||||
|
||||
@pyqtSlot(result = QObject)
|
||||
def getMaterialManager(self, *args) -> "MaterialManager":
|
||||
return self._material_manager
|
||||
|
||||
@pyqtSlot(result = QObject)
|
||||
def getQualityManager(self, *args) -> "QualityManager":
|
||||
return self._quality_manager
|
||||
def getIntentManager(self, *args) -> IntentManager:
|
||||
return IntentManager.getInstance()
|
||||
|
||||
def getObjectsModel(self, *args):
|
||||
if self._object_manager is None:
|
||||
self._object_manager = ObjectsModel.createObjectsModel()
|
||||
self._object_manager = ObjectsModel(self)
|
||||
return self._object_manager
|
||||
|
||||
@pyqtSlot(result = QObject)
|
||||
|
@ -918,6 +977,18 @@ class CuraApplication(QtApplication):
|
|||
def getMachineActionManager(self, *args):
|
||||
return self._machine_action_manager
|
||||
|
||||
@pyqtSlot(result = QObject)
|
||||
def getMaterialManagementModel(self) -> MaterialManagementModel:
|
||||
if not self._material_management_model:
|
||||
self._material_management_model = MaterialManagementModel(parent = self)
|
||||
return self._material_management_model
|
||||
|
||||
@pyqtSlot(result = QObject)
|
||||
def getQualityManagementModel(self) -> QualityManagementModel:
|
||||
if not self._quality_management_model:
|
||||
self._quality_management_model = QualityManagementModel(parent = self)
|
||||
return self._quality_management_model
|
||||
|
||||
def getSimpleModeSettingsManager(self, *args):
|
||||
if self._simple_mode_settings_manager is None:
|
||||
self._simple_mode_settings_manager = SimpleModeSettingsManager()
|
||||
|
@ -933,7 +1004,7 @@ class CuraApplication(QtApplication):
|
|||
|
||||
return super().event(event)
|
||||
|
||||
def getAutoSave(self):
|
||||
def getAutoSave(self) -> Optional[AutoSave]:
|
||||
return self._auto_save
|
||||
|
||||
## Get print information (duration / material used)
|
||||
|
@ -971,13 +1042,22 @@ class CuraApplication(QtApplication):
|
|||
qmlRegisterSingletonType(CuraSceneController, "Cura", 1, 0, "SceneController", self.getCuraSceneController)
|
||||
qmlRegisterSingletonType(ExtruderManager, "Cura", 1, 0, "ExtruderManager", self.getExtruderManager)
|
||||
qmlRegisterSingletonType(MachineManager, "Cura", 1, 0, "MachineManager", self.getMachineManager)
|
||||
qmlRegisterSingletonType(IntentManager, "Cura", 1, 6, "IntentManager", self.getIntentManager)
|
||||
qmlRegisterSingletonType(SettingInheritanceManager, "Cura", 1, 0, "SettingInheritanceManager", self.getSettingInheritanceManager)
|
||||
qmlRegisterSingletonType(SimpleModeSettingsManager, "Cura", 1, 0, "SimpleModeSettingsManager", self.getSimpleModeSettingsManager)
|
||||
qmlRegisterSingletonType(MachineActionManager.MachineActionManager, "Cura", 1, 0, "MachineActionManager", self.getMachineActionManager)
|
||||
|
||||
qmlRegisterType(NetworkingUtil, "Cura", 1, 5, "NetworkingUtil")
|
||||
|
||||
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(RecommendedMode, "Cura", 1, 0, "RecommendedMode")
|
||||
|
||||
qmlRegisterType(NetworkMJPGImage, "Cura", 1, 0, "NetworkMJPGImage")
|
||||
|
||||
qmlRegisterSingletonType(ObjectsModel, "Cura", 1, 0, "ObjectsModel", self.getObjectsModel)
|
||||
qmlRegisterType(ObjectsModel, "Cura", 1, 0, "ObjectsModel")
|
||||
qmlRegisterType(BuildPlateModel, "Cura", 1, 0, "BuildPlateModel")
|
||||
qmlRegisterType(MultiBuildPlateModel, "Cura", 1, 0, "MultiBuildPlateModel")
|
||||
qmlRegisterType(InstanceContainer, "Cura", 1, 0, "InstanceContainer")
|
||||
|
@ -987,18 +1067,23 @@ class CuraApplication(QtApplication):
|
|||
qmlRegisterType(FavoriteMaterialsModel, "Cura", 1, 0, "FavoriteMaterialsModel")
|
||||
qmlRegisterType(GenericMaterialsModel, "Cura", 1, 0, "GenericMaterialsModel")
|
||||
qmlRegisterType(MaterialBrandsModel, "Cura", 1, 0, "MaterialBrandsModel")
|
||||
qmlRegisterType(QualityManagementModel, "Cura", 1, 0, "QualityManagementModel")
|
||||
qmlRegisterType(MachineManagementModel, "Cura", 1, 0, "MachineManagementModel")
|
||||
qmlRegisterSingletonType(QualityManagementModel, "Cura", 1, 0, "QualityManagementModel", self.getQualityManagementModel)
|
||||
qmlRegisterSingletonType(MaterialManagementModel, "Cura", 1, 5, "MaterialManagementModel", self.getMaterialManagementModel)
|
||||
|
||||
qmlRegisterType(DiscoveredPrintersModel, "Cura", 1, 0, "DiscoveredPrintersModel")
|
||||
|
||||
qmlRegisterSingletonType(QualityProfilesDropDownMenuModel, "Cura", 1, 0,
|
||||
"QualityProfilesDropDownMenuModel", self.getQualityProfilesDropDownMenuModel)
|
||||
qmlRegisterSingletonType(CustomQualityProfilesDropDownMenuModel, "Cura", 1, 0,
|
||||
"CustomQualityProfilesDropDownMenuModel", self.getCustomQualityProfilesDropDownMenuModel)
|
||||
qmlRegisterType(NozzleModel, "Cura", 1, 0, "NozzleModel")
|
||||
qmlRegisterType(IntentModel, "Cura", 1, 6, "IntentModel")
|
||||
qmlRegisterType(IntentCategoryModel, "Cura", 1, 6, "IntentCategoryModel")
|
||||
|
||||
qmlRegisterType(MaterialSettingsVisibilityHandler, "Cura", 1, 0, "MaterialSettingsVisibilityHandler")
|
||||
qmlRegisterType(SettingVisibilityPresetsModel, "Cura", 1, 0, "SettingVisibilityPresetsModel")
|
||||
qmlRegisterType(QualitySettingsModel, "Cura", 1, 0, "QualitySettingsModel")
|
||||
qmlRegisterType(FirstStartMachineActionsModel, "Cura", 1, 0, "FirstStartMachineActionsModel")
|
||||
qmlRegisterType(MachineNameValidator, "Cura", 1, 0, "MachineNameValidator")
|
||||
qmlRegisterType(UserChangesModel, "Cura", 1, 0, "UserChangesModel")
|
||||
qmlRegisterSingletonType(ContainerManager, "Cura", 1, 0, "ContainerManager", ContainerManager.getInstance)
|
||||
|
@ -1055,7 +1140,6 @@ class CuraApplication(QtApplication):
|
|||
self._camera_animation.setTarget(Selection.getSelectedObject(0).getWorldPosition())
|
||||
self._camera_animation.start()
|
||||
|
||||
requestAddPrinter = pyqtSignal()
|
||||
activityChanged = pyqtSignal()
|
||||
sceneBoundingBoxChanged = pyqtSignal()
|
||||
|
||||
|
@ -1197,7 +1281,7 @@ class CuraApplication(QtApplication):
|
|||
@pyqtSlot()
|
||||
def arrangeObjectsToAllBuildPlates(self) -> None:
|
||||
nodes_to_arrange = []
|
||||
for node in DepthFirstIterator(self.getController().getScene().getRoot()): # type: ignore
|
||||
for node in DepthFirstIterator(self.getController().getScene().getRoot()):
|
||||
if not isinstance(node, SceneNode):
|
||||
continue
|
||||
|
||||
|
@ -1224,7 +1308,7 @@ class CuraApplication(QtApplication):
|
|||
def arrangeAll(self) -> None:
|
||||
nodes_to_arrange = []
|
||||
active_build_plate = self.getMultiBuildPlateModel().activeBuildPlate
|
||||
for node in DepthFirstIterator(self.getController().getScene().getRoot()): # type: ignore
|
||||
for node in DepthFirstIterator(self.getController().getScene().getRoot()):
|
||||
if not isinstance(node, SceneNode):
|
||||
continue
|
||||
|
||||
|
@ -1262,7 +1346,13 @@ class CuraApplication(QtApplication):
|
|||
Logger.log("i", "Reloading all loaded mesh data.")
|
||||
nodes = []
|
||||
has_merged_nodes = False
|
||||
for node in DepthFirstIterator(self.getController().getScene().getRoot()): # type: ignore
|
||||
gcode_filename = None # type: Optional[str]
|
||||
for node in DepthFirstIterator(self.getController().getScene().getRoot()):
|
||||
# Objects loaded from Gcode should also be included.
|
||||
gcode_filename = node.callDecoration("getGcodeFileName")
|
||||
if gcode_filename is not None:
|
||||
break
|
||||
|
||||
if not isinstance(node, CuraSceneNode) or not node.getMeshData():
|
||||
if node.getName() == "MergedMesh":
|
||||
has_merged_nodes = True
|
||||
|
@ -1270,11 +1360,19 @@ class CuraApplication(QtApplication):
|
|||
|
||||
nodes.append(node)
|
||||
|
||||
# We can open only one gcode file at the same time. If the current view has a gcode file open, just reopen it
|
||||
# for reloading.
|
||||
if gcode_filename:
|
||||
self._openFile(gcode_filename)
|
||||
|
||||
if not nodes:
|
||||
return
|
||||
|
||||
for node in nodes:
|
||||
file_name = node.getMeshData().getFileName()
|
||||
mesh_data = node.getMeshData()
|
||||
|
||||
if mesh_data:
|
||||
file_name = mesh_data.getFileName()
|
||||
if file_name:
|
||||
job = ReadMeshJob(file_name)
|
||||
job._node = node # type: ignore
|
||||
|
@ -1515,8 +1613,12 @@ class CuraApplication(QtApplication):
|
|||
|
||||
openProjectFile = pyqtSignal(QUrl, arguments = ["project_file"]) # Emitted when a project file is about to open.
|
||||
|
||||
@pyqtSlot(QUrl, bool)
|
||||
def readLocalFile(self, file, skip_project_file_check = False):
|
||||
@pyqtSlot(QUrl, str)
|
||||
@pyqtSlot(QUrl)
|
||||
## Open a local file
|
||||
# \param project_mode How to handle project files. Either None(default): Follow user preference, "open_as_model" or
|
||||
# "open_as_project". This parameter is only considered if the file is a project file.
|
||||
def readLocalFile(self, file: QUrl, project_mode: Optional[str] = None):
|
||||
if not file.isValid():
|
||||
return
|
||||
|
||||
|
@ -1527,10 +1629,24 @@ class CuraApplication(QtApplication):
|
|||
self.deleteAll()
|
||||
break
|
||||
|
||||
if not skip_project_file_check and self.checkIsValidProjectFile(file):
|
||||
is_project_file = self.checkIsValidProjectFile(file)
|
||||
|
||||
if project_mode is None:
|
||||
project_mode = self.getPreferences().getValue("cura/choice_on_open_project")
|
||||
|
||||
if is_project_file and project_mode == "open_as_project":
|
||||
# open as project immediately without presenting a dialog
|
||||
workspace_handler = self.getWorkspaceFileHandler()
|
||||
workspace_handler.readLocalFile(file)
|
||||
return
|
||||
|
||||
if is_project_file and project_mode == "always_ask":
|
||||
# present a dialog asking to open as project or import models
|
||||
self.callLater(self.openProjectFile.emit, file)
|
||||
return
|
||||
|
||||
# Either the file is a model file or we want to load only models from project. Continue to load models.
|
||||
|
||||
if self.getPreferences().getValue("cura/select_models_on_load"):
|
||||
Selection.clear()
|
||||
|
||||
|
@ -1591,7 +1707,7 @@ class CuraApplication(QtApplication):
|
|||
arranger = Arrange.create(x = machine_width, y = machine_depth, fixed_nodes = fixed_nodes)
|
||||
min_offset = 8
|
||||
default_extruder_position = self.getMachineManager().defaultExtruderPosition
|
||||
default_extruder_id = self._global_container_stack.extruders[default_extruder_position].getId()
|
||||
default_extruder_id = self._global_container_stack.extruderList[int(default_extruder_position)].getId()
|
||||
|
||||
select_models_on_load = self.getPreferences().getValue("cura/select_models_on_load")
|
||||
|
||||
|
@ -1685,7 +1801,7 @@ class CuraApplication(QtApplication):
|
|||
try:
|
||||
result = workspace_reader.preRead(file_path, show_dialog=False)
|
||||
return result == WorkspaceReader.PreReadResult.accepted
|
||||
except Exception as e:
|
||||
except Exception:
|
||||
Logger.logException("e", "Could not check file %s", file_url)
|
||||
return False
|
||||
|
||||
|
@ -1715,3 +1831,73 @@ class CuraApplication(QtApplication):
|
|||
def getSidebarCustomMenuItems(self) -> list:
|
||||
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
|
||||
|
||||
@pyqtSlot(result = int)
|
||||
def appWidth(self) -> int:
|
||||
main_window = QtApplication.getInstance().getMainWindow()
|
||||
if main_window:
|
||||
return main_window.width()
|
||||
else:
|
||||
return 0
|
||||
|
||||
@pyqtSlot(result = int)
|
||||
def appHeight(self) -> int:
|
||||
main_window = QtApplication.getInstance().getMainWindow()
|
||||
if main_window:
|
||||
return main_window.height()
|
||||
else:
|
||||
return 0
|
||||
|
||||
@pyqtSlot()
|
||||
def deleteAll(self, only_selectable: bool = True) -> None:
|
||||
super().deleteAll(only_selectable = only_selectable)
|
||||
|
||||
# Also remove nodes with LayerData
|
||||
self._removeNodesWithLayerData(only_selectable = only_selectable)
|
||||
|
||||
def _removeNodesWithLayerData(self, only_selectable: bool = True) -> None:
|
||||
Logger.log("i", "Clearing scene")
|
||||
nodes = []
|
||||
for node in DepthFirstIterator(self.getController().getScene().getRoot()):
|
||||
if not isinstance(node, SceneNode):
|
||||
continue
|
||||
if not node.isEnabled():
|
||||
continue
|
||||
if (not node.getMeshData() and not node.callDecoration("getLayerData")) and not node.callDecoration("isGroup"):
|
||||
continue # Node that doesnt have a mesh and is not a group.
|
||||
if only_selectable and not node.isSelectable():
|
||||
continue # Only remove nodes that are selectable.
|
||||
if not node.callDecoration("isSliceable") and not node.callDecoration("getLayerData") and not node.callDecoration("isGroup"):
|
||||
continue # Grouped nodes don't need resetting as their parent (the group) is resetted)
|
||||
nodes.append(node)
|
||||
if nodes:
|
||||
from UM.Operations.GroupedOperation import GroupedOperation
|
||||
op = GroupedOperation()
|
||||
|
||||
for node in nodes:
|
||||
from UM.Operations.RemoveSceneNodeOperation import RemoveSceneNodeOperation
|
||||
op.addOperation(RemoveSceneNodeOperation(node))
|
||||
|
||||
# Reset the print information
|
||||
self.getController().getScene().sceneChanged.emit(node)
|
||||
|
||||
op.push()
|
||||
from UM.Scene.Selection import Selection
|
||||
Selection.clear()
|
||||
|
||||
@classmethod
|
||||
def getInstance(cls, *args, **kwargs) -> "CuraApplication":
|
||||
return cast(CuraApplication, super().getInstance(**kwargs))
|
||||
|
|
|
@ -6,7 +6,6 @@ CuraAppDisplayName = "@CURA_APP_DISPLAY_NAME@"
|
|||
CuraVersion = "@CURA_VERSION@"
|
||||
CuraBuildType = "@CURA_BUILDTYPE@"
|
||||
CuraDebugMode = True if "@_cura_debugmode@" == "ON" else False
|
||||
CuraSDKVersion = "@CURA_SDK_VERSION@"
|
||||
CuraCloudAPIRoot = "@CURA_CLOUD_API_ROOT@"
|
||||
CuraCloudAPIVersion = "@CURA_CLOUD_API_VERSION@"
|
||||
CuraCloudAccountAPIRoot = "@CURA_CLOUD_ACCOUNT_API_ROOT@"
|
||||
|
|
|
@ -3,8 +3,11 @@
|
|||
|
||||
from PyQt5.QtCore import pyqtProperty, QUrl
|
||||
|
||||
from UM.Resources import Resources
|
||||
from UM.View.View import View
|
||||
|
||||
from cura.CuraApplication import CuraApplication
|
||||
|
||||
|
||||
# Since Cura has a few pre-defined "space claims" for the locations of certain components, we've provided some structure
|
||||
# to indicate this.
|
||||
|
@ -12,13 +15,20 @@ from UM.View.View import View
|
|||
# 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:
|
||||
def __init__(self, parent = None, use_empty_menu_placeholder: bool = False) -> None:
|
||||
super().__init__(parent)
|
||||
|
||||
self._empty_menu_placeholder_url = QUrl.fromLocalFile(Resources.getPath(CuraApplication.ResourceTypes.QmlFiles,
|
||||
"EmptyViewMenuComponent.qml"))
|
||||
self._use_empty_menu_placeholder = use_empty_menu_placeholder
|
||||
|
||||
@pyqtProperty(QUrl, constant = True)
|
||||
def mainComponent(self) -> QUrl:
|
||||
return self.getDisplayComponent("main")
|
||||
|
||||
@pyqtProperty(QUrl, constant = True)
|
||||
def stageMenuComponent(self) -> QUrl:
|
||||
return self.getDisplayComponent("menu")
|
||||
url = self.getDisplayComponent("menu")
|
||||
if not url.toString() and self._use_empty_menu_placeholder:
|
||||
url = self._empty_menu_placeholder_url
|
||||
return url
|
||||
|
|
|
@ -1,14 +1,20 @@
|
|||
from UM.Mesh.MeshBuilder import MeshBuilder
|
||||
# Copyright (c) 2019 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
from typing import List
|
||||
import numpy
|
||||
|
||||
from UM.Mesh.MeshBuilder import MeshBuilder
|
||||
from UM.Mesh.MeshData import MeshData
|
||||
from cura.LayerPolygon import LayerPolygon
|
||||
|
||||
|
||||
class Layer:
|
||||
def __init__(self, layer_id):
|
||||
def __init__(self, layer_id: int) -> None:
|
||||
self._id = layer_id
|
||||
self._height = 0.0
|
||||
self._thickness = 0.0
|
||||
self._polygons = []
|
||||
self._polygons = [] # type: List[LayerPolygon]
|
||||
self._element_count = 0
|
||||
|
||||
@property
|
||||
|
@ -20,7 +26,7 @@ class Layer:
|
|||
return self._thickness
|
||||
|
||||
@property
|
||||
def polygons(self):
|
||||
def polygons(self) -> List[LayerPolygon]:
|
||||
return self._polygons
|
||||
|
||||
@property
|
||||
|
@ -33,14 +39,14 @@ class Layer:
|
|||
def setThickness(self, thickness):
|
||||
self._thickness = thickness
|
||||
|
||||
def lineMeshVertexCount(self):
|
||||
def lineMeshVertexCount(self) -> int:
|
||||
result = 0
|
||||
for polygon in self._polygons:
|
||||
result += polygon.lineMeshVertexCount()
|
||||
|
||||
return result
|
||||
|
||||
def lineMeshElementCount(self):
|
||||
def lineMeshElementCount(self) -> int:
|
||||
result = 0
|
||||
for polygon in self._polygons:
|
||||
result += polygon.lineMeshElementCount()
|
||||
|
@ -57,18 +63,18 @@ class Layer:
|
|||
result_index_offset += polygon.lineMeshElementCount()
|
||||
self._element_count += polygon.elementCount
|
||||
|
||||
return (result_vertex_offset, result_index_offset)
|
||||
return result_vertex_offset, result_index_offset
|
||||
|
||||
def createMesh(self):
|
||||
def createMesh(self) -> MeshData:
|
||||
return self.createMeshOrJumps(True)
|
||||
|
||||
def createJumps(self):
|
||||
def createJumps(self) -> MeshData:
|
||||
return self.createMeshOrJumps(False)
|
||||
|
||||
# Defines the two triplets of local point indices to use to draw the two faces for each line segment in createMeshOrJump
|
||||
__index_pattern = numpy.array([[0, 3, 2, 0, 1, 3]], dtype = numpy.int32 )
|
||||
|
||||
def createMeshOrJumps(self, make_mesh):
|
||||
def createMeshOrJumps(self, make_mesh: bool) -> MeshData:
|
||||
builder = MeshBuilder()
|
||||
|
||||
line_count = 0
|
||||
|
@ -79,14 +85,14 @@ class Layer:
|
|||
for polygon in self._polygons:
|
||||
line_count += polygon.jumpCount
|
||||
|
||||
# Reserve the neccesary space for the data upfront
|
||||
# Reserve the necessary space for the data upfront
|
||||
builder.reserveFaceAndVertexCount(2 * line_count, 4 * line_count)
|
||||
|
||||
for polygon in self._polygons:
|
||||
# Filter out the types of lines we are not interesed in depending on whether we are drawing the mesh or the jumps.
|
||||
# Filter out the types of lines we are not interested in depending on whether we are drawing the mesh or the jumps.
|
||||
index_mask = numpy.logical_not(polygon.jumpMask) if make_mesh else polygon.jumpMask
|
||||
|
||||
# Create an array with rows [p p+1] and only keep those we whant to draw based on make_mesh
|
||||
# Create an array with rows [p p+1] and only keep those we want to draw based on make_mesh
|
||||
points = numpy.concatenate((polygon.data[:-1], polygon.data[1:]), 1)[index_mask.ravel()]
|
||||
# Line types of the points we want to draw
|
||||
line_types = polygon.types[index_mask]
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
# Copyright (c) 2017 Ultimaker B.V.
|
||||
# Copyright (c) 2019 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
from UM.Application import Application
|
||||
from UM.Qt.QtApplication import QtApplication
|
||||
from typing import Any, Optional
|
||||
import numpy
|
||||
|
||||
|
@ -20,7 +20,7 @@ class LayerPolygon:
|
|||
MoveCombingType = 8
|
||||
MoveRetractionType = 9
|
||||
SupportInterfaceType = 10
|
||||
PrimeTower = 11
|
||||
PrimeTowerType = 11
|
||||
__number_of_types = 12
|
||||
|
||||
__jump_map = numpy.logical_or(numpy.logical_or(numpy.arange(__number_of_types) == NoneType, numpy.arange(__number_of_types) == MoveCombingType), numpy.arange(__number_of_types) == MoveRetractionType)
|
||||
|
@ -61,19 +61,19 @@ class LayerPolygon:
|
|||
|
||||
# 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.
|
||||
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, 0], dtype = numpy.bool)
|
||||
|
||||
self._build_cache_line_mesh_mask = None # type: Optional[numpy.ndarray]
|
||||
self._build_cache_needed_points = None # type: Optional[numpy.ndarray]
|
||||
|
||||
def buildCache(self) -> None:
|
||||
# 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)
|
||||
self._index_begin = 0
|
||||
self._index_end = mesh_line_count
|
||||
|
||||
self._build_cache_needed_points = numpy.ones((len(self._types), 2), dtype=numpy.bool)
|
||||
self._build_cache_needed_points = numpy.ones((len(self._types), 2), dtype = numpy.bool)
|
||||
# Only if the type of line segment changes do we need to add an extra vertex to change colors
|
||||
self._build_cache_needed_points[1:, 0][:, numpy.newaxis] = self._types[1:] != self._types[:-1]
|
||||
# Mark points as unneeded if they are of types we don't want in the line mesh according to the calculated mask
|
||||
|
@ -136,9 +136,9 @@ class LayerPolygon:
|
|||
self._index_begin += index_offset
|
||||
self._index_end += index_offset
|
||||
|
||||
indices[self._index_begin:self._index_end, :] = numpy.arange(self._index_end-self._index_begin, dtype=numpy.int32).reshape((-1, 1))
|
||||
indices[self._index_begin:self._index_end, :] = numpy.arange(self._index_end-self._index_begin, dtype = numpy.int32).reshape((-1, 1))
|
||||
# When the line type changes the index needs to be increased by 2.
|
||||
indices[self._index_begin:self._index_end, :] += numpy.cumsum(needed_points_list[line_mesh_mask.ravel(), 0], dtype=numpy.int32).reshape((-1, 1))
|
||||
indices[self._index_begin:self._index_end, :] += numpy.cumsum(needed_points_list[line_mesh_mask.ravel(), 0], dtype = numpy.int32).reshape((-1, 1))
|
||||
# Each line segment goes from it's starting point p to p+1, offset by the vertex index.
|
||||
# The -1 is to compensate for the neccecarily True value of needed_points_list[0,0] which causes an unwanted +1 in cumsum above.
|
||||
indices[self._index_begin:self._index_end, :] += numpy.array([self._vertex_begin - 1, self._vertex_begin])
|
||||
|
@ -232,7 +232,7 @@ class LayerPolygon:
|
|||
@classmethod
|
||||
def getColorMap(cls):
|
||||
if cls.__color_map is None:
|
||||
theme = Application.getInstance().getTheme()
|
||||
theme = QtApplication.getInstance().getTheme()
|
||||
cls.__color_map = numpy.array([
|
||||
theme.getColor("layerview_none").getRgbF(), # NoneType
|
||||
theme.getColor("layerview_inset_0").getRgbF(), # Inset0Type
|
||||
|
@ -245,7 +245,7 @@ class LayerPolygon:
|
|||
theme.getColor("layerview_move_combing").getRgbF(), # MoveCombingType
|
||||
theme.getColor("layerview_move_retraction").getRgbF(), # MoveRetractionType
|
||||
theme.getColor("layerview_support_interface").getRgbF(), # SupportInterfaceType
|
||||
theme.getColor("layerview_prime_tower").getRgbF()
|
||||
theme.getColor("layerview_prime_tower").getRgbF() # PrimeTowerType
|
||||
])
|
||||
|
||||
return cls.__color_map
|
||||
|
|
|
@ -2,8 +2,9 @@
|
|||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
import os
|
||||
from typing import Optional
|
||||
|
||||
from PyQt5.QtCore import QObject, pyqtSlot, pyqtProperty, pyqtSignal
|
||||
from PyQt5.QtCore import QObject, QUrl, pyqtSlot, pyqtProperty, pyqtSignal
|
||||
|
||||
from UM.Logger import Logger
|
||||
from UM.PluginObject import PluginObject
|
||||
|
@ -33,6 +34,12 @@ class MachineAction(QObject, PluginObject):
|
|||
def getKey(self) -> str:
|
||||
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)
|
||||
def label(self) -> str:
|
||||
return self._label
|
||||
|
@ -66,18 +73,26 @@ class MachineAction(QObject, PluginObject):
|
|||
return self._finished
|
||||
|
||||
## Protected helper to create a view object based on provided QML.
|
||||
def _createViewFromQML(self) -> None:
|
||||
def _createViewFromQML(self) -> Optional["QObject"]:
|
||||
plugin_path = PluginRegistry.getInstance().getPluginPath(self.getPluginId())
|
||||
if plugin_path is None:
|
||||
Logger.log("e", "Cannot create QML view: cannot find plugin path for plugin [%s]", self.getPluginId())
|
||||
return
|
||||
return None
|
||||
path = os.path.join(plugin_path, self._qml_url)
|
||||
|
||||
from cura.CuraApplication import CuraApplication
|
||||
self._view = CuraApplication.getInstance().createQmlComponent(path, {"manager": self})
|
||||
view = CuraApplication.getInstance().createQmlComponent(path, {"manager": self})
|
||||
return view
|
||||
|
||||
@pyqtProperty(QObject, constant = True)
|
||||
def displayItem(self):
|
||||
if not self._view:
|
||||
self._createViewFromQML()
|
||||
return self._view
|
||||
@pyqtProperty(QUrl, constant = True)
|
||||
def qmlPath(self) -> "QUrl":
|
||||
plugin_path = PluginRegistry.getInstance().getPluginPath(self.getPluginId())
|
||||
if plugin_path is None:
|
||||
Logger.log("e", "Cannot create QML view: cannot find plugin path for plugin [%s]", self.getPluginId())
|
||||
return QUrl("")
|
||||
path = os.path.join(plugin_path, self._qml_url)
|
||||
return QUrl.fromLocalFile(path)
|
||||
|
||||
@pyqtSlot(result = QObject)
|
||||
def getDisplayItem(self) -> Optional["QObject"]:
|
||||
return self._createViewFromQML()
|
||||
|
|
|
@ -1,64 +1,64 @@
|
|||
# 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, Any, Dict, Union, TYPE_CHECKING
|
||||
|
||||
from collections import OrderedDict
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
from UM.ConfigurationErrorMessage import ConfigurationErrorMessage
|
||||
from UM.Settings.ContainerRegistry import ContainerRegistry
|
||||
from UM.Logger import Logger
|
||||
from UM.Settings.InstanceContainer import InstanceContainer
|
||||
|
||||
|
||||
##
|
||||
# A metadata / container combination. Use getContainer() to get the container corresponding to the metadata.
|
||||
#
|
||||
# ContainerNode is a multi-purpose class. It has two main purposes:
|
||||
# 1. It encapsulates an InstanceContainer. It contains that InstanceContainer's
|
||||
# - metadata (Always)
|
||||
# - container (lazy-loaded when needed)
|
||||
# 2. It also serves as a node in a hierarchical InstanceContainer lookup table/tree.
|
||||
# This is used in Variant, Material, and Quality Managers.
|
||||
## A node in the container tree. It represents one container.
|
||||
#
|
||||
# The container it represents is referenced by its container_id. During normal
|
||||
# use of the tree, this container is not constructed. Only when parts of the
|
||||
# tree need to get loaded in the container stack should it get constructed.
|
||||
class ContainerNode:
|
||||
__slots__ = ("_metadata", "_container", "children_map")
|
||||
|
||||
def __init__(self, metadata: Optional[Dict[str, Any]] = None) -> None:
|
||||
self._metadata = metadata
|
||||
## Creates a new node for the container tree.
|
||||
# \param container_id The ID of the container that this node should
|
||||
# represent.
|
||||
def __init__(self, container_id: str) -> None:
|
||||
self.container_id = container_id
|
||||
self._container = None # type: Optional[InstanceContainer]
|
||||
self.children_map = OrderedDict() # type: ignore # This is because it's children are supposed to override it.
|
||||
self.children_map = {} # type: Dict[str, ContainerNode] # Mapping from container ID to container node.
|
||||
|
||||
## Get an entry value from the metadata
|
||||
## Gets the metadata of the container that this node represents.
|
||||
# Getting the metadata from the container directly is about 10x as fast.
|
||||
# \return The metadata of the container in this node.
|
||||
def getMetadata(self):
|
||||
return ContainerRegistry.getInstance().findContainersMetadata(id = self.container_id)[0]
|
||||
|
||||
## Get an entry from the metadata of the container that this node contains.
|
||||
#
|
||||
# This is just a convenience function.
|
||||
# \param entry The metadata entry key to return.
|
||||
# \param default If the metadata is not present or the container is not
|
||||
# found, the value of this default is returned.
|
||||
# \return The value of the metadata entry, or the default if it was not
|
||||
# present.
|
||||
def getMetaDataEntry(self, entry: str, default: Any = None) -> Any:
|
||||
if self._metadata is None:
|
||||
container_metadata = ContainerRegistry.getInstance().findContainersMetadata(id = self.container_id)
|
||||
if len(container_metadata) == 0:
|
||||
return default
|
||||
return self._metadata.get(entry, default)
|
||||
return container_metadata[0].get(entry, default)
|
||||
|
||||
def getMetadata(self) -> Dict[str, Any]:
|
||||
if self._metadata is None:
|
||||
return {}
|
||||
return self._metadata
|
||||
|
||||
def getChildNode(self, child_key: str) -> Optional["ContainerNode"]:
|
||||
return self.children_map.get(child_key)
|
||||
|
||||
def getContainer(self) -> Optional["InstanceContainer"]:
|
||||
if self._metadata is None:
|
||||
Logger.log("e", "Cannot get container for a ContainerNode without metadata.")
|
||||
return None
|
||||
|
||||
if self._container is None:
|
||||
container_id = self._metadata["id"]
|
||||
from UM.Settings.ContainerRegistry import ContainerRegistry
|
||||
container_list = ContainerRegistry.getInstance().findInstanceContainers(id = container_id)
|
||||
if not container_list:
|
||||
Logger.log("e", "Failed to lazy-load container [{container_id}]. Cannot find it.".format(container_id = container_id))
|
||||
## The container that this node's container ID refers to.
|
||||
#
|
||||
# This can be used to finally instantiate the container in order to put it
|
||||
# in the container stack.
|
||||
# \return A container.
|
||||
@property
|
||||
def container(self) -> Optional[InstanceContainer]:
|
||||
if not self._container:
|
||||
container_list = ContainerRegistry.getInstance().findInstanceContainers(id = self.container_id)
|
||||
if len(container_list) == 0:
|
||||
Logger.log("e", "Failed to lazy-load container [{container_id}]. Cannot find it.".format(container_id = self.container_id))
|
||||
error_message = ConfigurationErrorMessage.getInstance()
|
||||
error_message.addFaultyContainers(container_id)
|
||||
error_message.addFaultyContainers(self.container_id)
|
||||
return None
|
||||
self._container = container_list[0]
|
||||
|
||||
return self._container
|
||||
|
||||
def __str__(self) -> str:
|
||||
return "%s[%s]" % (self.__class__.__name__, self.getMetaDataEntry("id"))
|
||||
return "%s[%s]" % (self.__class__.__name__, self.container_id)
|
158
cura/Machines/ContainerTree.py
Normal file
158
cura/Machines/ContainerTree.py
Normal file
|
@ -0,0 +1,158 @@
|
|||
# Copyright (c) 2019 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
from UM.Job import Job # For our background task of loading MachineNodes lazily.
|
||||
from UM.JobQueue import JobQueue # For our background task of loading MachineNodes lazily.
|
||||
from UM.Logger import Logger
|
||||
from UM.Settings.ContainerRegistry import ContainerRegistry # To listen to containers being added.
|
||||
from UM.Signal import Signal
|
||||
import cura.CuraApplication # Imported like this to prevent circular dependencies.
|
||||
from cura.Machines.MachineNode import MachineNode
|
||||
from cura.Settings.GlobalStack import GlobalStack # To listen only to global stacks being added.
|
||||
|
||||
from typing import Dict, List, Optional, TYPE_CHECKING
|
||||
import time
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from cura.Machines.QualityGroup import QualityGroup
|
||||
from cura.Machines.QualityChangesGroup import QualityChangesGroup
|
||||
from UM.Settings.ContainerStack import ContainerStack
|
||||
|
||||
|
||||
## This class contains a look-up tree for which containers are available at
|
||||
# which stages of configuration.
|
||||
#
|
||||
# The tree starts at the machine definitions. For every distinct definition
|
||||
# there will be one machine node here.
|
||||
#
|
||||
# All of the fallbacks for material choices, quality choices, etc. should be
|
||||
# encoded in this tree. There must always be at least one child node (for
|
||||
# nodes that have children) but that child node may be a node representing the
|
||||
# empty instance container.
|
||||
class ContainerTree:
|
||||
__instance = None
|
||||
|
||||
@classmethod
|
||||
def getInstance(cls):
|
||||
if cls.__instance is None:
|
||||
cls.__instance = ContainerTree()
|
||||
return cls.__instance
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.machines = self._MachineNodeMap() # Mapping from definition ID to machine nodes with lazy loading.
|
||||
self.materialsChanged = Signal() # Emitted when any of the material nodes in the tree got changed.
|
||||
cura.CuraApplication.CuraApplication.getInstance().initializationFinished.connect(self._onStartupFinished) # Start the background task to load more machine nodes after start-up is completed.
|
||||
|
||||
## Get the quality groups available for the currently activated printer.
|
||||
#
|
||||
# This contains all quality groups, enabled or disabled. To check whether
|
||||
# the quality group can be activated, test for the
|
||||
# ``QualityGroup.is_available`` property.
|
||||
# \return For every quality type, one quality group.
|
||||
def getCurrentQualityGroups(self) -> Dict[str, "QualityGroup"]:
|
||||
global_stack = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack()
|
||||
if global_stack is None:
|
||||
return {}
|
||||
variant_names = [extruder.variant.getName() for extruder in global_stack.extruderList]
|
||||
material_bases = [extruder.material.getMetaDataEntry("base_file") for extruder in global_stack.extruderList]
|
||||
extruder_enabled = [extruder.isEnabled for extruder in global_stack.extruderList]
|
||||
return self.machines[global_stack.definition.getId()].getQualityGroups(variant_names, material_bases, extruder_enabled)
|
||||
|
||||
## Get the quality changes groups available for the currently activated
|
||||
# printer.
|
||||
#
|
||||
# This contains all quality changes groups, enabled or disabled. To check
|
||||
# whether the quality changes group can be activated, test for the
|
||||
# ``QualityChangesGroup.is_available`` property.
|
||||
# \return A list of all quality changes groups.
|
||||
def getCurrentQualityChangesGroups(self) -> List["QualityChangesGroup"]:
|
||||
global_stack = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack()
|
||||
if global_stack is None:
|
||||
return []
|
||||
variant_names = [extruder.variant.getName() for extruder in global_stack.extruderList]
|
||||
material_bases = [extruder.material.getMetaDataEntry("base_file") for extruder in global_stack.extruderList]
|
||||
extruder_enabled = [extruder.isEnabled for extruder in global_stack.extruderList]
|
||||
return self.machines[global_stack.definition.getId()].getQualityChangesGroups(variant_names, material_bases, extruder_enabled)
|
||||
|
||||
## Ran after completely starting up the application.
|
||||
def _onStartupFinished(self):
|
||||
currently_added = ContainerRegistry.getInstance().findContainerStacks() # Find all currently added global stacks.
|
||||
JobQueue.getInstance().add(self._MachineNodeLoadJob(self, currently_added))
|
||||
|
||||
## Dictionary-like object that contains the machines.
|
||||
#
|
||||
# This handles the lazy loading of MachineNodes.
|
||||
class _MachineNodeMap:
|
||||
def __init__(self) -> None:
|
||||
self._machines = {} # type: Dict[str, MachineNode]
|
||||
|
||||
## Returns whether a printer with a certain definition ID exists. This
|
||||
# is regardless of whether or not the printer is loaded yet.
|
||||
# \param definition_id The definition to look for.
|
||||
# \return Whether or not a printer definition exists with that name.
|
||||
def __contains__(self, definition_id: str) -> bool:
|
||||
return len(ContainerRegistry.getInstance().findContainersMetadata(id = definition_id)) > 0
|
||||
|
||||
## Returns a machine node for the specified definition ID.
|
||||
#
|
||||
# If the machine node wasn't loaded yet, this will load it lazily.
|
||||
# \param definition_id The definition to look for.
|
||||
# \return A machine node for that definition.
|
||||
def __getitem__(self, definition_id: str) -> MachineNode:
|
||||
if definition_id not in self._machines:
|
||||
start_time = time.time()
|
||||
self._machines[definition_id] = MachineNode(definition_id)
|
||||
self._machines[definition_id].materialsChanged.connect(ContainerTree.getInstance().materialsChanged)
|
||||
Logger.log("d", "Adding container tree for {definition_id} took {duration} seconds.".format(definition_id = definition_id, duration = time.time() - start_time))
|
||||
return self._machines[definition_id]
|
||||
|
||||
## Gets a machine node for the specified definition ID, with default.
|
||||
#
|
||||
# The default is returned if there is no definition with the specified
|
||||
# ID. If the machine node wasn't loaded yet, this will load it lazily.
|
||||
# \param definition_id The definition to look for.
|
||||
# \param default The machine node to return if there is no machine
|
||||
# with that definition (can be ``None`` optionally or if not
|
||||
# provided).
|
||||
# \return A machine node for that definition, or the default if there
|
||||
# is no definition with the provided definition_id.
|
||||
def get(self, definition_id: str, default: Optional[MachineNode] = None) -> Optional[MachineNode]:
|
||||
if definition_id not in self:
|
||||
return default
|
||||
return self[definition_id]
|
||||
|
||||
## Returns whether we've already cached this definition's node.
|
||||
# \param definition_id The definition that we may have cached.
|
||||
# \return ``True`` if it's cached.
|
||||
def is_loaded(self, definition_id: str) -> bool:
|
||||
return definition_id in self._machines
|
||||
|
||||
## Pre-loads all currently added printers as a background task so that
|
||||
# switching printers in the interface is faster.
|
||||
class _MachineNodeLoadJob(Job):
|
||||
## Creates a new background task.
|
||||
# \param tree_root The container tree instance. This cannot be
|
||||
# obtained through the singleton static function since the instance
|
||||
# may not yet be constructed completely.
|
||||
# \param container_stacks All of the stacks to pre-load the container
|
||||
# trees for. This needs to be provided from here because the stacks
|
||||
# need to be constructed on the main thread because they are QObject.
|
||||
def __init__(self, tree_root: "ContainerTree", container_stacks: List["ContainerStack"]):
|
||||
self.tree_root = tree_root
|
||||
self.container_stacks = container_stacks
|
||||
super().__init__()
|
||||
|
||||
## Starts the background task.
|
||||
#
|
||||
# The ``JobQueue`` will schedule this on a different thread.
|
||||
def run(self) -> None:
|
||||
for stack in self.container_stacks: # Load all currently-added containers.
|
||||
if not isinstance(stack, GlobalStack):
|
||||
continue
|
||||
# Allow a thread switch after every container.
|
||||
# Experimentally, sleep(0) didn't allow switching. sleep(0.1) or sleep(0.2) neither.
|
||||
# We're in no hurry though. Half a second is fine.
|
||||
time.sleep(0.5)
|
||||
definition_id = stack.definition.getId()
|
||||
if not self.tree_root.machines.is_loaded(definition_id):
|
||||
_ = self.tree_root.machines[definition_id]
|
21
cura/Machines/IntentNode.py
Normal file
21
cura/Machines/IntentNode.py
Normal file
|
@ -0,0 +1,21 @@
|
|||
# Copyright (c) 2019 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from UM.Settings.ContainerRegistry import ContainerRegistry
|
||||
|
||||
from cura.Machines.ContainerNode import ContainerNode
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from cura.Machines.QualityNode import QualityNode
|
||||
|
||||
|
||||
## This class represents an intent profile in the container tree.
|
||||
#
|
||||
# This class has no more subnodes.
|
||||
class IntentNode(ContainerNode):
|
||||
def __init__(self, container_id: str, quality: "QualityNode") -> None:
|
||||
super().__init__(container_id)
|
||||
self.quality = quality
|
||||
self.intent_category = ContainerRegistry.getInstance().findContainersMetadata(id = container_id)[0].get("intent_category", "default")
|
|
@ -58,7 +58,6 @@ class MachineErrorChecker(QObject):
|
|||
|
||||
# Whenever the machine settings get changed, we schedule an error check.
|
||||
self._machine_manager.globalContainerChanged.connect(self.startErrorCheck)
|
||||
self._machine_manager.globalValueChanged.connect(self.startErrorCheck)
|
||||
|
||||
self._onMachineChanged()
|
||||
|
||||
|
@ -67,7 +66,7 @@ class MachineErrorChecker(QObject):
|
|||
self._global_stack.propertyChanged.disconnect(self.startErrorCheckPropertyChanged)
|
||||
self._global_stack.containersChanged.disconnect(self.startErrorCheck)
|
||||
|
||||
for extruder in self._global_stack.extruders.values():
|
||||
for extruder in self._global_stack.extruderList:
|
||||
extruder.propertyChanged.disconnect(self.startErrorCheckPropertyChanged)
|
||||
extruder.containersChanged.disconnect(self.startErrorCheck)
|
||||
|
||||
|
@ -77,7 +76,7 @@ class MachineErrorChecker(QObject):
|
|||
self._global_stack.propertyChanged.connect(self.startErrorCheckPropertyChanged)
|
||||
self._global_stack.containersChanged.connect(self.startErrorCheck)
|
||||
|
||||
for extruder in self._global_stack.extruders.values():
|
||||
for extruder in self._global_stack.extruderList:
|
||||
extruder.propertyChanged.connect(self.startErrorCheckPropertyChanged)
|
||||
extruder.containersChanged.connect(self.startErrorCheck)
|
||||
|
||||
|
@ -127,7 +126,7 @@ class MachineErrorChecker(QObject):
|
|||
|
||||
# Populate the (stack, key) tuples to check
|
||||
self._stacks_and_keys_to_check = deque()
|
||||
for stack in [global_stack] + list(global_stack.extruders.values()):
|
||||
for stack in global_stack.extruderList:
|
||||
for key in stack.getAllKeys():
|
||||
self._stacks_and_keys_to_check.append((stack, key))
|
||||
|
||||
|
@ -168,7 +167,7 @@ class MachineErrorChecker(QObject):
|
|||
if validator_type:
|
||||
validator = validator_type(key)
|
||||
validation_state = validator(stack)
|
||||
if validation_state in (ValidatorState.Exception, ValidatorState.MaximumError, ValidatorState.MinimumError):
|
||||
if validation_state in (ValidatorState.Exception, ValidatorState.MaximumError, ValidatorState.MinimumError, ValidatorState.Invalid):
|
||||
# Finish
|
||||
self._setResult(True)
|
||||
return
|
||||
|
|
183
cura/Machines/MachineNode.py
Normal file
183
cura/Machines/MachineNode.py
Normal file
|
@ -0,0 +1,183 @@
|
|||
# Copyright (c) 2019 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
from typing import Dict, List
|
||||
|
||||
from UM.Logger import Logger
|
||||
from UM.Signal import Signal
|
||||
from UM.Util import parseBool
|
||||
from UM.Settings.ContainerRegistry import ContainerRegistry # To find all the variants for this machine.
|
||||
|
||||
import cura.CuraApplication # Imported like this to prevent circular dependencies.
|
||||
from cura.Machines.ContainerNode import ContainerNode
|
||||
from cura.Machines.QualityChangesGroup import QualityChangesGroup # To construct groups of quality changes profiles that belong together.
|
||||
from cura.Machines.QualityGroup import QualityGroup # To construct groups of quality profiles that belong together.
|
||||
from cura.Machines.QualityNode import QualityNode
|
||||
from cura.Machines.VariantNode import VariantNode
|
||||
import UM.FlameProfiler
|
||||
|
||||
|
||||
## This class represents a machine in the container tree.
|
||||
#
|
||||
# The subnodes of these nodes are variants.
|
||||
class MachineNode(ContainerNode):
|
||||
def __init__(self, container_id: str) -> None:
|
||||
super().__init__(container_id)
|
||||
self.variants = {} # type: Dict[str, VariantNode] # Mapping variant names to their nodes.
|
||||
self.global_qualities = {} # type: Dict[str, QualityNode] # Mapping quality types to the global quality for those types.
|
||||
self.materialsChanged = Signal() # Emitted when one of the materials underneath this machine has been changed.
|
||||
|
||||
container_registry = ContainerRegistry.getInstance()
|
||||
try:
|
||||
my_metadata = container_registry.findContainersMetadata(id = container_id)[0]
|
||||
except IndexError:
|
||||
Logger.log("Unable to find metadata for container %s", container_id)
|
||||
my_metadata = {}
|
||||
# Some of the metadata is cached upon construction here.
|
||||
# ONLY DO THAT FOR METADATA THAT DOESN'T CHANGE DURING RUNTIME!
|
||||
# Otherwise you need to keep it up-to-date during runtime.
|
||||
self.has_materials = parseBool(my_metadata.get("has_materials", "true"))
|
||||
self.has_variants = parseBool(my_metadata.get("has_variants", "false"))
|
||||
self.has_machine_quality = parseBool(my_metadata.get("has_machine_quality", "false"))
|
||||
self.quality_definition = my_metadata.get("quality_definition", container_id) if self.has_machine_quality else "fdmprinter"
|
||||
self.exclude_materials = my_metadata.get("exclude_materials", [])
|
||||
self.preferred_variant_name = my_metadata.get("preferred_variant_name", "")
|
||||
self.preferred_material = my_metadata.get("preferred_material", "")
|
||||
self.preferred_quality_type = my_metadata.get("preferred_quality_type", "")
|
||||
|
||||
self._loadAll()
|
||||
|
||||
## Get the available quality groups for this machine.
|
||||
#
|
||||
# This returns all quality groups, regardless of whether they are
|
||||
# available to the combination of extruders or not. On the resulting
|
||||
# quality groups, the is_available property is set to indicate whether the
|
||||
# quality group can be selected according to the combination of extruders
|
||||
# in the parameters.
|
||||
# \param variant_names The names of the variants loaded in each extruder.
|
||||
# \param material_bases The base file names of the materials loaded in
|
||||
# each extruder.
|
||||
# \param extruder_enabled Whether or not the extruders are enabled. This
|
||||
# allows the function to set the is_available properly.
|
||||
# \return For each available quality type, a QualityGroup instance.
|
||||
def getQualityGroups(self, variant_names: List[str], material_bases: List[str], extruder_enabled: List[bool]) -> Dict[str, QualityGroup]:
|
||||
if len(variant_names) != len(material_bases) or len(variant_names) != len(extruder_enabled):
|
||||
Logger.log("e", "The number of extruders in the list of variants (" + str(len(variant_names)) + ") is not equal to the number of extruders in the list of materials (" + str(len(material_bases)) + ") or the list of enabled extruders (" + str(len(extruder_enabled)) + ").")
|
||||
return {}
|
||||
# For each extruder, find which quality profiles are available. Later we'll intersect the quality types.
|
||||
qualities_per_type_per_extruder = [{}] * len(variant_names) # type: List[Dict[str, QualityNode]]
|
||||
for extruder_nr, variant_name in enumerate(variant_names):
|
||||
if not extruder_enabled[extruder_nr]:
|
||||
continue # No qualities are available in this extruder. It'll get skipped when calculating the available quality types.
|
||||
material_base = material_bases[extruder_nr]
|
||||
if variant_name not in self.variants or material_base not in self.variants[variant_name].materials:
|
||||
# The printer has no variant/material-specific quality profiles. Use the global quality profiles.
|
||||
qualities_per_type_per_extruder[extruder_nr] = self.global_qualities
|
||||
else:
|
||||
# Use the actually specialised quality profiles.
|
||||
qualities_per_type_per_extruder[extruder_nr] = {node.quality_type: node for node in self.variants[variant_name].materials[material_base].qualities.values()}
|
||||
|
||||
# Create the quality group for each available type.
|
||||
quality_groups = {}
|
||||
for quality_type, global_quality_node in self.global_qualities.items():
|
||||
if not global_quality_node.container:
|
||||
Logger.log("w", "Node {0} doesn't have a container.".format(global_quality_node.container_id))
|
||||
continue
|
||||
quality_groups[quality_type] = QualityGroup(name = global_quality_node.getMetaDataEntry("name", "Unnamed profile"), quality_type = quality_type)
|
||||
quality_groups[quality_type].node_for_global = global_quality_node
|
||||
for extruder_position, qualities_per_type in enumerate(qualities_per_type_per_extruder):
|
||||
if quality_type in qualities_per_type:
|
||||
quality_groups[quality_type].setExtruderNode(extruder_position, qualities_per_type[quality_type])
|
||||
|
||||
available_quality_types = set(quality_groups.keys())
|
||||
for extruder_nr, qualities_per_type in enumerate(qualities_per_type_per_extruder):
|
||||
if not extruder_enabled[extruder_nr]:
|
||||
continue
|
||||
available_quality_types.intersection_update(qualities_per_type.keys())
|
||||
for quality_type in available_quality_types:
|
||||
quality_groups[quality_type].is_available = True
|
||||
return quality_groups
|
||||
|
||||
## Returns all of the quality changes groups available to this printer.
|
||||
#
|
||||
# The quality changes groups store which quality type and intent category
|
||||
# they were made for, but not which material and nozzle. Instead for the
|
||||
# quality type and intent category, the quality changes will always be
|
||||
# available but change the quality type and intent category when
|
||||
# activated.
|
||||
#
|
||||
# The quality changes group does depend on the printer: Which quality
|
||||
# definition is used.
|
||||
#
|
||||
# The quality changes groups that are available do depend on the quality
|
||||
# types that are available, so it must still be known which extruders are
|
||||
# enabled and which materials and variants are loaded in them. This allows
|
||||
# setting the correct is_available flag.
|
||||
# \param variant_names The names of the variants loaded in each extruder.
|
||||
# \param material_bases The base file names of the materials loaded in
|
||||
# each extruder.
|
||||
# \param extruder_enabled For each extruder whether or not they are
|
||||
# enabled.
|
||||
# \return List of all quality changes groups for the printer.
|
||||
def getQualityChangesGroups(self, variant_names: List[str], material_bases: List[str], extruder_enabled: List[bool]) -> List[QualityChangesGroup]:
|
||||
machine_quality_changes = ContainerRegistry.getInstance().findContainersMetadata(type = "quality_changes", definition = self.quality_definition) # All quality changes for each extruder.
|
||||
|
||||
groups_by_name = {} #type: Dict[str, QualityChangesGroup] # Group quality changes profiles by their display name. The display name must be unique for quality changes. This finds profiles that belong together in a group.
|
||||
for quality_changes in machine_quality_changes:
|
||||
name = quality_changes["name"]
|
||||
if name not in groups_by_name:
|
||||
# CURA-6599
|
||||
# For some reason, QML will get null or fail to convert type for MachineManager.activeQualityChangesGroup() to
|
||||
# a QObject. Setting the object ownership to QQmlEngine.CppOwnership doesn't work, but setting the object
|
||||
# parent to application seems to work.
|
||||
from cura.CuraApplication import CuraApplication
|
||||
groups_by_name[name] = QualityChangesGroup(name, quality_type = quality_changes["quality_type"],
|
||||
intent_category = quality_changes.get("intent_category", "default"),
|
||||
parent = CuraApplication.getInstance())
|
||||
# CURA-6882
|
||||
# Custom qualities are always available, even if they are based on the "not supported" profile.
|
||||
groups_by_name[name].is_available = True
|
||||
elif groups_by_name[name].intent_category == "default": # Intent category should be stored as "default" if everything is default or as the intent if any of the extruder have an actual intent.
|
||||
groups_by_name[name].intent_category = quality_changes.get("intent_category", "default")
|
||||
|
||||
if quality_changes.get("position") is not None and quality_changes.get("position") != "None": # An extruder profile.
|
||||
groups_by_name[name].metadata_per_extruder[int(quality_changes["position"])] = quality_changes
|
||||
else: # Global profile.
|
||||
groups_by_name[name].metadata_for_global = quality_changes
|
||||
|
||||
return list(groups_by_name.values())
|
||||
|
||||
## Gets the preferred global quality node, going by the preferred quality
|
||||
# type.
|
||||
#
|
||||
# If the preferred global quality is not in there, an arbitrary global
|
||||
# quality is taken.
|
||||
# If there are no global qualities, an empty quality is returned.
|
||||
def preferredGlobalQuality(self) -> "QualityNode":
|
||||
return self.global_qualities.get(self.preferred_quality_type, next(iter(self.global_qualities.values())))
|
||||
|
||||
## (Re)loads all variants under this printer.
|
||||
@UM.FlameProfiler.profile
|
||||
def _loadAll(self) -> None:
|
||||
container_registry = ContainerRegistry.getInstance()
|
||||
if not self.has_variants:
|
||||
self.variants["empty"] = VariantNode("empty_variant", machine = self)
|
||||
else:
|
||||
# Find all the variants for this definition ID.
|
||||
variants = container_registry.findInstanceContainersMetadata(type = "variant", definition = self.container_id, hardware_type = "nozzle")
|
||||
for variant in variants:
|
||||
variant_name = variant["name"]
|
||||
if variant_name not in self.variants:
|
||||
self.variants[variant_name] = VariantNode(variant["id"], machine = self)
|
||||
self.variants[variant_name].materialsChanged.connect(self.materialsChanged)
|
||||
if not self.variants:
|
||||
self.variants["empty"] = VariantNode("empty_variant", machine = self)
|
||||
|
||||
# Find the global qualities for this printer.
|
||||
global_qualities = container_registry.findInstanceContainersMetadata(type = "quality", definition = self.quality_definition, global_quality = "True") # First try specific to this printer.
|
||||
if len(global_qualities) == 0: # This printer doesn't override the global qualities.
|
||||
global_qualities = container_registry.findInstanceContainersMetadata(type = "quality", definition = "fdmprinter", global_quality = "True") # Otherwise pick the global global qualities.
|
||||
if len(global_qualities) == 0: # There are no global qualities either?! Something went very wrong, but we'll not crash and properly fill the tree.
|
||||
global_qualities = [cura.CuraApplication.CuraApplication.getInstance().empty_quality_container.getMetaData()]
|
||||
for global_quality in global_qualities:
|
||||
self.global_qualities[global_quality["quality_type"]] = QualityNode(global_quality["id"], parent = self)
|
|
@ -1,698 +0,0 @@
|
|||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
from collections import defaultdict, OrderedDict
|
||||
import copy
|
||||
import uuid
|
||||
from typing import Dict, Optional, TYPE_CHECKING, Any, Set, List, cast, Tuple
|
||||
|
||||
from PyQt5.Qt import QTimer, QObject, pyqtSignal, pyqtSlot
|
||||
|
||||
from UM.Application import Application
|
||||
from UM.ConfigurationErrorMessage import ConfigurationErrorMessage
|
||||
from UM.Logger import Logger
|
||||
from UM.Settings.ContainerRegistry import ContainerRegistry
|
||||
from UM.Settings.SettingFunction import SettingFunction
|
||||
from UM.Util import parseBool
|
||||
|
||||
from .MaterialNode import MaterialNode
|
||||
from .MaterialGroup import MaterialGroup
|
||||
from .VariantType import VariantType
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from UM.Settings.DefinitionContainer import DefinitionContainer
|
||||
from UM.Settings.InstanceContainer import InstanceContainer
|
||||
from cura.Settings.GlobalStack import GlobalStack
|
||||
from cura.Settings.ExtruderStack import ExtruderStack
|
||||
|
||||
|
||||
#
|
||||
# MaterialManager maintains a number of maps and trees for material lookup.
|
||||
# The models GUI and QML use are now only dependent on the MaterialManager. That means as long as the data in
|
||||
# MaterialManager gets updated correctly, the GUI models should be updated correctly too, and the same goes for GUI.
|
||||
#
|
||||
# For now, updating the lookup maps and trees here is very simple: we discard the old data completely and recreate them
|
||||
# again. This means the update is exactly the same as initialization. There are performance concerns about this approach
|
||||
# but so far the creation of the tables and maps is very fast and there is no noticeable slowness, we keep it like this
|
||||
# because it's simple.
|
||||
#
|
||||
class MaterialManager(QObject):
|
||||
|
||||
materialsUpdated = pyqtSignal() # Emitted whenever the material lookup tables are updated.
|
||||
favoritesUpdated = pyqtSignal() # Emitted whenever the favorites are changed
|
||||
|
||||
def __init__(self, container_registry, parent = None):
|
||||
super().__init__(parent)
|
||||
self._application = Application.getInstance()
|
||||
self._container_registry = container_registry # type: ContainerRegistry
|
||||
|
||||
# Material_type -> generic material metadata
|
||||
self._fallback_materials_map = dict() # type: Dict[str, Dict[str, Any]]
|
||||
|
||||
# Root_material_id -> MaterialGroup
|
||||
self._material_group_map = dict() # type: Dict[str, MaterialGroup]
|
||||
|
||||
# Approximate diameter str
|
||||
self._diameter_machine_nozzle_buildplate_material_map = dict() # type: Dict[str, Dict[str, MaterialNode]]
|
||||
|
||||
# We're using these two maps to convert between the specific diameter material id and the generic material id
|
||||
# because the generic material ids are used in qualities and definitions, while the specific diameter material is meant
|
||||
# i.e. generic_pla -> generic_pla_175
|
||||
# root_material_id -> approximate diameter str -> root_material_id for that diameter
|
||||
self._material_diameter_map = defaultdict(dict) # type: Dict[str, Dict[str, str]]
|
||||
|
||||
# Material id including diameter (generic_pla_175) -> material root id (generic_pla)
|
||||
self._diameter_material_map = dict() # type: Dict[str, str]
|
||||
|
||||
# This is used in Legacy UM3 send material function and the material management page.
|
||||
# GUID -> a list of material_groups
|
||||
self._guid_material_groups_map = defaultdict(list) # type: Dict[str, List[MaterialGroup]]
|
||||
|
||||
# The machine definition ID for the non-machine-specific materials.
|
||||
# This is used as the last fallback option if the given machine-specific material(s) cannot be found.
|
||||
self._default_machine_definition_id = "fdmprinter"
|
||||
self._default_approximate_diameter_for_quality_search = "3"
|
||||
|
||||
# When a material gets added/imported, there can be more than one InstanceContainers. In those cases, we don't
|
||||
# want to react on every container/metadata changed signal. The timer here is to buffer it a bit so we don't
|
||||
# react too many time.
|
||||
self._update_timer = QTimer(self)
|
||||
self._update_timer.setInterval(300)
|
||||
self._update_timer.setSingleShot(True)
|
||||
self._update_timer.timeout.connect(self._updateMaps)
|
||||
|
||||
self._container_registry.containerMetaDataChanged.connect(self._onContainerMetadataChanged)
|
||||
self._container_registry.containerAdded.connect(self._onContainerMetadataChanged)
|
||||
self._container_registry.containerRemoved.connect(self._onContainerMetadataChanged)
|
||||
|
||||
self._favorites = set() # type: Set[str]
|
||||
|
||||
def initialize(self) -> None:
|
||||
# Find all materials and put them in a matrix for quick search.
|
||||
material_metadatas = {metadata["id"]: metadata for metadata in
|
||||
self._container_registry.findContainersMetadata(type = "material") if
|
||||
metadata.get("GUID")} # type: Dict[str, Dict[str, Any]]
|
||||
|
||||
self._material_group_map = dict() # type: Dict[str, MaterialGroup]
|
||||
|
||||
# Map #1
|
||||
# root_material_id -> MaterialGroup
|
||||
for material_id, material_metadata in material_metadatas.items():
|
||||
# We don't store empty material in the lookup tables
|
||||
if material_id == "empty_material":
|
||||
continue
|
||||
|
||||
root_material_id = material_metadata.get("base_file", "")
|
||||
if root_material_id not in self._material_group_map:
|
||||
self._material_group_map[root_material_id] = MaterialGroup(root_material_id, MaterialNode(material_metadatas[root_material_id]))
|
||||
self._material_group_map[root_material_id].is_read_only = self._container_registry.isReadOnly(root_material_id)
|
||||
group = self._material_group_map[root_material_id]
|
||||
|
||||
# Store this material in the group of the appropriate root material.
|
||||
if material_id != root_material_id:
|
||||
new_node = MaterialNode(material_metadata)
|
||||
group.derived_material_node_list.append(new_node)
|
||||
|
||||
# Order this map alphabetically so it's easier to navigate in a debugger
|
||||
self._material_group_map = OrderedDict(sorted(self._material_group_map.items(), key = lambda x: x[0]))
|
||||
|
||||
# Map #1.5
|
||||
# GUID -> material group list
|
||||
self._guid_material_groups_map = defaultdict(list) # type: Dict[str, List[MaterialGroup]]
|
||||
for root_material_id, material_group in self._material_group_map.items():
|
||||
guid = material_group.root_material_node.getMetaDataEntry("GUID", "")
|
||||
self._guid_material_groups_map[guid].append(material_group)
|
||||
|
||||
# Map #2
|
||||
# Lookup table for material type -> fallback material metadata, only for read-only materials
|
||||
grouped_by_type_dict = dict() # type: Dict[str, Any]
|
||||
material_types_without_fallback = set()
|
||||
for root_material_id, material_node in self._material_group_map.items():
|
||||
material_type = material_node.root_material_node.getMetaDataEntry("material", "")
|
||||
if material_type not in grouped_by_type_dict:
|
||||
grouped_by_type_dict[material_type] = {"generic": None,
|
||||
"others": []}
|
||||
material_types_without_fallback.add(material_type)
|
||||
brand = material_node.root_material_node.getMetaDataEntry("brand", "")
|
||||
if brand.lower() == "generic":
|
||||
to_add = True
|
||||
if material_type in grouped_by_type_dict:
|
||||
diameter = material_node.root_material_node.getMetaDataEntry("approximate_diameter", "")
|
||||
if diameter != self._default_approximate_diameter_for_quality_search:
|
||||
to_add = False # don't add if it's not the default diameter
|
||||
|
||||
if to_add:
|
||||
# Checking this first allow us to differentiate between not read only materials:
|
||||
# - if it's in the list, it means that is a new material without fallback
|
||||
# - if it is not, then it is a custom material with a fallback material (parent)
|
||||
if material_type in material_types_without_fallback:
|
||||
grouped_by_type_dict[material_type] = material_node.root_material_node._metadata
|
||||
material_types_without_fallback.remove(material_type)
|
||||
|
||||
# Remove the materials that have no fallback materials
|
||||
for material_type in material_types_without_fallback:
|
||||
del grouped_by_type_dict[material_type]
|
||||
self._fallback_materials_map = grouped_by_type_dict
|
||||
|
||||
# Map #3
|
||||
# There can be multiple material profiles for the same material with different diameters, such as "generic_pla"
|
||||
# and "generic_pla_175". This is inconvenient when we do material-specific quality lookup because a quality can
|
||||
# be for either "generic_pla" or "generic_pla_175", but not both. This map helps to get the correct material ID
|
||||
# for quality search.
|
||||
self._material_diameter_map = defaultdict(dict)
|
||||
self._diameter_material_map = dict()
|
||||
|
||||
# Group the material IDs by the same name, material, brand, and color but with different diameters.
|
||||
material_group_dict = dict() # type: Dict[Tuple[Any], Dict[str, str]]
|
||||
keys_to_fetch = ("name", "material", "brand", "color")
|
||||
for root_material_id, machine_node in self._material_group_map.items():
|
||||
root_material_metadata = machine_node.root_material_node._metadata
|
||||
|
||||
key_data_list = [] # type: List[Any]
|
||||
for key in keys_to_fetch:
|
||||
key_data_list.append(machine_node.root_material_node.getMetaDataEntry(key))
|
||||
key_data = cast(Tuple[Any], tuple(key_data_list)) # type: Tuple[Any]
|
||||
|
||||
# If the key_data doesn't exist, it doesn't matter if the material is read only...
|
||||
if key_data not in material_group_dict:
|
||||
material_group_dict[key_data] = dict()
|
||||
else:
|
||||
# ...but if key_data exists, we just overwrite it if the material is read only, otherwise we skip it
|
||||
if not machine_node.is_read_only:
|
||||
continue
|
||||
approximate_diameter = machine_node.root_material_node.getMetaDataEntry("approximate_diameter", "")
|
||||
material_group_dict[key_data][approximate_diameter] = machine_node.root_material_node.getMetaDataEntry("id", "")
|
||||
|
||||
# Map [root_material_id][diameter] -> root_material_id for this diameter
|
||||
for data_dict in material_group_dict.values():
|
||||
for root_material_id1 in data_dict.values():
|
||||
if root_material_id1 in self._material_diameter_map:
|
||||
continue
|
||||
diameter_map = data_dict
|
||||
for root_material_id2 in data_dict.values():
|
||||
self._material_diameter_map[root_material_id2] = diameter_map
|
||||
|
||||
default_root_material_id = data_dict.get(self._default_approximate_diameter_for_quality_search)
|
||||
if default_root_material_id is None:
|
||||
default_root_material_id = list(data_dict.values())[0] # no default diameter present, just take "the" only one
|
||||
for root_material_id in data_dict.values():
|
||||
self._diameter_material_map[root_material_id] = default_root_material_id
|
||||
|
||||
# Map #4
|
||||
# "machine" -> "nozzle name" -> "buildplate name" -> "root material ID" -> specific material InstanceContainer
|
||||
self._diameter_machine_nozzle_buildplate_material_map = dict() # type: Dict[str, Dict[str, MaterialNode]]
|
||||
for material_metadata in material_metadatas.values():
|
||||
self.__addMaterialMetadataIntoLookupTree(material_metadata)
|
||||
|
||||
favorites = self._application.getPreferences().getValue("cura/favorite_materials")
|
||||
for item in favorites.split(";"):
|
||||
self._favorites.add(item)
|
||||
|
||||
self.materialsUpdated.emit()
|
||||
|
||||
def __addMaterialMetadataIntoLookupTree(self, material_metadata: Dict[str, Any]) -> None:
|
||||
material_id = material_metadata["id"]
|
||||
|
||||
# We don't store empty material in the lookup tables
|
||||
if material_id == "empty_material":
|
||||
return
|
||||
|
||||
root_material_id = material_metadata["base_file"]
|
||||
definition = material_metadata["definition"]
|
||||
approximate_diameter = material_metadata["approximate_diameter"]
|
||||
|
||||
if approximate_diameter not in self._diameter_machine_nozzle_buildplate_material_map:
|
||||
self._diameter_machine_nozzle_buildplate_material_map[approximate_diameter] = {}
|
||||
|
||||
machine_nozzle_buildplate_material_map = self._diameter_machine_nozzle_buildplate_material_map[
|
||||
approximate_diameter]
|
||||
if definition not in machine_nozzle_buildplate_material_map:
|
||||
machine_nozzle_buildplate_material_map[definition] = MaterialNode()
|
||||
|
||||
# This is a list of information regarding the intermediate nodes:
|
||||
# nozzle -> buildplate
|
||||
nozzle_name = material_metadata.get("variant_name")
|
||||
buildplate_name = material_metadata.get("buildplate_name")
|
||||
intermediate_node_info_list = [(nozzle_name, VariantType.NOZZLE),
|
||||
(buildplate_name, VariantType.BUILD_PLATE),
|
||||
]
|
||||
|
||||
variant_manager = self._application.getVariantManager()
|
||||
|
||||
machine_node = machine_nozzle_buildplate_material_map[definition]
|
||||
current_node = machine_node
|
||||
current_intermediate_node_info_idx = 0
|
||||
error_message = None # type: Optional[str]
|
||||
while current_intermediate_node_info_idx < len(intermediate_node_info_list):
|
||||
variant_name, variant_type = intermediate_node_info_list[current_intermediate_node_info_idx]
|
||||
if variant_name is not None:
|
||||
# The new material has a specific variant, so it needs to be added to that specific branch in the tree.
|
||||
variant = variant_manager.getVariantNode(definition, variant_name, variant_type)
|
||||
if variant is None:
|
||||
error_message = "Material {id} contains a variant {name} that does not exist.".format(
|
||||
id = material_metadata["id"], name = variant_name)
|
||||
break
|
||||
|
||||
# Update the current node to advance to a more specific branch
|
||||
if variant_name not in current_node.children_map:
|
||||
current_node.children_map[variant_name] = MaterialNode()
|
||||
current_node = current_node.children_map[variant_name]
|
||||
|
||||
current_intermediate_node_info_idx += 1
|
||||
|
||||
if error_message is not None:
|
||||
Logger.log("e", "%s It will not be added into the material lookup tree.", error_message)
|
||||
self._container_registry.addWrongContainerId(material_metadata["id"])
|
||||
return
|
||||
|
||||
# Add the material to the current tree node, which is the deepest (the most specific) branch we can find.
|
||||
# Sanity check: Make sure that there is no duplicated materials.
|
||||
if root_material_id in current_node.material_map:
|
||||
Logger.log("e", "Duplicated material [%s] with root ID [%s]. It has already been added.",
|
||||
material_id, root_material_id)
|
||||
ConfigurationErrorMessage.getInstance().addFaultyContainers(root_material_id)
|
||||
return
|
||||
|
||||
current_node.material_map[root_material_id] = MaterialNode(material_metadata)
|
||||
|
||||
def _updateMaps(self):
|
||||
Logger.log("i", "Updating material lookup data ...")
|
||||
self.initialize()
|
||||
|
||||
def _onContainerMetadataChanged(self, container):
|
||||
self._onContainerChanged(container)
|
||||
|
||||
def _onContainerChanged(self, container):
|
||||
container_type = container.getMetaDataEntry("type")
|
||||
if container_type != "material":
|
||||
return
|
||||
|
||||
# update the maps
|
||||
self._update_timer.start()
|
||||
|
||||
def getMaterialGroup(self, root_material_id: str) -> Optional[MaterialGroup]:
|
||||
return self._material_group_map.get(root_material_id)
|
||||
|
||||
def getRootMaterialIDForDiameter(self, root_material_id: str, approximate_diameter: str) -> str:
|
||||
return self._material_diameter_map.get(root_material_id, {}).get(approximate_diameter, root_material_id)
|
||||
|
||||
def getRootMaterialIDWithoutDiameter(self, root_material_id: str) -> str:
|
||||
return self._diameter_material_map.get(root_material_id, "")
|
||||
|
||||
def getMaterialGroupListByGUID(self, guid: str) -> Optional[List[MaterialGroup]]:
|
||||
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.
|
||||
#
|
||||
def getAvailableMaterials(self, machine_definition: "DefinitionContainer", nozzle_name: Optional[str],
|
||||
buildplate_name: Optional[str], diameter: float) -> Dict[str, MaterialNode]:
|
||||
# round the diameter to get the approximate diameter
|
||||
rounded_diameter = str(round(diameter))
|
||||
if rounded_diameter not in self._diameter_machine_nozzle_buildplate_material_map:
|
||||
Logger.log("i", "Cannot find materials with diameter [%s] (rounded to [%s])", diameter, rounded_diameter)
|
||||
return dict()
|
||||
|
||||
machine_definition_id = machine_definition.getId()
|
||||
|
||||
# If there are nozzle-and-or-buildplate materials, get the nozzle-and-or-buildplate material
|
||||
machine_nozzle_buildplate_material_map = self._diameter_machine_nozzle_buildplate_material_map[rounded_diameter]
|
||||
machine_node = machine_nozzle_buildplate_material_map.get(machine_definition_id)
|
||||
default_machine_node = machine_nozzle_buildplate_material_map.get(self._default_machine_definition_id)
|
||||
nozzle_node = None
|
||||
buildplate_node = None
|
||||
if nozzle_name is not None and machine_node is not None:
|
||||
nozzle_node = machine_node.getChildNode(nozzle_name)
|
||||
# Get buildplate node if possible
|
||||
if nozzle_node is not None and buildplate_name is not None:
|
||||
buildplate_node = nozzle_node.getChildNode(buildplate_name)
|
||||
|
||||
nodes_to_check = [buildplate_node, nozzle_node, machine_node, default_machine_node]
|
||||
|
||||
# Fallback mechanism of finding materials:
|
||||
# 1. buildplate-specific material
|
||||
# 2. nozzle-specific material
|
||||
# 3. machine-specific material
|
||||
# 4. generic material (for fdmprinter)
|
||||
machine_exclude_materials = machine_definition.getMetaDataEntry("exclude_materials", [])
|
||||
|
||||
material_id_metadata_dict = dict() # type: Dict[str, MaterialNode]
|
||||
excluded_materials = set()
|
||||
for current_node in nodes_to_check:
|
||||
if current_node is None:
|
||||
continue
|
||||
|
||||
# Only exclude the materials that are explicitly specified in the "exclude_materials" field.
|
||||
# Do not exclude other materials that are of the same type.
|
||||
for material_id, node in current_node.material_map.items():
|
||||
if material_id in machine_exclude_materials:
|
||||
excluded_materials.add(material_id)
|
||||
continue
|
||||
|
||||
if material_id not in material_id_metadata_dict:
|
||||
material_id_metadata_dict[material_id] = node
|
||||
|
||||
if excluded_materials:
|
||||
Logger.log("d", "Exclude materials {excluded_materials} for machine {machine_definition_id}".format(excluded_materials = ", ".join(excluded_materials), machine_definition_id = machine_definition_id))
|
||||
|
||||
return material_id_metadata_dict
|
||||
|
||||
#
|
||||
# A convenience function to get available materials for the given machine with the extruder position.
|
||||
#
|
||||
def getAvailableMaterialsForMachineExtruder(self, machine: "GlobalStack",
|
||||
extruder_stack: "ExtruderStack") -> Optional[Dict[str, MaterialNode]]:
|
||||
buildplate_name = machine.getBuildplateName()
|
||||
nozzle_name = None
|
||||
if extruder_stack.variant.getId() != "empty_variant":
|
||||
nozzle_name = extruder_stack.variant.getName()
|
||||
diameter = extruder_stack.getApproximateMaterialDiameter()
|
||||
|
||||
# Fetch the available materials (ContainerNode) for the current active machine and extruder setup.
|
||||
return self.getAvailableMaterials(machine.definition, nozzle_name, buildplate_name, diameter)
|
||||
|
||||
#
|
||||
# Gets MaterialNode for the given extruder and machine with the given material name.
|
||||
# Returns None if:
|
||||
# 1. the given machine doesn't have materials;
|
||||
# 2. cannot find any material InstanceContainers with the given settings.
|
||||
#
|
||||
def getMaterialNode(self, machine_definition_id: str, nozzle_name: Optional[str],
|
||||
buildplate_name: Optional[str], diameter: float, root_material_id: str) -> Optional["MaterialNode"]:
|
||||
# round the diameter to get the approximate diameter
|
||||
rounded_diameter = str(round(diameter))
|
||||
if rounded_diameter not in self._diameter_machine_nozzle_buildplate_material_map:
|
||||
Logger.log("i", "Cannot find materials with diameter [%s] (rounded to [%s]) for root material id [%s]",
|
||||
diameter, rounded_diameter, root_material_id)
|
||||
return None
|
||||
|
||||
# If there are nozzle materials, get the nozzle-specific material
|
||||
machine_nozzle_buildplate_material_map = self._diameter_machine_nozzle_buildplate_material_map[rounded_diameter] # type: Dict[str, MaterialNode]
|
||||
machine_node = machine_nozzle_buildplate_material_map.get(machine_definition_id)
|
||||
nozzle_node = None
|
||||
buildplate_node = None
|
||||
|
||||
# Fallback for "fdmprinter" if the machine-specific materials cannot be found
|
||||
if machine_node is None:
|
||||
machine_node = machine_nozzle_buildplate_material_map.get(self._default_machine_definition_id)
|
||||
if machine_node is not None and nozzle_name is not None:
|
||||
nozzle_node = machine_node.getChildNode(nozzle_name)
|
||||
if nozzle_node is not None and buildplate_name is not None:
|
||||
buildplate_node = nozzle_node.getChildNode(buildplate_name)
|
||||
|
||||
# Fallback mechanism of finding materials:
|
||||
# 1. buildplate-specific material
|
||||
# 2. nozzle-specific material
|
||||
# 3. machine-specific material
|
||||
# 4. generic material (for fdmprinter)
|
||||
nodes_to_check = [buildplate_node, nozzle_node, machine_node,
|
||||
machine_nozzle_buildplate_material_map.get(self._default_machine_definition_id)]
|
||||
|
||||
material_node = None
|
||||
for node in nodes_to_check:
|
||||
if node is not None:
|
||||
material_node = node.material_map.get(root_material_id)
|
||||
if material_node:
|
||||
break
|
||||
|
||||
return material_node
|
||||
|
||||
#
|
||||
# Gets MaterialNode for the given extruder and machine with the given material type.
|
||||
# Returns None if:
|
||||
# 1. the given machine doesn't have materials;
|
||||
# 2. cannot find any material InstanceContainers with the given settings.
|
||||
#
|
||||
def getMaterialNodeByType(self, global_stack: "GlobalStack", position: str, nozzle_name: str,
|
||||
buildplate_name: Optional[str], material_guid: str) -> Optional["MaterialNode"]:
|
||||
node = None
|
||||
machine_definition = global_stack.definition
|
||||
extruder_definition = global_stack.extruders[position].definition
|
||||
if parseBool(machine_definition.getMetaDataEntry("has_materials", False)):
|
||||
material_diameter = extruder_definition.getProperty("material_diameter", "value")
|
||||
if isinstance(material_diameter, SettingFunction):
|
||||
material_diameter = material_diameter(global_stack)
|
||||
|
||||
# Look at the guid to material dictionary
|
||||
root_material_id = None
|
||||
for material_group in self._guid_material_groups_map[material_guid]:
|
||||
root_material_id = cast(str, material_group.root_material_node.getMetaDataEntry("id", ""))
|
||||
break
|
||||
|
||||
if not root_material_id:
|
||||
Logger.log("i", "Cannot find materials with guid [%s] ", material_guid)
|
||||
return None
|
||||
|
||||
node = self.getMaterialNode(machine_definition.getId(), nozzle_name, buildplate_name,
|
||||
material_diameter, root_material_id)
|
||||
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".
|
||||
# For materials such as ultimaker_pla_orange, no quality profiles may be found, so we should fall back to use
|
||||
# the generic material IDs to search for qualities.
|
||||
#
|
||||
# An example would be, suppose we have machine with preferred material set to "filo3d_pla" (1.75mm), but its
|
||||
# extruders only use 2.85mm materials, then we won't be able to find the preferred material for this machine.
|
||||
# A fallback would be to fetch a generic material of the same type "PLA" as "filo3d_pla", and in this case it will
|
||||
# be "generic_pla". This function is intended to get a generic fallback material for the given material type.
|
||||
#
|
||||
# This function returns the generic root material ID for the given material type, where material types are "PLA",
|
||||
# "ABS", etc.
|
||||
#
|
||||
def getFallbackMaterialIdByMaterialType(self, material_type: str) -> Optional[str]:
|
||||
# For safety
|
||||
if material_type not in self._fallback_materials_map:
|
||||
Logger.log("w", "The material type [%s] does not have a fallback material" % material_type)
|
||||
return None
|
||||
fallback_material = self._fallback_materials_map[material_type]
|
||||
if fallback_material:
|
||||
return self.getRootMaterialIDWithoutDiameter(fallback_material["id"])
|
||||
else:
|
||||
return None
|
||||
|
||||
## Get default material for given global stack, extruder position and extruder nozzle name
|
||||
# you can provide the extruder_definition and then the position is ignored (useful when building up global stack in CuraStackBuilder)
|
||||
def getDefaultMaterial(self, global_stack: "GlobalStack", position: str, nozzle_name: Optional[str],
|
||||
extruder_definition: Optional["DefinitionContainer"] = None) -> Optional["MaterialNode"]:
|
||||
node = None
|
||||
|
||||
buildplate_name = global_stack.getBuildplateName()
|
||||
machine_definition = global_stack.definition
|
||||
|
||||
# The extruder-compatible material diameter in the extruder definition may not be the correct value because
|
||||
# the user can change it in the definition_changes container.
|
||||
if extruder_definition is None:
|
||||
extruder_stack_or_definition = global_stack.extruders[position]
|
||||
is_extruder_stack = True
|
||||
else:
|
||||
extruder_stack_or_definition = extruder_definition
|
||||
is_extruder_stack = False
|
||||
|
||||
if extruder_stack_or_definition and parseBool(global_stack.getMetaDataEntry("has_materials", False)):
|
||||
if is_extruder_stack:
|
||||
material_diameter = extruder_stack_or_definition.getCompatibleMaterialDiameter()
|
||||
else:
|
||||
material_diameter = extruder_stack_or_definition.getProperty("material_diameter", "value")
|
||||
|
||||
if isinstance(material_diameter, SettingFunction):
|
||||
material_diameter = material_diameter(global_stack)
|
||||
approximate_material_diameter = str(round(material_diameter))
|
||||
root_material_id = machine_definition.getMetaDataEntry("preferred_material")
|
||||
root_material_id = self.getRootMaterialIDForDiameter(root_material_id, approximate_material_diameter)
|
||||
node = self.getMaterialNode(machine_definition.getId(), nozzle_name, buildplate_name,
|
||||
material_diameter, root_material_id)
|
||||
return node
|
||||
|
||||
def removeMaterialByRootId(self, root_material_id: str):
|
||||
material_group = self.getMaterialGroup(root_material_id)
|
||||
if not material_group:
|
||||
Logger.log("i", "Unable to remove the material with id %s, because it doesn't exist.", root_material_id)
|
||||
return
|
||||
|
||||
nodes_to_remove = [material_group.root_material_node] + material_group.derived_material_node_list
|
||||
for node in nodes_to_remove:
|
||||
self._container_registry.removeContainer(node.getMetaDataEntry("id", ""))
|
||||
|
||||
#
|
||||
# Methods for GUI
|
||||
#
|
||||
|
||||
#
|
||||
# Sets the new name for the given material.
|
||||
#
|
||||
@pyqtSlot("QVariant", str)
|
||||
def setMaterialName(self, material_node: "MaterialNode", name: str) -> None:
|
||||
root_material_id = material_node.getMetaDataEntry("base_file")
|
||||
if root_material_id is None:
|
||||
return
|
||||
if self._container_registry.isReadOnly(root_material_id):
|
||||
Logger.log("w", "Cannot set name of read-only container %s.", root_material_id)
|
||||
return
|
||||
|
||||
material_group = self.getMaterialGroup(root_material_id)
|
||||
if material_group:
|
||||
container = material_group.root_material_node.getContainer()
|
||||
if container:
|
||||
container.setName(name)
|
||||
|
||||
#
|
||||
# Removes the given material.
|
||||
#
|
||||
@pyqtSlot("QVariant")
|
||||
def removeMaterial(self, material_node: "MaterialNode") -> None:
|
||||
root_material_id = material_node.getMetaDataEntry("base_file")
|
||||
if root_material_id is not None:
|
||||
self.removeMaterialByRootId(root_material_id)
|
||||
|
||||
#
|
||||
# Creates a duplicate of a material, which has the same GUID and base_file metadata.
|
||||
# Returns the root material ID of the duplicated material if successful.
|
||||
#
|
||||
@pyqtSlot("QVariant", result = str)
|
||||
def duplicateMaterial(self, material_node: MaterialNode, new_base_id: Optional[str] = None, new_metadata: Dict[str, Any] = None) -> Optional[str]:
|
||||
root_material_id = cast(str, material_node.getMetaDataEntry("base_file", ""))
|
||||
|
||||
material_group = self.getMaterialGroup(root_material_id)
|
||||
if not material_group:
|
||||
Logger.log("i", "Unable to duplicate the material with id %s, because it doesn't exist.", root_material_id)
|
||||
return None
|
||||
|
||||
base_container = material_group.root_material_node.getContainer()
|
||||
if not base_container:
|
||||
return None
|
||||
|
||||
# Ensure all settings are saved.
|
||||
self._application.saveSettings()
|
||||
|
||||
# Create a new ID & container to hold the data.
|
||||
new_containers = []
|
||||
if new_base_id is None:
|
||||
new_base_id = self._container_registry.uniqueName(base_container.getId())
|
||||
new_base_container = copy.deepcopy(base_container)
|
||||
new_base_container.getMetaData()["id"] = new_base_id
|
||||
new_base_container.getMetaData()["base_file"] = new_base_id
|
||||
if new_metadata is not None:
|
||||
for key, value in new_metadata.items():
|
||||
new_base_container.getMetaData()[key] = value
|
||||
new_containers.append(new_base_container)
|
||||
|
||||
# Clone all of them.
|
||||
for node in material_group.derived_material_node_list:
|
||||
container_to_copy = node.getContainer()
|
||||
if not container_to_copy:
|
||||
continue
|
||||
# Create unique IDs for every clone.
|
||||
new_id = new_base_id
|
||||
if container_to_copy.getMetaDataEntry("definition") != "fdmprinter":
|
||||
new_id += "_" + container_to_copy.getMetaDataEntry("definition")
|
||||
if container_to_copy.getMetaDataEntry("variant_name"):
|
||||
nozzle_name = container_to_copy.getMetaDataEntry("variant_name")
|
||||
new_id += "_" + nozzle_name.replace(" ", "_")
|
||||
|
||||
new_container = copy.deepcopy(container_to_copy)
|
||||
new_container.getMetaData()["id"] = new_id
|
||||
new_container.getMetaData()["base_file"] = new_base_id
|
||||
if new_metadata is not None:
|
||||
for key, value in new_metadata.items():
|
||||
new_container.getMetaData()[key] = value
|
||||
|
||||
new_containers.append(new_container)
|
||||
|
||||
for container_to_add in new_containers:
|
||||
container_to_add.setDirty(True)
|
||||
self._container_registry.addContainer(container_to_add)
|
||||
|
||||
# if the duplicated material was favorite then the new material should also be added to favorite.
|
||||
if root_material_id in self.getFavorites():
|
||||
self.addFavorite(new_base_id)
|
||||
|
||||
return new_base_id
|
||||
|
||||
#
|
||||
# Create a new material by cloning Generic PLA for the current material diameter and generate a new GUID.
|
||||
# Returns the ID of the newly created material.
|
||||
@pyqtSlot(result = str)
|
||||
def createMaterial(self) -> str:
|
||||
from UM.i18n import i18nCatalog
|
||||
catalog = i18nCatalog("cura")
|
||||
# Ensure all settings are saved.
|
||||
self._application.saveSettings()
|
||||
|
||||
machine_manager = self._application.getMachineManager()
|
||||
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)
|
||||
root_material_id = self.getRootMaterialIDForDiameter(root_material_id, approximate_diameter)
|
||||
material_group = self.getMaterialGroup(root_material_id)
|
||||
|
||||
if not material_group: # This should never happen
|
||||
Logger.log("w", "Cannot get the material group of %s.", root_material_id)
|
||||
return ""
|
||||
|
||||
# Create a new ID & container to hold the data.
|
||||
new_id = self._container_registry.uniqueName("custom_material")
|
||||
new_metadata = {"name": catalog.i18nc("@label", "Custom Material"),
|
||||
"brand": catalog.i18nc("@label", "Custom"),
|
||||
"GUID": str(uuid.uuid4()),
|
||||
}
|
||||
|
||||
self.duplicateMaterial(material_group.root_material_node,
|
||||
new_base_id = new_id,
|
||||
new_metadata = new_metadata)
|
||||
return new_id
|
||||
|
||||
@pyqtSlot(str)
|
||||
def addFavorite(self, root_material_id: str) -> None:
|
||||
self._favorites.add(root_material_id)
|
||||
self.materialsUpdated.emit()
|
||||
|
||||
# Ensure all settings are saved.
|
||||
self._application.getPreferences().setValue("cura/favorite_materials", ";".join(list(self._favorites)))
|
||||
self._application.saveSettings()
|
||||
|
||||
@pyqtSlot(str)
|
||||
def removeFavorite(self, root_material_id: str) -> None:
|
||||
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()
|
||||
|
||||
# Ensure all settings are saved.
|
||||
self._application.getPreferences().setValue("cura/favorite_materials", ";".join(list(self._favorites)))
|
||||
self._application.saveSettings()
|
||||
|
||||
@pyqtSlot()
|
||||
def getFavorites(self):
|
||||
return self._favorites
|
|
@ -1,25 +1,136 @@
|
|||
# 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, Dict, Any
|
||||
from collections import OrderedDict
|
||||
from .ContainerNode import ContainerNode
|
||||
|
||||
from typing import Any, Optional, TYPE_CHECKING
|
||||
|
||||
from UM.Logger import Logger
|
||||
from UM.Settings.ContainerRegistry import ContainerRegistry
|
||||
from UM.Settings.Interfaces import ContainerInterface
|
||||
from UM.Signal import Signal
|
||||
from cura.Machines.ContainerNode import ContainerNode
|
||||
from cura.Machines.QualityNode import QualityNode
|
||||
import UM.FlameProfiler
|
||||
if TYPE_CHECKING:
|
||||
from typing import Dict
|
||||
from cura.Machines.VariantNode import VariantNode
|
||||
|
||||
## Represents a material in the container tree.
|
||||
#
|
||||
# A MaterialNode is a node in the material lookup tree/map/table. It contains 2 (extra) fields:
|
||||
# - material_map: a one-to-one map of "material_root_id" to material_node.
|
||||
# - children_map: the key-value map for child nodes of this node. This is used in a lookup tree.
|
||||
#
|
||||
#
|
||||
# Its subcontainers are quality profiles.
|
||||
class MaterialNode(ContainerNode):
|
||||
__slots__ = ("material_map", "children_map")
|
||||
def __init__(self, container_id: str, variant: "VariantNode") -> None:
|
||||
super().__init__(container_id)
|
||||
self.variant = variant
|
||||
self.qualities = {} # type: Dict[str, QualityNode] # Mapping container IDs to quality profiles.
|
||||
self.materialChanged = Signal() # Triggered when the material is removed or its metadata is updated.
|
||||
|
||||
def __init__(self, metadata: Optional[Dict[str, Any]] = None) -> None:
|
||||
super().__init__(metadata = metadata)
|
||||
self.material_map = {} # type: Dict[str, MaterialNode] # material_root_id -> material_node
|
||||
container_registry = ContainerRegistry.getInstance()
|
||||
my_metadata = container_registry.findContainersMetadata(id = container_id)[0]
|
||||
self.base_file = my_metadata["base_file"]
|
||||
self.material_type = my_metadata["material"]
|
||||
self.guid = my_metadata["GUID"]
|
||||
self._loadAll()
|
||||
container_registry.containerRemoved.connect(self._onRemoved)
|
||||
container_registry.containerMetaDataChanged.connect(self._onMetadataChanged)
|
||||
|
||||
# We overide this as we want to indicate that MaterialNodes can only contain other material nodes.
|
||||
self.children_map = OrderedDict() # type: OrderedDict[str, "MaterialNode"]
|
||||
## Finds the preferred quality for this printer with this material and this
|
||||
# variant loaded.
|
||||
#
|
||||
# If the preferred quality is not available, an arbitrary quality is
|
||||
# returned. If there is a configuration mistake (like a typo in the
|
||||
# preferred quality) this returns a random available quality. If there are
|
||||
# no available qualities, this will return the empty quality node.
|
||||
# \return The node for the preferred quality, or any arbitrary quality if
|
||||
# there is no match.
|
||||
def preferredQuality(self) -> QualityNode:
|
||||
for quality_id, quality_node in self.qualities.items():
|
||||
if self.variant.machine.preferred_quality_type == quality_node.quality_type:
|
||||
return quality_node
|
||||
fallback = next(iter(self.qualities.values())) # Should only happen with empty quality node.
|
||||
Logger.log("w", "Could not find preferred quality type {preferred_quality_type} for material {material_id} and variant {variant_id}, falling back to {fallback}.".format(
|
||||
preferred_quality_type = self.variant.machine.preferred_quality_type,
|
||||
material_id = self.container_id,
|
||||
variant_id = self.variant.container_id,
|
||||
fallback = fallback.container_id
|
||||
))
|
||||
return fallback
|
||||
|
||||
def getChildNode(self, child_key: str) -> Optional["MaterialNode"]:
|
||||
return self.children_map.get(child_key)
|
||||
@UM.FlameProfiler.profile
|
||||
def _loadAll(self) -> None:
|
||||
container_registry = ContainerRegistry.getInstance()
|
||||
# Find all quality profiles that fit on this material.
|
||||
if not self.variant.machine.has_machine_quality: # Need to find the global qualities.
|
||||
qualities = container_registry.findInstanceContainersMetadata(type = "quality", definition = "fdmprinter")
|
||||
elif not self.variant.machine.has_materials:
|
||||
qualities = container_registry.findInstanceContainersMetadata(type="quality", definition=self.variant.machine.quality_definition)
|
||||
else:
|
||||
if self.variant.machine.has_variants:
|
||||
# Need to find the qualities that specify a material profile with the same material type.
|
||||
qualities = container_registry.findInstanceContainersMetadata(type = "quality", definition = self.variant.machine.quality_definition, variant = self.variant.variant_name, material = self.container_id) # First try by exact material ID.
|
||||
else:
|
||||
qualities = container_registry.findInstanceContainersMetadata(type="quality", definition=self.variant.machine.quality_definition, material=self.container_id)
|
||||
if not qualities:
|
||||
my_material_type = self.material_type
|
||||
if self.variant.machine.has_variants:
|
||||
qualities_any_material = container_registry.findInstanceContainersMetadata(type = "quality", definition = self.variant.machine.quality_definition, variant = self.variant.variant_name)
|
||||
else:
|
||||
qualities_any_material = container_registry.findInstanceContainersMetadata(type="quality", definition = self.variant.machine.quality_definition)
|
||||
for material_metadata in container_registry.findInstanceContainersMetadata(type = "material", material = my_material_type):
|
||||
qualities.extend((quality for quality in qualities_any_material if quality.get("material") == material_metadata["id"]))
|
||||
|
||||
if not qualities: # No quality profiles found. Go by GUID then.
|
||||
my_guid = self.guid
|
||||
for material_metadata in container_registry.findInstanceContainersMetadata(type = "material", guid = my_guid):
|
||||
qualities.extend((quality for quality in qualities_any_material if quality["material"] == material_metadata["id"]))
|
||||
|
||||
if not qualities:
|
||||
# There are still some machines that should use global profiles in the extruder, so do that now.
|
||||
# These are mostly older machines that haven't received updates (so single extruder machines without specific qualities
|
||||
# but that do have materials and profiles specific to that machine)
|
||||
qualities.extend([quality for quality in qualities_any_material if quality.get("global_quality", "False") != "False"])
|
||||
|
||||
for quality in qualities:
|
||||
quality_id = quality["id"]
|
||||
if quality_id not in self.qualities:
|
||||
self.qualities[quality_id] = QualityNode(quality_id, parent = self)
|
||||
if not self.qualities:
|
||||
self.qualities["empty_quality"] = QualityNode("empty_quality", parent = self)
|
||||
|
||||
## Triggered when any container is removed, but only handles it when the
|
||||
# container is removed that this node represents.
|
||||
# \param container The container that was allegedly removed.
|
||||
def _onRemoved(self, container: ContainerInterface) -> None:
|
||||
if container.getId() == self.container_id:
|
||||
# Remove myself from my parent.
|
||||
if self.base_file in self.variant.materials:
|
||||
del self.variant.materials[self.base_file]
|
||||
if not self.variant.materials:
|
||||
self.variant.materials["empty_material"] = MaterialNode("empty_material", variant = self.variant)
|
||||
self.materialChanged.emit(self)
|
||||
|
||||
## Triggered when any metadata changed in any container, but only handles
|
||||
# it when the metadata of this node is changed.
|
||||
# \param container The container whose metadata changed.
|
||||
# \param kwargs Key-word arguments provided when changing the metadata.
|
||||
# These are ignored. As far as I know they are never provided to this
|
||||
# call.
|
||||
def _onMetadataChanged(self, container: ContainerInterface, **kwargs: Any) -> None:
|
||||
if container.getId() != self.container_id:
|
||||
return
|
||||
|
||||
new_metadata = container.getMetaData()
|
||||
old_base_file = self.base_file
|
||||
if new_metadata["base_file"] != old_base_file:
|
||||
self.base_file = new_metadata["base_file"]
|
||||
if old_base_file in self.variant.materials: # Move in parent node.
|
||||
del self.variant.materials[old_base_file]
|
||||
self.variant.materials[self.base_file] = self
|
||||
|
||||
old_material_type = self.material_type
|
||||
self.material_type = new_metadata["material"]
|
||||
old_guid = self.guid
|
||||
self.guid = new_metadata["GUID"]
|
||||
if self.base_file != old_base_file or self.material_type != old_material_type or self.guid != old_guid: # List of quality profiles could've changed.
|
||||
self.qualities = {}
|
||||
self._loadAll() # Re-load the quality profiles for this node.
|
||||
self.materialChanged.emit(self)
|
||||
|
|
|
@ -1,42 +1,63 @@
|
|||
# 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, Dict, Set
|
||||
|
||||
from PyQt5.QtCore import Qt, pyqtSignal, pyqtProperty
|
||||
from typing import Dict, Set
|
||||
|
||||
from PyQt5.QtCore import Qt, QTimer, pyqtSignal, pyqtProperty
|
||||
|
||||
from UM.Qt.ListModel import ListModel
|
||||
from UM.Logger import Logger
|
||||
|
||||
import cura.CuraApplication # Imported like this to prevent a circular reference.
|
||||
from cura.Machines.ContainerTree import ContainerTree
|
||||
from cura.Machines.MaterialNode import MaterialNode
|
||||
from cura.Settings.CuraContainerRegistry import CuraContainerRegistry
|
||||
|
||||
## This is the base model class for GenericMaterialsModel and MaterialBrandsModel.
|
||||
# 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
|
||||
# bar menu "Settings" -> "Extruder nr" -> "Material" -> this menu
|
||||
from cura.Machines.MaterialNode import MaterialNode
|
||||
|
||||
|
||||
class BaseMaterialsModel(ListModel):
|
||||
|
||||
extruderPositionChanged = pyqtSignal()
|
||||
enabledChanged = pyqtSignal()
|
||||
|
||||
def __init__(self, parent = None):
|
||||
super().__init__(parent)
|
||||
|
||||
from cura.CuraApplication import CuraApplication
|
||||
|
||||
self._application = CuraApplication.getInstance()
|
||||
|
||||
self._available_materials = {} # type: Dict[str, MaterialNode]
|
||||
self._favorite_ids = set() # type: Set[str]
|
||||
|
||||
# Make these managers available to all material models
|
||||
self._container_registry = self._application.getInstance().getContainerRegistry()
|
||||
self._machine_manager = self._application.getMachineManager()
|
||||
self._material_manager = self._application.getMaterialManager()
|
||||
|
||||
self._extruder_position = 0
|
||||
self._extruder_stack = None
|
||||
self._enabled = True
|
||||
|
||||
# CURA-6904
|
||||
# Updating the material model requires information from material nodes and containers. We use a timer here to
|
||||
# make sure that an update function call will not be directly invoked by an event. Because the triggered event
|
||||
# can be caused in the middle of a XMLMaterial loading, and the material container we try to find may not be
|
||||
# in the system yet. This will cause an infinite recursion of (1) trying to load a material, (2) trying to
|
||||
# update the material model, (3) cannot find the material container, load it, (4) repeat #1.
|
||||
self._update_timer = QTimer()
|
||||
self._update_timer.setInterval(100)
|
||||
self._update_timer.setSingleShot(True)
|
||||
self._update_timer.timeout.connect(self._update)
|
||||
|
||||
# Update the stack and the model data when the machine changes
|
||||
self._machine_manager.globalContainerChanged.connect(self._updateExtruderStack)
|
||||
self._updateExtruderStack()
|
||||
|
||||
# Update this model when switching machines
|
||||
self._machine_manager.activeStackChanged.connect(self._update)
|
||||
|
||||
# Update this model when list of materials changes
|
||||
self._material_manager.materialsUpdated.connect(self._update)
|
||||
# Update this model when switching machines or tabs, when adding materials or changing their metadata.
|
||||
self._machine_manager.activeStackChanged.connect(self._onChanged)
|
||||
ContainerTree.getInstance().materialsChanged.connect(self._materialsListChanged)
|
||||
self._application.getMaterialManagementModel().favoritesChanged.connect(self._onChanged)
|
||||
|
||||
self.addRoleName(Qt.UserRole + 1, "root_material_id")
|
||||
self.addRoleName(Qt.UserRole + 2, "id")
|
||||
|
@ -55,11 +76,8 @@ class BaseMaterialsModel(ListModel):
|
|||
self.addRoleName(Qt.UserRole + 15, "container_node")
|
||||
self.addRoleName(Qt.UserRole + 16, "is_favorite")
|
||||
|
||||
self._extruder_position = 0
|
||||
self._extruder_stack = None
|
||||
|
||||
self._available_materials = None # type: Optional[Dict[str, MaterialNode]]
|
||||
self._favorite_ids = set() # type: Set[str]
|
||||
def _onChanged(self) -> None:
|
||||
self._update_timer.start()
|
||||
|
||||
def _updateExtruderStack(self):
|
||||
global_stack = self._machine_manager.activeMachine
|
||||
|
@ -67,14 +85,19 @@ class BaseMaterialsModel(ListModel):
|
|||
return
|
||||
|
||||
if self._extruder_stack is not None:
|
||||
self._extruder_stack.pyqtContainersChanged.disconnect(self._update)
|
||||
self._extruder_stack.approximateMaterialDiameterChanged.disconnect(self._update)
|
||||
self._extruder_stack = global_stack.extruders.get(str(self._extruder_position))
|
||||
self._extruder_stack.pyqtContainersChanged.disconnect(self._onChanged)
|
||||
self._extruder_stack.approximateMaterialDiameterChanged.disconnect(self._onChanged)
|
||||
|
||||
try:
|
||||
self._extruder_stack = global_stack.extruderList[self._extruder_position]
|
||||
except IndexError:
|
||||
self._extruder_stack = None
|
||||
|
||||
if self._extruder_stack is not None:
|
||||
self._extruder_stack.pyqtContainersChanged.connect(self._update)
|
||||
self._extruder_stack.approximateMaterialDiameterChanged.connect(self._update)
|
||||
self._extruder_stack.pyqtContainersChanged.connect(self._onChanged)
|
||||
self._extruder_stack.approximateMaterialDiameterChanged.connect(self._onChanged)
|
||||
# Force update the model when the extruder stack changes
|
||||
self._update()
|
||||
self._onChanged()
|
||||
|
||||
def setExtruderPosition(self, position: int):
|
||||
if self._extruder_stack is None or self._extruder_position != position:
|
||||
|
@ -86,36 +109,81 @@ class BaseMaterialsModel(ListModel):
|
|||
def extruderPosition(self) -> int:
|
||||
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._onChanged()
|
||||
self.enabledChanged.emit()
|
||||
|
||||
@pyqtProperty(bool, fset = setEnabled, notify = enabledChanged)
|
||||
def enabled(self):
|
||||
return self._enabled
|
||||
|
||||
## Triggered when a list of materials changed somewhere in the container
|
||||
# tree. This change may trigger an _update() call when the materials
|
||||
# changed for the configuration that this model is looking for.
|
||||
def _materialsListChanged(self, material: MaterialNode) -> None:
|
||||
if self._extruder_stack is None:
|
||||
return
|
||||
if material.variant.container_id != self._extruder_stack.variant.getId():
|
||||
return
|
||||
global_stack = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack()
|
||||
if not global_stack:
|
||||
return
|
||||
if material.variant.machine.container_id != global_stack.definition.getId():
|
||||
return
|
||||
self._onChanged()
|
||||
|
||||
## Triggered when the list of favorite materials is changed.
|
||||
def _favoritesChanged(self, material_base_file: str) -> None:
|
||||
if material_base_file in self._available_materials:
|
||||
self._onChanged()
|
||||
|
||||
## This is an abstract method that needs to be implemented by the specific
|
||||
# models themselves.
|
||||
def _update(self):
|
||||
pass
|
||||
self._favorite_ids = set(cura.CuraApplication.CuraApplication.getInstance().getPreferences().getValue("cura/favorite_materials").split(";"))
|
||||
|
||||
# Update the available materials (ContainerNode) for the current active machine and extruder setup.
|
||||
global_stack = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack()
|
||||
if not global_stack.hasMaterials:
|
||||
return # There are no materials for this machine, so nothing to do.
|
||||
extruder_stack = global_stack.extruders.get(str(self._extruder_position))
|
||||
if not extruder_stack:
|
||||
return
|
||||
nozzle_name = extruder_stack.variant.getName()
|
||||
machine_node = ContainerTree.getInstance().machines[global_stack.definition.getId()]
|
||||
if nozzle_name not in machine_node.variants:
|
||||
Logger.log("w", "Unable to find variant %s in container tree", nozzle_name)
|
||||
self._available_materials = {}
|
||||
return
|
||||
materials = machine_node.variants[nozzle_name].materials
|
||||
approximate_material_diameter = extruder_stack.getApproximateMaterialDiameter()
|
||||
self._available_materials = {key: material for key, material in materials.items() if float(material.getMetaDataEntry("approximate_diameter", -1)) == approximate_material_diameter}
|
||||
|
||||
## This method is used by all material models in the beginning of the
|
||||
# _update() method in order to prevent errors. It's the same in all models
|
||||
# so it's placed here for easy access.
|
||||
def _canUpdate(self):
|
||||
global_stack = self._machine_manager.activeMachine
|
||||
|
||||
if global_stack is None:
|
||||
if global_stack is None or not self._enabled:
|
||||
return False
|
||||
|
||||
extruder_position = str(self._extruder_position)
|
||||
|
||||
if extruder_position not in global_stack.extruders:
|
||||
return False
|
||||
|
||||
extruder_stack = global_stack.extruders[extruder_position]
|
||||
self._available_materials = self._material_manager.getAvailableMaterialsForMachineExtruder(global_stack, extruder_stack)
|
||||
if self._available_materials is None:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
## This is another convenience function which is shared by all material
|
||||
# models so it's put here to avoid having so much duplicated code.
|
||||
def _createMaterialItem(self, root_material_id, container_node):
|
||||
metadata = container_node.getMetadata()
|
||||
metadata_list = CuraContainerRegistry.getInstance().findContainersMetadata(id = container_node.container_id)
|
||||
if not metadata_list:
|
||||
return None
|
||||
metadata = metadata_list[0]
|
||||
item = {
|
||||
"root_material_id": root_material_id,
|
||||
"id": metadata["id"],
|
||||
|
@ -136,4 +204,3 @@ class BaseMaterialsModel(ListModel):
|
|||
"is_favorite": root_material_id in self._favorite_ids
|
||||
}
|
||||
return item
|
||||
|
||||
|
|
|
@ -1,14 +1,9 @@
|
|||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
# Copyright (c) 2019 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
from PyQt5.QtCore import Qt
|
||||
|
||||
from UM.Application import Application
|
||||
from UM.Logger import Logger
|
||||
from UM.Qt.ListModel import ListModel
|
||||
from UM.Util import parseBool
|
||||
|
||||
from cura.Machines.VariantType import VariantType
|
||||
|
||||
|
||||
class BuildPlateModel(ListModel):
|
||||
|
@ -21,31 +16,9 @@ class BuildPlateModel(ListModel):
|
|||
self.addRoleName(self.NameRole, "name")
|
||||
self.addRoleName(self.ContainerNodeRole, "container_node")
|
||||
|
||||
self._application = Application.getInstance()
|
||||
self._variant_manager = self._application._variant_manager
|
||||
self._machine_manager = self._application.getMachineManager()
|
||||
|
||||
self._machine_manager.globalContainerChanged.connect(self._update)
|
||||
|
||||
self._update()
|
||||
|
||||
def _update(self):
|
||||
Logger.log("d", "Updating {model_class_name}.".format(model_class_name = self.__class__.__name__))
|
||||
global_stack = self._machine_manager._global_container_stack
|
||||
if not global_stack:
|
||||
self.setItems([])
|
||||
return
|
||||
|
||||
has_variants = parseBool(global_stack.getMetaDataEntry("has_variant_buildplates", False))
|
||||
if not has_variants:
|
||||
self.setItems([])
|
||||
return
|
||||
|
||||
variant_dict = self._variant_manager.getVariantNodes(global_stack, variant_type = VariantType.BUILD_PLATE)
|
||||
|
||||
item_list = []
|
||||
for name, variant_node in variant_dict.items():
|
||||
item = {"name": name,
|
||||
"container_node": variant_node}
|
||||
item_list.append(item)
|
||||
self.setItems(item_list)
|
||||
|
|
|
@ -1,31 +1,48 @@
|
|||
# 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, TYPE_CHECKING
|
||||
|
||||
from UM.Logger import Logger
|
||||
|
||||
import cura.CuraApplication # Imported this way to prevent circular references.
|
||||
from cura.Machines.ContainerTree import ContainerTree
|
||||
from cura.Machines.Models.QualityProfilesDropDownMenuModel import QualityProfilesDropDownMenuModel
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from PyQt5.QtCore import QObject
|
||||
from UM.Settings.Interfaces import ContainerInterface
|
||||
|
||||
#
|
||||
# This model is used for the custom profile items in the profile drop down menu.
|
||||
#
|
||||
|
||||
## This model is used for the custom profile items in the profile drop down
|
||||
# menu.
|
||||
class CustomQualityProfilesDropDownMenuModel(QualityProfilesDropDownMenuModel):
|
||||
|
||||
def _update(self):
|
||||
def __init__(self, parent: Optional["QObject"] = None) -> None:
|
||||
super().__init__(parent)
|
||||
|
||||
container_registry = cura.CuraApplication.CuraApplication.getInstance().getContainerRegistry()
|
||||
container_registry.containerAdded.connect(self._qualityChangesListChanged)
|
||||
container_registry.containerRemoved.connect(self._qualityChangesListChanged)
|
||||
container_registry.containerMetaDataChanged.connect(self._qualityChangesListChanged)
|
||||
|
||||
def _qualityChangesListChanged(self, container: "ContainerInterface") -> None:
|
||||
if container.getMetaDataEntry("type") == "quality_changes":
|
||||
self._update()
|
||||
|
||||
def _update(self) -> None:
|
||||
Logger.log("d", "Updating {model_class_name}.".format(model_class_name = self.__class__.__name__))
|
||||
|
||||
active_global_stack = self._machine_manager.activeMachine
|
||||
active_global_stack = cura.CuraApplication.CuraApplication.getInstance().getMachineManager().activeMachine
|
||||
if active_global_stack is None:
|
||||
self.setItems([])
|
||||
Logger.log("d", "No active GlobalStack, set %s as empty.", self.__class__.__name__)
|
||||
return
|
||||
|
||||
quality_changes_group_dict = self._quality_manager.getQualityChangesGroups(active_global_stack)
|
||||
quality_changes_list = ContainerTree.getInstance().getCurrentQualityChangesGroups()
|
||||
|
||||
item_list = []
|
||||
for key in sorted(quality_changes_group_dict, key = lambda name: name.upper()):
|
||||
quality_changes_group = quality_changes_group_dict[key]
|
||||
|
||||
for quality_changes_group in sorted(quality_changes_list, key = lambda qgc: qgc.name.lower()):
|
||||
item = {"name": quality_changes_group.name,
|
||||
"layer_height": "",
|
||||
"layer_height_without_unit": "",
|
||||
|
|
263
cura/Machines/Models/DiscoveredPrintersModel.py
Normal file
263
cura/Machines/Models/DiscoveredPrintersModel.py
Normal file
|
@ -0,0 +1,263 @@
|
|||
# Copyright (c) 2019 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
from typing import Callable, Dict, List, Optional, TYPE_CHECKING
|
||||
|
||||
from PyQt5.QtCore import pyqtSlot, pyqtProperty, pyqtSignal, QObject, QTimer
|
||||
|
||||
from UM.i18n import i18nCatalog
|
||||
from UM.Logger import Logger
|
||||
from UM.Util import parseBool
|
||||
from UM.OutputDevice.OutputDeviceManager import ManualDeviceAdditionAttempt
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from UM.OutputDevice.OutputDevicePlugin import OutputDevicePlugin
|
||||
from cura.CuraApplication import CuraApplication
|
||||
from cura.PrinterOutput.NetworkedPrinterOutputDevice import NetworkedPrinterOutputDevice
|
||||
|
||||
|
||||
catalog = i18nCatalog("cura")
|
||||
|
||||
|
||||
class DiscoveredPrinter(QObject):
|
||||
|
||||
def __init__(self, ip_address: str, key: str, name: str, create_callback: Callable[[str], None], machine_type: str,
|
||||
device: "NetworkedPrinterOutputDevice", parent: Optional["QObject"] = None) -> None:
|
||||
super().__init__(parent)
|
||||
|
||||
self._ip_address = ip_address
|
||||
self._key = key
|
||||
self._name = name
|
||||
self.create_callback = create_callback
|
||||
self._machine_type = machine_type
|
||||
self._device = device
|
||||
|
||||
nameChanged = pyqtSignal()
|
||||
|
||||
def getKey(self) -> str:
|
||||
return self._key
|
||||
|
||||
@pyqtProperty(str, notify = nameChanged)
|
||||
def name(self) -> str:
|
||||
return self._name
|
||||
|
||||
def setName(self, name: str) -> None:
|
||||
if self._name != name:
|
||||
self._name = name
|
||||
self.nameChanged.emit()
|
||||
|
||||
@pyqtProperty(str, constant = True)
|
||||
def address(self) -> str:
|
||||
return self._ip_address
|
||||
|
||||
machineTypeChanged = pyqtSignal()
|
||||
|
||||
@pyqtProperty(str, notify = machineTypeChanged)
|
||||
def machineType(self) -> str:
|
||||
return self._machine_type
|
||||
|
||||
def setMachineType(self, machine_type: str) -> None:
|
||||
if self._machine_type != machine_type:
|
||||
self._machine_type = machine_type
|
||||
self.machineTypeChanged.emit()
|
||||
|
||||
# Checks if the given machine type name in the available machine list.
|
||||
# The machine type is a code name such as "ultimaker_3", while the machine type name is the human-readable name of
|
||||
# the machine type, which is "Ultimaker 3" for "ultimaker_3".
|
||||
def _hasHumanReadableMachineTypeName(self, machine_type_name: str) -> bool:
|
||||
from cura.CuraApplication import CuraApplication
|
||||
results = CuraApplication.getInstance().getContainerRegistry().findDefinitionContainersMetadata(name = machine_type_name)
|
||||
return len(results) > 0
|
||||
|
||||
# Human readable machine type string
|
||||
@pyqtProperty(str, notify = machineTypeChanged)
|
||||
def readableMachineType(self) -> str:
|
||||
from cura.CuraApplication import CuraApplication
|
||||
machine_manager = CuraApplication.getInstance().getMachineManager()
|
||||
# In NetworkOutputDevice, when it updates a printer information, it updates the machine type using the field
|
||||
# "machine_variant", and for some reason, it's not the machine type ID/codename/... but a human-readable string
|
||||
# like "Ultimaker 3". The code below handles this case.
|
||||
if self._hasHumanReadableMachineTypeName(self._machine_type):
|
||||
readable_type = self._machine_type
|
||||
else:
|
||||
readable_type = self._getMachineTypeNameFromId(self._machine_type)
|
||||
if not readable_type:
|
||||
readable_type = catalog.i18nc("@label", "Unknown")
|
||||
return readable_type
|
||||
|
||||
@pyqtProperty(bool, notify = machineTypeChanged)
|
||||
def isUnknownMachineType(self) -> bool:
|
||||
if self._hasHumanReadableMachineTypeName(self._machine_type):
|
||||
readable_type = self._machine_type
|
||||
else:
|
||||
readable_type = self._getMachineTypeNameFromId(self._machine_type)
|
||||
return not readable_type
|
||||
|
||||
def _getMachineTypeNameFromId(self, machine_type_id: str) -> str:
|
||||
machine_type_name = ""
|
||||
from cura.CuraApplication import CuraApplication
|
||||
results = CuraApplication.getInstance().getContainerRegistry().findDefinitionContainersMetadata(id = machine_type_id)
|
||||
if results:
|
||||
machine_type_name = results[0]["name"]
|
||||
return machine_type_name
|
||||
|
||||
@pyqtProperty(QObject, constant = True)
|
||||
def device(self) -> "NetworkedPrinterOutputDevice":
|
||||
return self._device
|
||||
|
||||
@pyqtProperty(bool, constant = True)
|
||||
def isHostOfGroup(self) -> bool:
|
||||
return getattr(self._device, "clusterSize", 1) > 0
|
||||
|
||||
@pyqtProperty(str, constant = True)
|
||||
def sectionName(self) -> str:
|
||||
if self.isUnknownMachineType or not self.isHostOfGroup:
|
||||
return catalog.i18nc("@label", "The printer(s) below cannot be connected because they are part of a group")
|
||||
else:
|
||||
return catalog.i18nc("@label", "Available networked printers")
|
||||
|
||||
|
||||
#
|
||||
# Discovered printers are all the printers that were found on the network, which provide a more convenient way
|
||||
# to add networked printers (Plugin finds a bunch of printers, user can select one from the list, plugin can then
|
||||
# add that printer to Cura as the active one).
|
||||
#
|
||||
class DiscoveredPrintersModel(QObject):
|
||||
|
||||
def __init__(self, application: "CuraApplication", parent: Optional["QObject"] = None) -> None:
|
||||
super().__init__(parent)
|
||||
|
||||
self._application = application
|
||||
self._discovered_printer_by_ip_dict = dict() # type: Dict[str, DiscoveredPrinter]
|
||||
|
||||
self._plugin_for_manual_device = None # type: Optional[OutputDevicePlugin]
|
||||
self._manual_device_address = ""
|
||||
|
||||
self._manual_device_request_timeout_in_seconds = 5 # timeout for adding a manual device in seconds
|
||||
self._manual_device_request_timer = QTimer()
|
||||
self._manual_device_request_timer.setInterval(self._manual_device_request_timeout_in_seconds * 1000)
|
||||
self._manual_device_request_timer.setSingleShot(True)
|
||||
self._manual_device_request_timer.timeout.connect(self._onManualRequestTimeout)
|
||||
|
||||
discoveredPrintersChanged = pyqtSignal()
|
||||
|
||||
@pyqtSlot(str)
|
||||
def checkManualDevice(self, address: str) -> None:
|
||||
if self.hasManualDeviceRequestInProgress:
|
||||
Logger.log("i", "A manual device request for address [%s] is still in progress, do nothing",
|
||||
self._manual_device_address)
|
||||
return
|
||||
|
||||
priority_order = [
|
||||
ManualDeviceAdditionAttempt.PRIORITY,
|
||||
ManualDeviceAdditionAttempt.POSSIBLE,
|
||||
] # type: List[ManualDeviceAdditionAttempt]
|
||||
|
||||
all_plugins_dict = self._application.getOutputDeviceManager().getAllOutputDevicePlugins()
|
||||
|
||||
can_add_manual_plugins = [item for item in filter(
|
||||
lambda plugin_item: plugin_item.canAddManualDevice(address) in priority_order,
|
||||
all_plugins_dict.values())]
|
||||
|
||||
if not can_add_manual_plugins:
|
||||
Logger.log("d", "Could not find a plugin to accept adding %s manually via address.", address)
|
||||
return
|
||||
|
||||
plugin = max(can_add_manual_plugins, key = lambda p: priority_order.index(p.canAddManualDevice(address)))
|
||||
self._plugin_for_manual_device = plugin
|
||||
self._plugin_for_manual_device.addManualDevice(address, callback = self._onManualDeviceRequestFinished)
|
||||
self._manual_device_address = address
|
||||
self._manual_device_request_timer.start()
|
||||
self.hasManualDeviceRequestInProgressChanged.emit()
|
||||
|
||||
@pyqtSlot()
|
||||
def cancelCurrentManualDeviceRequest(self) -> None:
|
||||
self._manual_device_request_timer.stop()
|
||||
|
||||
if self._manual_device_address:
|
||||
if self._plugin_for_manual_device is not None:
|
||||
self._plugin_for_manual_device.removeManualDevice(self._manual_device_address, address = self._manual_device_address)
|
||||
self._manual_device_address = ""
|
||||
self._plugin_for_manual_device = None
|
||||
self.hasManualDeviceRequestInProgressChanged.emit()
|
||||
self.manualDeviceRequestFinished.emit(False)
|
||||
|
||||
def _onManualRequestTimeout(self) -> None:
|
||||
Logger.log("w", "Manual printer [%s] request timed out. Cancel the current request.", self._manual_device_address)
|
||||
self.cancelCurrentManualDeviceRequest()
|
||||
|
||||
hasManualDeviceRequestInProgressChanged = pyqtSignal()
|
||||
|
||||
@pyqtProperty(bool, notify = hasManualDeviceRequestInProgressChanged)
|
||||
def hasManualDeviceRequestInProgress(self) -> bool:
|
||||
return self._manual_device_address != ""
|
||||
|
||||
manualDeviceRequestFinished = pyqtSignal(bool, arguments = ["success"])
|
||||
|
||||
def _onManualDeviceRequestFinished(self, success: bool, address: str) -> None:
|
||||
self._manual_device_request_timer.stop()
|
||||
if address == self._manual_device_address:
|
||||
self._manual_device_address = ""
|
||||
self.hasManualDeviceRequestInProgressChanged.emit()
|
||||
self.manualDeviceRequestFinished.emit(success)
|
||||
|
||||
@pyqtProperty("QVariantMap", notify = discoveredPrintersChanged)
|
||||
def discoveredPrintersByAddress(self) -> Dict[str, DiscoveredPrinter]:
|
||||
return self._discovered_printer_by_ip_dict
|
||||
|
||||
@pyqtProperty("QVariantList", notify = discoveredPrintersChanged)
|
||||
def discoveredPrinters(self) -> List["DiscoveredPrinter"]:
|
||||
item_list = list(
|
||||
x for x in self._discovered_printer_by_ip_dict.values() if not parseBool(x.device.getProperty("temporary")))
|
||||
|
||||
# Split the printers into 2 lists and sort them ascending based on names.
|
||||
available_list = []
|
||||
not_available_list = []
|
||||
for item in item_list:
|
||||
if item.isUnknownMachineType or getattr(item.device, "clusterSize", 1) < 1:
|
||||
not_available_list.append(item)
|
||||
else:
|
||||
available_list.append(item)
|
||||
|
||||
available_list.sort(key = lambda x: x.device.name)
|
||||
not_available_list.sort(key = lambda x: x.device.name)
|
||||
|
||||
return available_list + not_available_list
|
||||
|
||||
def addDiscoveredPrinter(self, ip_address: str, key: str, name: str, create_callback: Callable[[str], None],
|
||||
machine_type: str, device: "NetworkedPrinterOutputDevice") -> None:
|
||||
if ip_address in self._discovered_printer_by_ip_dict:
|
||||
Logger.log("e", "Printer with ip [%s] has already been added", ip_address)
|
||||
return
|
||||
|
||||
discovered_printer = DiscoveredPrinter(ip_address, key, name, create_callback, machine_type, device, parent = self)
|
||||
self._discovered_printer_by_ip_dict[ip_address] = discovered_printer
|
||||
self.discoveredPrintersChanged.emit()
|
||||
|
||||
def updateDiscoveredPrinter(self, ip_address: str,
|
||||
name: Optional[str] = None,
|
||||
machine_type: Optional[str] = None) -> None:
|
||||
if ip_address not in self._discovered_printer_by_ip_dict:
|
||||
Logger.log("w", "Printer with ip [%s] is not known", ip_address)
|
||||
return
|
||||
|
||||
item = self._discovered_printer_by_ip_dict[ip_address]
|
||||
|
||||
if name is not None:
|
||||
item.setName(name)
|
||||
if machine_type is not None:
|
||||
item.setMachineType(machine_type)
|
||||
|
||||
def removeDiscoveredPrinter(self, ip_address: str) -> None:
|
||||
if ip_address not in self._discovered_printer_by_ip_dict:
|
||||
Logger.log("w", "Key [%s] does not exist in the discovered printers list.", ip_address)
|
||||
return
|
||||
|
||||
del self._discovered_printer_by_ip_dict[ip_address]
|
||||
self.discoveredPrintersChanged.emit()
|
||||
|
||||
# A convenience function for QML to create a machine (GlobalStack) out of the given discovered printer.
|
||||
# This function invokes the given discovered printer's "create_callback" to do this.
|
||||
@pyqtSlot("QVariant")
|
||||
def createMachineFromDiscoveredPrinter(self, discovered_printer: "DiscoveredPrinter") -> None:
|
||||
discovered_printer.create_callback(discovered_printer.getKey())
|
|
@ -2,23 +2,25 @@
|
|||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
from PyQt5.QtCore import Qt, pyqtSignal, pyqtProperty, QTimer
|
||||
from typing import Iterable
|
||||
from typing import Iterable, TYPE_CHECKING
|
||||
|
||||
from UM.i18n import i18nCatalog
|
||||
import UM.Qt.ListModel
|
||||
from UM.Qt.ListModel import ListModel
|
||||
from UM.Application import Application
|
||||
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")
|
||||
|
||||
|
||||
## Model that holds extruders.
|
||||
#
|
||||
# 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
|
||||
# settings.
|
||||
class ExtrudersModel(UM.Qt.ListModel.ListModel):
|
||||
class ExtrudersModel(ListModel):
|
||||
# The ID of the container stack for the extruder.
|
||||
IdRole = Qt.UserRole + 1
|
||||
|
||||
|
@ -134,8 +136,8 @@ class ExtrudersModel(UM.Qt.ListModel.ListModel):
|
|||
self._updateExtruders() # Since the new extruders may have different properties, update our own model.
|
||||
|
||||
def _onExtruderStackContainersChanged(self, container):
|
||||
# Update when there is an empty container or material change
|
||||
if container.getMetaDataEntry("type") == "material" or container.getMetaDataEntry("type") is None:
|
||||
# Update when there is an empty container or material or variant change
|
||||
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
|
||||
self._updateExtruders()
|
||||
|
|
@ -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.
|
||||
|
||||
from cura.Machines.Models.BaseMaterialsModel import BaseMaterialsModel
|
||||
import cura.CuraApplication # To listen to changes to the preferences.
|
||||
|
||||
## Model that shows the list of favorite materials.
|
||||
class FavoriteMaterialsModel(BaseMaterialsModel):
|
||||
def __init__(self, parent = None):
|
||||
super().__init__(parent)
|
||||
self._update()
|
||||
cura.CuraApplication.CuraApplication.getInstance().getPreferences().preferenceChanged.connect(self._onFavoritesChanged)
|
||||
self._onChanged()
|
||||
|
||||
## Triggered when any preference changes, but only handles it when the list
|
||||
# of favourites is changed.
|
||||
def _onFavoritesChanged(self, preference_key: str) -> None:
|
||||
if preference_key != "cura/favorite_materials":
|
||||
return
|
||||
self._onChanged()
|
||||
|
||||
def _update(self):
|
||||
if not self._canUpdate():
|
||||
return
|
||||
|
||||
# Get updated list of favorites
|
||||
self._favorite_ids = self._material_manager.getFavorites()
|
||||
super()._update()
|
||||
|
||||
item_list = []
|
||||
|
||||
for root_material_id, container_node in self._available_materials.items():
|
||||
metadata = container_node.getMetadata()
|
||||
|
||||
# Do not include the materials from a to-be-removed package
|
||||
if bool(metadata.get("removed", False)):
|
||||
if bool(container_node.getMetaDataEntry("removed", False)):
|
||||
continue
|
||||
|
||||
# Only add results for favorite materials
|
||||
|
@ -30,6 +35,7 @@ class FavoriteMaterialsModel(BaseMaterialsModel):
|
|||
continue
|
||||
|
||||
item = self._createMaterialItem(root_material_id, container_node)
|
||||
if item:
|
||||
item_list.append(item)
|
||||
|
||||
# Sort the item list alphabetically by name
|
||||
|
|
112
cura/Machines/Models/FirstStartMachineActionsModel.py
Normal file
112
cura/Machines/Models/FirstStartMachineActionsModel.py
Normal file
|
@ -0,0 +1,112 @@
|
|||
# Copyright (c) 2019 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
from typing import Optional, Dict, Any, TYPE_CHECKING
|
||||
|
||||
from PyQt5.QtCore import QObject, Qt, pyqtProperty, pyqtSignal, pyqtSlot
|
||||
|
||||
from UM.Qt.ListModel import ListModel
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from cura.CuraApplication import CuraApplication
|
||||
|
||||
|
||||
#
|
||||
# This model holds all first-start machine actions for the currently active machine. It has 2 roles:
|
||||
# - title : the title/name of the action
|
||||
# - content : the QObject of the QML content of the action
|
||||
# - action : the MachineAction object itself
|
||||
#
|
||||
class FirstStartMachineActionsModel(ListModel):
|
||||
|
||||
TitleRole = Qt.UserRole + 1
|
||||
ContentRole = Qt.UserRole + 2
|
||||
ActionRole = Qt.UserRole + 3
|
||||
|
||||
def __init__(self, application: "CuraApplication", parent: Optional[QObject] = None) -> None:
|
||||
super().__init__(parent)
|
||||
|
||||
self.addRoleName(self.TitleRole, "title")
|
||||
self.addRoleName(self.ContentRole, "content")
|
||||
self.addRoleName(self.ActionRole, "action")
|
||||
|
||||
self._current_action_index = 0
|
||||
|
||||
self._application = application
|
||||
self._application.initializationFinished.connect(self.initialize)
|
||||
|
||||
self._previous_global_stack = None
|
||||
|
||||
def initialize(self) -> None:
|
||||
self._application.getMachineManager().globalContainerChanged.connect(self._update)
|
||||
self._update()
|
||||
|
||||
currentActionIndexChanged = pyqtSignal()
|
||||
allFinished = pyqtSignal() # Emitted when all actions have been finished.
|
||||
|
||||
@pyqtProperty(int, notify = currentActionIndexChanged)
|
||||
def currentActionIndex(self) -> int:
|
||||
return self._current_action_index
|
||||
|
||||
@pyqtProperty("QVariantMap", notify = currentActionIndexChanged)
|
||||
def currentItem(self) -> Optional[Dict[str, Any]]:
|
||||
if self._current_action_index >= self.count:
|
||||
return dict()
|
||||
else:
|
||||
return self.getItem(self._current_action_index)
|
||||
|
||||
@pyqtProperty(bool, notify = currentActionIndexChanged)
|
||||
def hasMoreActions(self) -> bool:
|
||||
return self._current_action_index < self.count - 1
|
||||
|
||||
@pyqtSlot()
|
||||
def goToNextAction(self) -> None:
|
||||
# finish the current item
|
||||
if "action" in self.currentItem:
|
||||
self.currentItem["action"].setFinished()
|
||||
|
||||
if not self.hasMoreActions:
|
||||
self.allFinished.emit()
|
||||
self.reset()
|
||||
return
|
||||
|
||||
self._current_action_index += 1
|
||||
self.currentActionIndexChanged.emit()
|
||||
|
||||
# Resets the current action index to 0 so the wizard panel can show actions from the beginning.
|
||||
@pyqtSlot()
|
||||
def reset(self) -> None:
|
||||
self._current_action_index = 0
|
||||
self.currentActionIndexChanged.emit()
|
||||
|
||||
if self.count == 0:
|
||||
self.allFinished.emit()
|
||||
|
||||
def _update(self) -> None:
|
||||
global_stack = self._application.getMachineManager().activeMachine
|
||||
if global_stack is None:
|
||||
self.setItems([])
|
||||
return
|
||||
|
||||
# Do not update if the machine has not been switched. This can cause the SettingProviders on the Machine
|
||||
# Setting page to do a force update, but they can use potential outdated cached values.
|
||||
if self._previous_global_stack is not None and global_stack.getId() == self._previous_global_stack.getId():
|
||||
return
|
||||
self._previous_global_stack = global_stack
|
||||
|
||||
definition_id = global_stack.definition.getId()
|
||||
first_start_actions = self._application.getMachineActionManager().getFirstStartActions(definition_id)
|
||||
|
||||
item_list = []
|
||||
for item in first_start_actions:
|
||||
item_list.append({"title": item.label,
|
||||
"content": item.getDisplayItem(),
|
||||
"action": item,
|
||||
})
|
||||
item.reset()
|
||||
|
||||
self.setItems(item_list)
|
||||
self.reset()
|
||||
|
||||
|
||||
__all__ = ["FirstStartMachineActionsModel"]
|
|
@ -1,36 +1,32 @@
|
|||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
# Copyright (c) 2019 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
from UM.Logger import Logger
|
||||
from cura.Machines.Models.BaseMaterialsModel import BaseMaterialsModel
|
||||
|
||||
class GenericMaterialsModel(BaseMaterialsModel):
|
||||
|
||||
def __init__(self, parent = None):
|
||||
super().__init__(parent)
|
||||
self._update()
|
||||
self._onChanged()
|
||||
|
||||
def _update(self):
|
||||
if not self._canUpdate():
|
||||
return
|
||||
|
||||
# Get updated list of favorites
|
||||
self._favorite_ids = self._material_manager.getFavorites()
|
||||
super()._update()
|
||||
|
||||
item_list = []
|
||||
|
||||
for root_material_id, container_node in self._available_materials.items():
|
||||
metadata = container_node.getMetadata()
|
||||
|
||||
# Do not include the materials from a to-be-removed package
|
||||
if bool(metadata.get("removed", False)):
|
||||
if bool(container_node.getMetaDataEntry("removed", False)):
|
||||
continue
|
||||
|
||||
# Only add results for generic materials
|
||||
if metadata["brand"].lower() != "generic":
|
||||
if container_node.getMetaDataEntry("brand", "unknown").lower() != "generic":
|
||||
continue
|
||||
|
||||
item = self._createMaterialItem(root_material_id, container_node)
|
||||
if item:
|
||||
item_list.append(item)
|
||||
|
||||
# Sort the item list alphabetically by name
|
||||
|
|
|
@ -1,11 +1,15 @@
|
|||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
from PyQt5.QtCore import Qt
|
||||
from PyQt5.QtCore import Qt, QTimer
|
||||
|
||||
from UM.Qt.ListModel import ListModel
|
||||
from cura.PrinterOutputDevice import ConnectionType
|
||||
from UM.i18n import i18nCatalog
|
||||
from UM.Util import parseBool
|
||||
|
||||
from cura.PrinterOutput.PrinterOutputDevice import ConnectionType
|
||||
from cura.Settings.CuraContainerRegistry import CuraContainerRegistry
|
||||
from cura.Settings.GlobalStack import GlobalStack
|
||||
|
||||
|
||||
class GlobalStacksModel(ListModel):
|
||||
|
@ -14,47 +18,60 @@ class GlobalStacksModel(ListModel):
|
|||
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):
|
||||
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._container_stacks = []
|
||||
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._filter_dict = {}
|
||||
self._update()
|
||||
self._updateDelayed()
|
||||
|
||||
## Handler for container added/removed events from registry
|
||||
def _onContainerChanged(self, container):
|
||||
from cura.Settings.GlobalStack import GlobalStack # otherwise circular imports
|
||||
|
||||
def _onContainerChanged(self, container) -> None:
|
||||
# We only need to update when the added / removed container GlobalStack
|
||||
if isinstance(container, GlobalStack):
|
||||
self._update()
|
||||
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]
|
||||
has_remote_connection |= connection_type in [ConnectionType.NetworkConnection.value,
|
||||
ConnectionType.CloudConnection.value]
|
||||
|
||||
if container_stack.getMetaDataEntry("hidden", False) in ["True", True]:
|
||||
if parseBool(container_stack.getMetaDataEntry("hidden", False)):
|
||||
continue
|
||||
|
||||
section_name = "Network enabled printers" if has_remote_connection else "Local printers"
|
||||
section_name = self._catalog.i18nc("@info:title", section_name)
|
||||
|
||||
items.append({"name": container_stack.getMetaDataEntry("group_name", container_stack.getName()),
|
||||
"id": container_stack.getId(),
|
||||
"hasRemoteConnection": has_remote_connection,
|
||||
"metadata": container_stack.getMetaData().copy()})
|
||||
items.sort(key=lambda i: not i["hasRemoteConnection"])
|
||||
"metadata": container_stack.getMetaData().copy(),
|
||||
"discoverySource": section_name})
|
||||
items.sort(key = lambda i: (not i["hasRemoteConnection"], i["name"]))
|
||||
self.setItems(items)
|
118
cura/Machines/Models/IntentCategoryModel.py
Normal file
118
cura/Machines/Models/IntentCategoryModel.py
Normal file
|
@ -0,0 +1,118 @@
|
|||
#Copyright (c) 2019 Ultimaker B.V.
|
||||
#Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
import collections
|
||||
from PyQt5.QtCore import Qt, QTimer
|
||||
from typing import TYPE_CHECKING, Optional, Dict
|
||||
from cura.Machines.Models.IntentTranslations import intent_translations
|
||||
|
||||
from cura.Machines.Models.IntentModel import IntentModel
|
||||
from cura.Settings.IntentManager import IntentManager
|
||||
from UM.Qt.ListModel import ListModel
|
||||
from UM.Settings.ContainerRegistry import ContainerRegistry #To update the list if anything changes.
|
||||
from PyQt5.QtCore import pyqtProperty, pyqtSignal
|
||||
import cura.CuraApplication
|
||||
if TYPE_CHECKING:
|
||||
from UM.Settings.ContainerRegistry import ContainerInterface
|
||||
|
||||
from UM.i18n import i18nCatalog
|
||||
catalog = i18nCatalog("cura")
|
||||
|
||||
|
||||
## Lists the intent categories that are available for the current printer
|
||||
# configuration.
|
||||
class IntentCategoryModel(ListModel):
|
||||
NameRole = Qt.UserRole + 1
|
||||
IntentCategoryRole = Qt.UserRole + 2
|
||||
WeightRole = Qt.UserRole + 3
|
||||
QualitiesRole = Qt.UserRole + 4
|
||||
DescriptionRole = Qt.UserRole + 5
|
||||
|
||||
modelUpdated = pyqtSignal()
|
||||
|
||||
_translations = collections.OrderedDict() # type: "collections.OrderedDict[str,Dict[str,Optional[str]]]"
|
||||
|
||||
# Translations to user-visible string. Ordered by weight.
|
||||
# TODO: Create a solution for this name and weight to be used dynamically.
|
||||
@classmethod
|
||||
def _get_translations(cls):
|
||||
if len(cls._translations) == 0:
|
||||
cls._translations["default"] = {
|
||||
"name": catalog.i18nc("@label", "Default")
|
||||
}
|
||||
cls._translations["visual"] = {
|
||||
"name": catalog.i18nc("@label", "Visual"),
|
||||
"description": catalog.i18nc("@text", "The visual profile is designed to print visual prototypes and models with the intent of high visual and surface quality.")
|
||||
}
|
||||
cls._translations["engineering"] = {
|
||||
"name": catalog.i18nc("@label", "Engineering"),
|
||||
"description": catalog.i18nc("@text", "The engineering profile is designed to print functional prototypes and end-use parts with the intent of better accuracy and for closer tolerances.")
|
||||
}
|
||||
cls._translations["quick"] = {
|
||||
"name": catalog.i18nc("@label", "Draft"),
|
||||
"description": catalog.i18nc("@text", "The draft profile is designed to print initial prototypes and concept validation with the intent of significant print time reduction.")
|
||||
}
|
||||
return cls._translations
|
||||
|
||||
## Creates a new model for a certain intent category.
|
||||
# \param The category to list the intent profiles for.
|
||||
def __init__(self, intent_category: str) -> None:
|
||||
super().__init__()
|
||||
self._intent_category = intent_category
|
||||
|
||||
self.addRoleName(self.NameRole, "name")
|
||||
self.addRoleName(self.IntentCategoryRole, "intent_category")
|
||||
self.addRoleName(self.WeightRole, "weight")
|
||||
self.addRoleName(self.QualitiesRole, "qualities")
|
||||
self.addRoleName(self.DescriptionRole, "description")
|
||||
|
||||
application = cura.CuraApplication.CuraApplication.getInstance()
|
||||
|
||||
ContainerRegistry.getInstance().containerAdded.connect(self._onContainerChange)
|
||||
ContainerRegistry.getInstance().containerRemoved.connect(self._onContainerChange)
|
||||
machine_manager = cura.CuraApplication.CuraApplication.getInstance().getMachineManager()
|
||||
machine_manager.activeMaterialChanged.connect(self.update)
|
||||
machine_manager.activeVariantChanged.connect(self.update)
|
||||
machine_manager.extruderChanged.connect(self.update)
|
||||
|
||||
extruder_manager = application.getExtruderManager()
|
||||
extruder_manager.extrudersChanged.connect(self.update)
|
||||
|
||||
self._update_timer = QTimer()
|
||||
self._update_timer.setInterval(500)
|
||||
self._update_timer.setSingleShot(True)
|
||||
self._update_timer.timeout.connect(self._update)
|
||||
|
||||
self.update()
|
||||
|
||||
## Updates the list of intents if an intent profile was added or removed.
|
||||
def _onContainerChange(self, container: "ContainerInterface") -> None:
|
||||
if container.getMetaDataEntry("type") == "intent":
|
||||
self.update()
|
||||
|
||||
def update(self):
|
||||
self._update_timer.start()
|
||||
|
||||
## Updates the list of intents.
|
||||
def _update(self) -> None:
|
||||
available_categories = IntentManager.getInstance().currentAvailableIntentCategories()
|
||||
result = []
|
||||
for category in available_categories:
|
||||
qualities = IntentModel()
|
||||
qualities.setIntentCategory(category)
|
||||
result.append({
|
||||
"name": IntentCategoryModel.translation(category, "name", catalog.i18nc("@label", "Unknown")),
|
||||
"description": IntentCategoryModel.translation(category, "description", None),
|
||||
"intent_category": category,
|
||||
"weight": list(IntentCategoryModel._get_translations().keys()).index(category),
|
||||
"qualities": qualities
|
||||
})
|
||||
result.sort(key = lambda k: k["weight"])
|
||||
self.setItems(result)
|
||||
|
||||
## Get a display value for a category.
|
||||
## for categories and keys
|
||||
@staticmethod
|
||||
def translation(category: str, key: str, default: Optional[str] = None):
|
||||
display_strings = IntentCategoryModel._get_translations().get(category, {})
|
||||
return display_strings.get(key, default)
|
135
cura/Machines/Models/IntentModel.py
Normal file
135
cura/Machines/Models/IntentModel.py
Normal file
|
@ -0,0 +1,135 @@
|
|||
# Copyright (c) 2019 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
from typing import Optional, Dict, Any, Set, List
|
||||
|
||||
from PyQt5.QtCore import Qt, QObject, pyqtProperty, pyqtSignal
|
||||
|
||||
import cura.CuraApplication
|
||||
from UM.Qt.ListModel import ListModel
|
||||
from UM.Settings.ContainerRegistry import ContainerRegistry
|
||||
from UM.Logger import Logger
|
||||
from cura.Machines.ContainerTree import ContainerTree
|
||||
from cura.Machines.MaterialNode import MaterialNode
|
||||
from cura.Machines.Models.MachineModelUtils import fetchLayerHeight
|
||||
from cura.Machines.QualityGroup import QualityGroup
|
||||
|
||||
|
||||
class IntentModel(ListModel):
|
||||
NameRole = Qt.UserRole + 1
|
||||
QualityTypeRole = Qt.UserRole + 2
|
||||
LayerHeightRole = Qt.UserRole + 3
|
||||
AvailableRole = Qt.UserRole + 4
|
||||
IntentRole = Qt.UserRole + 5
|
||||
|
||||
def __init__(self, parent: Optional[QObject] = None) -> None:
|
||||
super().__init__(parent)
|
||||
|
||||
self.addRoleName(self.NameRole, "name")
|
||||
self.addRoleName(self.QualityTypeRole, "quality_type")
|
||||
self.addRoleName(self.LayerHeightRole, "layer_height")
|
||||
self.addRoleName(self.AvailableRole, "available")
|
||||
self.addRoleName(self.IntentRole, "intent_category")
|
||||
|
||||
self._intent_category = "engineering"
|
||||
|
||||
machine_manager = cura.CuraApplication.CuraApplication.getInstance().getMachineManager()
|
||||
machine_manager.globalContainerChanged.connect(self._update)
|
||||
machine_manager.extruderChanged.connect(self._update) # We also need to update if an extruder gets disabled
|
||||
ContainerRegistry.getInstance().containerAdded.connect(self._onChanged)
|
||||
ContainerRegistry.getInstance().containerRemoved.connect(self._onChanged)
|
||||
self._layer_height_unit = "" # This is cached
|
||||
self._update()
|
||||
|
||||
intentCategoryChanged = pyqtSignal()
|
||||
|
||||
def setIntentCategory(self, new_category: str) -> None:
|
||||
if self._intent_category != new_category:
|
||||
self._intent_category = new_category
|
||||
self.intentCategoryChanged.emit()
|
||||
self._update()
|
||||
|
||||
@pyqtProperty(str, fset = setIntentCategory, notify = intentCategoryChanged)
|
||||
def intentCategory(self) -> str:
|
||||
return self._intent_category
|
||||
|
||||
def _onChanged(self, container):
|
||||
if container.getMetaDataEntry("type") == "intent":
|
||||
self._update()
|
||||
|
||||
def _update(self) -> None:
|
||||
new_items = [] # type: List[Dict[str, Any]]
|
||||
global_stack = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack()
|
||||
if not global_stack:
|
||||
self.setItems(new_items)
|
||||
return
|
||||
quality_groups = ContainerTree.getInstance().getCurrentQualityGroups()
|
||||
|
||||
material_nodes = self._getActiveMaterials()
|
||||
|
||||
added_quality_type_set = set() # type: Set[str]
|
||||
for material_node in material_nodes:
|
||||
intents = self._getIntentsForMaterial(material_node, quality_groups)
|
||||
for intent in intents:
|
||||
if intent["quality_type"] not in added_quality_type_set:
|
||||
new_items.append(intent)
|
||||
added_quality_type_set.add(intent["quality_type"])
|
||||
|
||||
# Now that we added all intents that we found something for, ensure that we set add ticks (and layer_heights)
|
||||
# for all groups that we don't have anything for (and set it to not available)
|
||||
for quality_type, quality_group in quality_groups.items():
|
||||
# Add the intents that are of the correct category
|
||||
if quality_type not in added_quality_type_set:
|
||||
layer_height = fetchLayerHeight(quality_group)
|
||||
new_items.append({"name": "Unavailable",
|
||||
"quality_type": quality_type,
|
||||
"layer_height": layer_height,
|
||||
"intent_category": self._intent_category,
|
||||
"available": False})
|
||||
added_quality_type_set.add(quality_type)
|
||||
|
||||
new_items = sorted(new_items, key = lambda x: x["layer_height"])
|
||||
self.setItems(new_items)
|
||||
|
||||
## Get the active materials for all extruders. No duplicates will be returned
|
||||
def _getActiveMaterials(self) -> Set["MaterialNode"]:
|
||||
global_stack = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack()
|
||||
if global_stack is None:
|
||||
return set()
|
||||
|
||||
container_tree = ContainerTree.getInstance()
|
||||
machine_node = container_tree.machines[global_stack.definition.getId()]
|
||||
nodes = set() # type: Set[MaterialNode]
|
||||
|
||||
for extruder in global_stack.extruderList:
|
||||
active_variant_name = extruder.variant.getMetaDataEntry("name")
|
||||
if active_variant_name not in machine_node.variants:
|
||||
Logger.log("w", "Could not find the variant %s", active_variant_name)
|
||||
continue
|
||||
active_variant_node = machine_node.variants[active_variant_name]
|
||||
active_material_node = active_variant_node.materials[extruder.material.getMetaDataEntry("base_file")]
|
||||
nodes.add(active_material_node)
|
||||
|
||||
return nodes
|
||||
|
||||
def _getIntentsForMaterial(self, active_material_node: "MaterialNode", quality_groups: Dict[str, "QualityGroup"]) -> List[Dict[str, Any]]:
|
||||
extruder_intents = [] # type: List[Dict[str, Any]]
|
||||
|
||||
for quality_id, quality_node in active_material_node.qualities.items():
|
||||
if quality_node.quality_type not in quality_groups: # Don't add the empty quality type (or anything else that would crash, defensively).
|
||||
continue
|
||||
quality_group = quality_groups[quality_node.quality_type]
|
||||
layer_height = fetchLayerHeight(quality_group)
|
||||
|
||||
for intent_id, intent_node in quality_node.intents.items():
|
||||
if intent_node.intent_category != self._intent_category:
|
||||
continue
|
||||
extruder_intents.append({"name": quality_group.name,
|
||||
"quality_type": quality_group.quality_type,
|
||||
"layer_height": layer_height,
|
||||
"available": quality_group.is_available,
|
||||
"intent_category": self._intent_category
|
||||
})
|
||||
return extruder_intents
|
||||
|
||||
def __repr__(self):
|
||||
return str(self.items)
|
24
cura/Machines/Models/IntentTranslations.py
Normal file
24
cura/Machines/Models/IntentTranslations.py
Normal file
|
@ -0,0 +1,24 @@
|
|||
import collections
|
||||
from typing import Dict, Optional
|
||||
|
||||
from UM.i18n import i18nCatalog
|
||||
from typing import Dict, Optional
|
||||
catalog = i18nCatalog("cura")
|
||||
|
||||
|
||||
intent_translations = collections.OrderedDict() # type: collections.OrderedDict[str, Dict[str, Optional[str]]]
|
||||
intent_translations["default"] = {
|
||||
"name": catalog.i18nc("@label", "Default")
|
||||
}
|
||||
intent_translations["visual"] = {
|
||||
"name": catalog.i18nc("@label", "Visual"),
|
||||
"description": catalog.i18nc("@text", "The visual profile is designed to print visual prototypes and models with the intent of high visual and surface quality.")
|
||||
}
|
||||
intent_translations["engineering"] = {
|
||||
"name": catalog.i18nc("@label", "Engineering"),
|
||||
"description": catalog.i18nc("@text", "The engineering profile is designed to print functional prototypes and end-use parts with the intent of better accuracy and for closer tolerances.")
|
||||
}
|
||||
intent_translations["quick"] = {
|
||||
"name": catalog.i18nc("@label", "Draft"),
|
||||
"description": catalog.i18nc("@text", "The draft profile is designed to print initial prototypes and concept validation with the intent of significant print time reduction.")
|
||||
}
|
|
@ -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("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.get("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)
|
37
cura/Machines/Models/MachineModelUtils.py
Normal file
37
cura/Machines/Models/MachineModelUtils.py
Normal file
|
@ -0,0 +1,37 @@
|
|||
# Copyright (c) 2019 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from UM.Settings.SettingFunction import SettingFunction
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from cura.Machines.QualityGroup import QualityGroup
|
||||
|
||||
layer_height_unit = ""
|
||||
|
||||
|
||||
def fetchLayerHeight(quality_group: "QualityGroup") -> float:
|
||||
from cura.CuraApplication import CuraApplication
|
||||
global_stack = CuraApplication.getInstance().getMachineManager().activeMachine
|
||||
|
||||
default_layer_height = global_stack.definition.getProperty("layer_height", "value")
|
||||
|
||||
# Get layer_height from the quality profile for the GlobalStack
|
||||
if quality_group.node_for_global is None:
|
||||
return float(default_layer_height)
|
||||
container = quality_group.node_for_global.container
|
||||
|
||||
layer_height = default_layer_height
|
||||
if container and container.hasProperty("layer_height", "value"):
|
||||
layer_height = container.getProperty("layer_height", "value")
|
||||
else:
|
||||
# Look for layer_height in the GlobalStack from material -> definition
|
||||
container = global_stack.definition
|
||||
if container and container.hasProperty("layer_height", "value"):
|
||||
layer_height = container.getProperty("layer_height", "value")
|
||||
|
||||
if isinstance(layer_height, SettingFunction):
|
||||
layer_height = layer_height(global_stack)
|
||||
|
||||
return float(layer_height)
|
|
@ -1,9 +1,8 @@
|
|||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
# Copyright (c) 2019 Ultimaker B.V.
|
||||
# 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.Logger import Logger
|
||||
from cura.Machines.Models.BaseMaterialsModel import BaseMaterialsModel
|
||||
|
||||
class MaterialTypesModel(ListModel):
|
||||
|
@ -30,8 +29,7 @@ class MaterialBrandsModel(BaseMaterialsModel):
|
|||
def _update(self):
|
||||
if not self._canUpdate():
|
||||
return
|
||||
# Get updated list of favorites
|
||||
self._favorite_ids = self._material_manager.getFavorites()
|
||||
super()._update()
|
||||
|
||||
brand_item_list = []
|
||||
brand_group_dict = {}
|
||||
|
@ -56,6 +54,7 @@ class MaterialBrandsModel(BaseMaterialsModel):
|
|||
|
||||
# Now handle the individual materials
|
||||
item = self._createMaterialItem(root_material_id, container_node)
|
||||
if item:
|
||||
brand_group_dict[brand][material_type].append(item)
|
||||
|
||||
# Part 2: Organize the tree into models
|
||||
|
|
246
cura/Machines/Models/MaterialManagementModel.py
Normal file
246
cura/Machines/Models/MaterialManagementModel.py
Normal file
|
@ -0,0 +1,246 @@
|
|||
# Copyright (c) 2019 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
import copy # To duplicate materials.
|
||||
from PyQt5.QtCore import QObject, pyqtSignal, pyqtSlot # To allow the preference page proxy to be used from the actual preferences page.
|
||||
from typing import Any, Dict, Optional, TYPE_CHECKING
|
||||
import uuid # To generate new GUIDs for new materials.
|
||||
|
||||
from UM.i18n import i18nCatalog
|
||||
from UM.Logger import Logger
|
||||
from UM.Signal import postponeSignals, CompressTechnique
|
||||
|
||||
import cura.CuraApplication # Imported like this to prevent circular imports.
|
||||
from cura.Machines.ContainerTree import ContainerTree
|
||||
from cura.Settings.CuraContainerRegistry import CuraContainerRegistry # To find the sets of materials belonging to each other, and currently loaded extruder stacks.
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from cura.Machines.MaterialNode import MaterialNode
|
||||
|
||||
catalog = i18nCatalog("cura")
|
||||
|
||||
## Proxy class to the materials page in the preferences.
|
||||
#
|
||||
# This class handles the actions in that page, such as creating new materials,
|
||||
# renaming them, etc.
|
||||
class MaterialManagementModel(QObject):
|
||||
## Triggered when a favorite is added or removed.
|
||||
# \param The base file of the material is provided as parameter when this
|
||||
# emits.
|
||||
favoritesChanged = pyqtSignal(str)
|
||||
|
||||
## Can a certain material be deleted, or is it still in use in one of the
|
||||
# container stacks anywhere?
|
||||
#
|
||||
# We forbid the user from deleting a material if it's in use in any stack.
|
||||
# Deleting it while it's in use can lead to corrupted stacks. In the
|
||||
# future we might enable this functionality again (deleting the material
|
||||
# from those stacks) but for now it is easier to prevent the user from
|
||||
# doing this.
|
||||
# \param material_node The ContainerTree node of the material to check.
|
||||
# \return Whether or not the material can be removed.
|
||||
@pyqtSlot("QVariant", result = bool)
|
||||
def canMaterialBeRemoved(self, material_node: "MaterialNode") -> bool:
|
||||
container_registry = CuraContainerRegistry.getInstance()
|
||||
ids_to_remove = {metadata.get("id", "") for metadata in container_registry.findInstanceContainersMetadata(base_file = material_node.base_file)}
|
||||
for extruder_stack in container_registry.findContainerStacks(type = "extruder_train"):
|
||||
if extruder_stack.material.getId() in ids_to_remove:
|
||||
return False
|
||||
return True
|
||||
|
||||
## Change the user-visible name of a material.
|
||||
# \param material_node The ContainerTree node of the material to rename.
|
||||
# \param name The new name for the material.
|
||||
@pyqtSlot("QVariant", str)
|
||||
def setMaterialName(self, material_node: "MaterialNode", name: str) -> None:
|
||||
container_registry = CuraContainerRegistry.getInstance()
|
||||
root_material_id = material_node.base_file
|
||||
if container_registry.isReadOnly(root_material_id):
|
||||
Logger.log("w", "Cannot set name of read-only container %s.", root_material_id)
|
||||
return
|
||||
return container_registry.findContainers(id = root_material_id)[0].setName(name)
|
||||
|
||||
## Deletes a material from Cura.
|
||||
#
|
||||
# This function does not do any safety checking any more. Please call this
|
||||
# function only if:
|
||||
# - The material is not read-only.
|
||||
# - The material is not used in any stacks.
|
||||
# If the material was not lazy-loaded yet, this will fully load the
|
||||
# container. When removing this material node, all other materials with
|
||||
# the same base fill will also be removed.
|
||||
# \param material_node The material to remove.
|
||||
@pyqtSlot("QVariant")
|
||||
def removeMaterial(self, material_node: "MaterialNode") -> None:
|
||||
container_registry = CuraContainerRegistry.getInstance()
|
||||
materials_this_base_file = container_registry.findContainersMetadata(base_file = material_node.base_file)
|
||||
|
||||
# The material containers belonging to the same material file are supposed to work together. This postponeSignals()
|
||||
# does two things:
|
||||
# - optimizing the signal emitting.
|
||||
# - making sure that the signals will only be emitted after all the material containers have been removed.
|
||||
with postponeSignals(container_registry.containerRemoved, compress = CompressTechnique.CompressPerParameterValue):
|
||||
# CURA-6886: Some containers may not have been loaded. If remove one material container, its material file
|
||||
# will be removed. If later we remove a sub-material container which hasn't been loaded previously, it will
|
||||
# crash because removeContainer() requires to load the container first, but the material file was already
|
||||
# gone.
|
||||
for material_metadata in materials_this_base_file:
|
||||
container_registry.findInstanceContainers(id = material_metadata["id"])
|
||||
for material_metadata in materials_this_base_file:
|
||||
container_registry.removeContainer(material_metadata["id"])
|
||||
|
||||
## Creates a duplicate of a material with the same GUID and base_file
|
||||
# metadata.
|
||||
# \param base_file: The base file of the material to duplicate.
|
||||
# \param new_base_id A new material ID for the base material. The IDs of
|
||||
# the submaterials will be based off this one. If not provided, a material
|
||||
# ID will be generated automatically.
|
||||
# \param new_metadata Metadata for the new material. If not provided, this
|
||||
# will be duplicated from the original material.
|
||||
# \return The root material ID of the duplicate material.
|
||||
def duplicateMaterialByBaseFile(self, base_file: str, new_base_id: Optional[str] = None,
|
||||
new_metadata: Optional[Dict[str, Any]] = None) -> Optional[str]:
|
||||
container_registry = CuraContainerRegistry.getInstance()
|
||||
|
||||
root_materials = container_registry.findContainers(id = base_file)
|
||||
if not root_materials:
|
||||
Logger.log("i", "Unable to duplicate the root material with ID {root_id}, because it doesn't exist.".format(root_id = base_file))
|
||||
return None
|
||||
root_material = root_materials[0]
|
||||
|
||||
# Ensure that all settings are saved.
|
||||
application = cura.CuraApplication.CuraApplication.getInstance()
|
||||
application.saveSettings()
|
||||
|
||||
# Create a new ID and container to hold the data.
|
||||
if new_base_id is None:
|
||||
new_base_id = container_registry.uniqueName(root_material.getId())
|
||||
new_root_material = copy.deepcopy(root_material)
|
||||
new_root_material.getMetaData()["id"] = new_base_id
|
||||
new_root_material.getMetaData()["base_file"] = new_base_id
|
||||
if new_metadata is not None:
|
||||
new_root_material.getMetaData().update(new_metadata)
|
||||
new_containers = [new_root_material]
|
||||
|
||||
# Clone all submaterials.
|
||||
for container_to_copy in container_registry.findInstanceContainers(base_file = base_file):
|
||||
if container_to_copy.getId() == base_file:
|
||||
continue # We already have that one. Skip it.
|
||||
new_id = new_base_id
|
||||
definition = container_to_copy.getMetaDataEntry("definition")
|
||||
if definition != "fdmprinter":
|
||||
new_id += "_" + definition
|
||||
variant_name = container_to_copy.getMetaDataEntry("variant_name")
|
||||
if variant_name:
|
||||
new_id += "_" + variant_name.replace(" ", "_")
|
||||
|
||||
new_container = copy.deepcopy(container_to_copy)
|
||||
new_container.getMetaData()["id"] = new_id
|
||||
new_container.getMetaData()["base_file"] = new_base_id
|
||||
if new_metadata is not None:
|
||||
new_container.getMetaData().update(new_metadata)
|
||||
new_containers.append(new_container)
|
||||
|
||||
# CURA-6863: Nodes in ContainerTree will be updated upon ContainerAdded signals, one at a time. It will use the
|
||||
# best fit material container at the time it sees one. For example, if you duplicate and get generic_pva #2,
|
||||
# if the node update function sees the containers in the following order:
|
||||
#
|
||||
# - generic_pva #2
|
||||
# - generic_pva #2_um3_aa04
|
||||
#
|
||||
# It will first use "generic_pva #2" because that's the best fit it has ever seen, and later "generic_pva #2_um3_aa04"
|
||||
# once it sees that. Because things run in the Qt event loop, they don't happen at the same time. This means if
|
||||
# between those two events, the ContainerTree will have nodes that contain invalid data.
|
||||
#
|
||||
# This sort fixes the problem by emitting the most specific containers first.
|
||||
new_containers = sorted(new_containers, key = lambda x: x.getId(), reverse = True)
|
||||
|
||||
# Optimization. Serving the same purpose as the postponeSignals() in removeMaterial()
|
||||
# postpone the signals emitted when duplicating materials. This is easier on the event loop; changes the
|
||||
# behavior to be like a transaction. Prevents concurrency issues.
|
||||
with postponeSignals(container_registry.containerAdded, compress=CompressTechnique.CompressPerParameterValue):
|
||||
for container_to_add in new_containers:
|
||||
container_to_add.setDirty(True)
|
||||
container_registry.addContainer(container_to_add)
|
||||
|
||||
# If the duplicated material was favorite then the new material should also be added to the favorites.
|
||||
favorites_set = set(application.getPreferences().getValue("cura/favorite_materials").split(";"))
|
||||
if base_file in favorites_set:
|
||||
favorites_set.add(new_base_id)
|
||||
application.getPreferences().setValue("cura/favorite_materials", ";".join(favorites_set))
|
||||
|
||||
return new_base_id
|
||||
|
||||
## Creates a duplicate of a material with the same GUID and base_file
|
||||
# metadata.
|
||||
# \param material_node The node representing the material to duplicate.
|
||||
# \param new_base_id A new material ID for the base material. The IDs of
|
||||
# the submaterials will be based off this one. If not provided, a material
|
||||
# ID will be generated automatically.
|
||||
# \param new_metadata Metadata for the new material. If not provided, this
|
||||
# will be duplicated from the original material.
|
||||
# \return The root material ID of the duplicate material.
|
||||
@pyqtSlot("QVariant", result = str)
|
||||
def duplicateMaterial(self, material_node: "MaterialNode", new_base_id: Optional[str] = None,
|
||||
new_metadata: Optional[Dict[str, Any]] = None) -> Optional[str]:
|
||||
return self.duplicateMaterialByBaseFile(material_node.base_file, new_base_id, new_metadata)
|
||||
|
||||
## Create a new material by cloning the preferred material for the current
|
||||
# material diameter and generate a new GUID.
|
||||
#
|
||||
# The material type is explicitly left to be the one from the preferred
|
||||
# material, since this allows the user to still have SOME profiles to work
|
||||
# with.
|
||||
# \return The ID of the newly created material.
|
||||
@pyqtSlot(result = str)
|
||||
def createMaterial(self) -> str:
|
||||
# Ensure all settings are saved.
|
||||
application = cura.CuraApplication.CuraApplication.getInstance()
|
||||
application.saveSettings()
|
||||
|
||||
# Find the preferred material.
|
||||
extruder_stack = application.getMachineManager().activeStack
|
||||
active_variant_name = extruder_stack.variant.getName()
|
||||
approximate_diameter = int(extruder_stack.approximateMaterialDiameter)
|
||||
global_container_stack = application.getGlobalContainerStack()
|
||||
if not global_container_stack:
|
||||
return ""
|
||||
machine_node = ContainerTree.getInstance().machines[global_container_stack.definition.getId()]
|
||||
preferred_material_node = machine_node.variants[active_variant_name].preferredMaterial(approximate_diameter)
|
||||
|
||||
# Create a new ID & new metadata for the new material.
|
||||
new_id = CuraContainerRegistry.getInstance().uniqueName("custom_material")
|
||||
new_metadata = {"name": catalog.i18nc("@label", "Custom Material"),
|
||||
"brand": catalog.i18nc("@label", "Custom"),
|
||||
"GUID": str(uuid.uuid4()),
|
||||
}
|
||||
|
||||
self.duplicateMaterial(preferred_material_node, new_base_id = new_id, new_metadata = new_metadata)
|
||||
return new_id
|
||||
|
||||
## Adds a certain material to the favorite materials.
|
||||
# \param material_base_file The base file of the material to add.
|
||||
@pyqtSlot(str)
|
||||
def addFavorite(self, material_base_file: str) -> None:
|
||||
application = cura.CuraApplication.CuraApplication.getInstance()
|
||||
favorites = application.getPreferences().getValue("cura/favorite_materials").split(";")
|
||||
if material_base_file not in favorites:
|
||||
favorites.append(material_base_file)
|
||||
application.getPreferences().setValue("cura/favorite_materials", ";".join(favorites))
|
||||
application.saveSettings()
|
||||
self.favoritesChanged.emit(material_base_file)
|
||||
|
||||
## Removes a certain material from the favorite materials.
|
||||
#
|
||||
# If the material was not in the favorite materials, nothing happens.
|
||||
@pyqtSlot(str)
|
||||
def removeFavorite(self, material_base_file: str) -> None:
|
||||
application = cura.CuraApplication.CuraApplication.getInstance()
|
||||
favorites = application.getPreferences().getValue("cura/favorite_materials").split(";")
|
||||
try:
|
||||
favorites.remove(material_base_file)
|
||||
application.getPreferences().setValue("cura/favorite_materials", ";".join(favorites))
|
||||
application.saveSettings()
|
||||
self.favoritesChanged.emit(material_base_file)
|
||||
except ValueError: # Material was not in the favorites list.
|
||||
Logger.log("w", "Material {material_base_file} was already not a favorite material.".format(material_base_file = material_base_file))
|
|
@ -4,6 +4,7 @@
|
|||
from PyQt5.QtCore import QTimer, pyqtSignal, pyqtProperty
|
||||
|
||||
from UM.Application import Application
|
||||
from UM.Scene.Camera import Camera
|
||||
from UM.Scene.Selection import Selection
|
||||
from UM.Qt.ListModel import ListModel
|
||||
|
||||
|
@ -34,6 +35,7 @@ class MultiBuildPlateModel(ListModel):
|
|||
self._active_build_plate = -1
|
||||
|
||||
def setMaxBuildPlate(self, max_build_plate):
|
||||
if self._max_build_plate != max_build_plate:
|
||||
self._max_build_plate = max_build_plate
|
||||
self.maxBuildPlateChanged.emit()
|
||||
|
||||
|
@ -43,6 +45,7 @@ class MultiBuildPlateModel(ListModel):
|
|||
return self._max_build_plate
|
||||
|
||||
def setActiveBuildPlate(self, nr):
|
||||
if self._active_build_plate != nr:
|
||||
self._active_build_plate = nr
|
||||
self.activeBuildPlateChanged.emit()
|
||||
|
||||
|
@ -51,6 +54,7 @@ class MultiBuildPlateModel(ListModel):
|
|||
return self._active_build_plate
|
||||
|
||||
def _updateSelectedObjectBuildPlateNumbersDelayed(self, *args):
|
||||
if not isinstance(args[0], Camera):
|
||||
self._update_timer.start()
|
||||
|
||||
def _updateSelectedObjectBuildPlateNumbers(self, *args):
|
||||
|
|
|
@ -1,14 +1,12 @@
|
|||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
# Copyright (c) 2019 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
from PyQt5.QtCore import Qt
|
||||
|
||||
from UM.Application import Application
|
||||
from UM.Logger import Logger
|
||||
from UM.Qt.ListModel import ListModel
|
||||
from UM.Util import parseBool
|
||||
|
||||
from cura.Machines.VariantType import VariantType
|
||||
import cura.CuraApplication # Imported like this to prevent circular dependencies.
|
||||
from cura.Machines.ContainerTree import ContainerTree
|
||||
|
||||
|
||||
class NozzleModel(ListModel):
|
||||
|
@ -23,33 +21,24 @@ class NozzleModel(ListModel):
|
|||
self.addRoleName(self.HotendNameRole, "hotend_name")
|
||||
self.addRoleName(self.ContainerNodeRole, "container_node")
|
||||
|
||||
self._application = Application.getInstance()
|
||||
self._machine_manager = self._application.getMachineManager()
|
||||
self._variant_manager = self._application.getVariantManager()
|
||||
|
||||
self._machine_manager.globalContainerChanged.connect(self._update)
|
||||
cura.CuraApplication.CuraApplication.getInstance().getMachineManager().globalContainerChanged.connect(self._update)
|
||||
self._update()
|
||||
|
||||
def _update(self):
|
||||
Logger.log("d", "Updating {model_class_name}.".format(model_class_name = self.__class__.__name__))
|
||||
|
||||
global_stack = self._machine_manager.activeMachine
|
||||
global_stack = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack()
|
||||
if global_stack is None:
|
||||
self.setItems([])
|
||||
return
|
||||
machine_node = ContainerTree.getInstance().machines[global_stack.definition.getId()]
|
||||
|
||||
has_variants = parseBool(global_stack.getMetaDataEntry("has_variants", False))
|
||||
if not has_variants:
|
||||
self.setItems([])
|
||||
return
|
||||
|
||||
variant_node_dict = self._variant_manager.getVariantNodes(global_stack, VariantType.NOZZLE)
|
||||
if not variant_node_dict:
|
||||
if not machine_node.has_variants:
|
||||
self.setItems([])
|
||||
return
|
||||
|
||||
item_list = []
|
||||
for hotend_name, container_node in sorted(variant_node_dict.items(), key = lambda i: i[0].upper()):
|
||||
for hotend_name, container_node in sorted(machine_node.variants.items(), key = lambda i: i[0].upper()):
|
||||
item = {"id": hotend_name,
|
||||
"hotend_name": hotend_name,
|
||||
"container_node": container_node
|
||||
|
|
|
@ -1,10 +1,30 @@
|
|||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
# Copyright (c) 2019 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
from PyQt5.QtCore import Qt, pyqtSlot
|
||||
from typing import Any, cast, Dict, Optional, TYPE_CHECKING
|
||||
from PyQt5.QtCore import pyqtSlot, QObject, Qt, QTimer
|
||||
|
||||
from UM.Qt.ListModel import ListModel
|
||||
from UM.Logger import Logger
|
||||
from UM.Qt.ListModel import ListModel
|
||||
from UM.Settings.InstanceContainer import InstanceContainer # To create new profiles.
|
||||
|
||||
import cura.CuraApplication # Imported this way to prevent circular imports.
|
||||
from cura.Settings.ContainerManager import ContainerManager
|
||||
from cura.Machines.ContainerTree import ContainerTree
|
||||
from cura.Settings.cura_empty_instance_containers import empty_quality_changes_container
|
||||
from cura.Settings.IntentManager import IntentManager
|
||||
from cura.Machines.Models.MachineModelUtils import fetchLayerHeight
|
||||
from cura.Machines.Models.IntentTranslations import intent_translations
|
||||
|
||||
from UM.i18n import i18nCatalog
|
||||
catalog = i18nCatalog("cura")
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from UM.Settings.Interfaces import ContainerInterface
|
||||
from cura.Machines.QualityChangesGroup import QualityChangesGroup
|
||||
from cura.Settings.ExtruderStack import ExtruderStack
|
||||
from cura.Settings.GlobalStack import GlobalStack
|
||||
|
||||
|
||||
#
|
||||
# This the QML model for the quality management page.
|
||||
|
@ -13,27 +33,258 @@ class QualityManagementModel(ListModel):
|
|||
NameRole = Qt.UserRole + 1
|
||||
IsReadOnlyRole = Qt.UserRole + 2
|
||||
QualityGroupRole = Qt.UserRole + 3
|
||||
QualityChangesGroupRole = Qt.UserRole + 4
|
||||
QualityTypeRole = Qt.UserRole + 4
|
||||
QualityChangesGroupRole = Qt.UserRole + 5
|
||||
IntentCategoryRole = Qt.UserRole + 6
|
||||
SectionNameRole = Qt.UserRole + 7
|
||||
|
||||
def __init__(self, parent = None):
|
||||
def __init__(self, parent: Optional["QObject"] = None) -> None:
|
||||
super().__init__(parent)
|
||||
|
||||
self.addRoleName(self.NameRole, "name")
|
||||
self.addRoleName(self.IsReadOnlyRole, "is_read_only")
|
||||
self.addRoleName(self.QualityGroupRole, "quality_group")
|
||||
self.addRoleName(self.QualityTypeRole, "quality_type")
|
||||
self.addRoleName(self.QualityChangesGroupRole, "quality_changes_group")
|
||||
self.addRoleName(self.IntentCategoryRole, "intent_category")
|
||||
self.addRoleName(self.SectionNameRole, "section_name")
|
||||
|
||||
from cura.CuraApplication import CuraApplication
|
||||
self._container_registry = CuraApplication.getInstance().getContainerRegistry()
|
||||
self._machine_manager = CuraApplication.getInstance().getMachineManager()
|
||||
self._extruder_manager = CuraApplication.getInstance().getExtruderManager()
|
||||
self._quality_manager = CuraApplication.getInstance().getQualityManager()
|
||||
application = cura.CuraApplication.CuraApplication.getInstance()
|
||||
container_registry = application.getContainerRegistry()
|
||||
self._machine_manager = application.getMachineManager()
|
||||
self._machine_manager.activeQualityGroupChanged.connect(self._onChange)
|
||||
self._machine_manager.activeStackChanged.connect(self._onChange)
|
||||
self._machine_manager.extruderChanged.connect(self._onChange)
|
||||
self._machine_manager.globalContainerChanged.connect(self._onChange)
|
||||
|
||||
self._machine_manager.globalContainerChanged.connect(self._update)
|
||||
self._quality_manager.qualitiesUpdated.connect(self._update)
|
||||
self._extruder_manager = application.getExtruderManager()
|
||||
self._extruder_manager.extrudersChanged.connect(self._onChange)
|
||||
|
||||
container_registry.containerAdded.connect(self._qualityChangesListChanged)
|
||||
container_registry.containerRemoved.connect(self._qualityChangesListChanged)
|
||||
container_registry.containerMetaDataChanged.connect(self._qualityChangesListChanged)
|
||||
|
||||
self._update_timer = 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()
|
||||
|
||||
## Deletes a custom profile. It will be gone forever.
|
||||
# \param quality_changes_group The quality changes group representing the
|
||||
# profile to delete.
|
||||
@pyqtSlot(QObject)
|
||||
def removeQualityChangesGroup(self, quality_changes_group: "QualityChangesGroup") -> None:
|
||||
Logger.log("i", "Removing quality changes group {group_name}".format(group_name = quality_changes_group.name))
|
||||
removed_quality_changes_ids = set()
|
||||
container_registry = cura.CuraApplication.CuraApplication.getInstance().getContainerRegistry()
|
||||
for metadata in [quality_changes_group.metadata_for_global] + list(quality_changes_group.metadata_per_extruder.values()):
|
||||
container_id = metadata["id"]
|
||||
container_registry.removeContainer(container_id)
|
||||
removed_quality_changes_ids.add(container_id)
|
||||
|
||||
# Reset all machines that have activated this custom profile.
|
||||
for global_stack in container_registry.findContainerStacks(type = "machine"):
|
||||
if global_stack.qualityChanges.getId() in removed_quality_changes_ids:
|
||||
global_stack.qualityChanges = empty_quality_changes_container
|
||||
for extruder_stack in container_registry.findContainerStacks(type = "extruder_train"):
|
||||
if extruder_stack.qualityChanges.getId() in removed_quality_changes_ids:
|
||||
extruder_stack.qualityChanges = empty_quality_changes_container
|
||||
|
||||
## Rename a custom profile.
|
||||
#
|
||||
# Because the names must be unique, the new name may not actually become
|
||||
# the name that was given. The actual name is returned by this function.
|
||||
# \param quality_changes_group The custom profile that must be renamed.
|
||||
# \param new_name The desired name for the profile.
|
||||
# \return The actual new name of the profile, after making the name
|
||||
# unique.
|
||||
@pyqtSlot(QObject, str, result = str)
|
||||
def renameQualityChangesGroup(self, quality_changes_group: "QualityChangesGroup", new_name: str) -> str:
|
||||
Logger.log("i", "Renaming QualityChangesGroup {old_name} to {new_name}.".format(old_name = quality_changes_group.name, new_name = new_name))
|
||||
if new_name == quality_changes_group.name:
|
||||
Logger.log("i", "QualityChangesGroup name {name} unchanged.".format(name = quality_changes_group.name))
|
||||
return new_name
|
||||
|
||||
application = cura.CuraApplication.CuraApplication.getInstance()
|
||||
container_registry = application.getContainerRegistry()
|
||||
new_name = container_registry.uniqueName(new_name)
|
||||
# CURA-6842
|
||||
# FIXME: setName() will trigger metaDataChanged signal that are connected with type Qt.AutoConnection. In this
|
||||
# case, setName() will trigger direct connections which in turn causes the quality changes group and the models
|
||||
# to update. Because multiple containers need to be renamed, and every time a container gets renamed, updates
|
||||
# gets triggered and this results in partial updates. For example, if we rename the global quality changes
|
||||
# container first, the rest of the system still thinks that I have selected "my_profile" instead of
|
||||
# "my_new_profile", but an update already gets triggered, and the quality changes group that's selected will
|
||||
# have no container for the global stack, because "my_profile" just got renamed to "my_new_profile". This results
|
||||
# in crashes because the rest of the system assumes that all data in a QualityChangesGroup will be correct.
|
||||
#
|
||||
# Renaming the container for the global stack in the end seems to be ok, because the assumption is mostly based
|
||||
# on the quality changes container for the global stack.
|
||||
for metadata in quality_changes_group.metadata_per_extruder.values():
|
||||
extruder_container = cast(InstanceContainer, container_registry.findContainers(id = metadata["id"])[0])
|
||||
extruder_container.setName(new_name)
|
||||
global_container = cast(InstanceContainer, container_registry.findContainers(id=quality_changes_group.metadata_for_global["id"])[0])
|
||||
global_container.setName(new_name)
|
||||
|
||||
quality_changes_group.name = new_name
|
||||
|
||||
application.getMachineManager().activeQualityChanged.emit()
|
||||
application.getMachineManager().activeQualityGroupChanged.emit()
|
||||
|
||||
return new_name
|
||||
|
||||
## Duplicates a given quality profile OR quality changes profile.
|
||||
# \param new_name The desired name of the new profile. This will be made
|
||||
# unique, so it might end up with a different name.
|
||||
# \param quality_model_item The item of this model to duplicate, as
|
||||
# dictionary. See the descriptions of the roles of this list model.
|
||||
@pyqtSlot(str, "QVariantMap")
|
||||
def duplicateQualityChanges(self, new_name: str, quality_model_item: Dict[str, Any]) -> None:
|
||||
global_stack = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack()
|
||||
if not global_stack:
|
||||
Logger.log("i", "No active global stack, cannot duplicate quality (changes) profile.")
|
||||
return
|
||||
|
||||
container_registry = cura.CuraApplication.CuraApplication.getInstance().getContainerRegistry()
|
||||
new_name = container_registry.uniqueName(new_name)
|
||||
|
||||
intent_category = quality_model_item["intent_category"]
|
||||
quality_group = quality_model_item["quality_group"]
|
||||
quality_changes_group = quality_model_item["quality_changes_group"]
|
||||
if quality_changes_group is None:
|
||||
# Create global quality changes only.
|
||||
new_quality_changes = self._createQualityChanges(quality_group.quality_type, intent_category, new_name,
|
||||
global_stack, extruder_stack = None)
|
||||
container_registry.addContainer(new_quality_changes)
|
||||
else:
|
||||
for metadata in [quality_changes_group.metadata_for_global] + list(quality_changes_group.metadata_per_extruder.values()):
|
||||
containers = container_registry.findContainers(id = metadata["id"])
|
||||
if not containers:
|
||||
continue
|
||||
container = containers[0]
|
||||
new_id = container_registry.uniqueName(container.getId())
|
||||
container_registry.addContainer(container.duplicate(new_id, new_name))
|
||||
|
||||
## Create quality changes containers from the user containers in the active
|
||||
# stacks.
|
||||
#
|
||||
# This will go through the global and extruder stacks and create
|
||||
# quality_changes containers from the user containers in each stack. These
|
||||
# then replace the quality_changes containers in the stack and clear the
|
||||
# user settings.
|
||||
# \param base_name The new name for the quality changes profile. The final
|
||||
# name of the profile might be different from this, because it needs to be
|
||||
# made unique.
|
||||
@pyqtSlot(str)
|
||||
def createQualityChanges(self, base_name: str) -> None:
|
||||
machine_manager = cura.CuraApplication.CuraApplication.getInstance().getMachineManager()
|
||||
|
||||
global_stack = machine_manager.activeMachine
|
||||
if not global_stack:
|
||||
return
|
||||
|
||||
active_quality_name = machine_manager.activeQualityOrQualityChangesName
|
||||
if active_quality_name == "":
|
||||
Logger.log("w", "No quality container found in stack %s, cannot create profile", global_stack.getId())
|
||||
return
|
||||
|
||||
machine_manager.blurSettings.emit()
|
||||
if base_name is None or base_name == "":
|
||||
base_name = active_quality_name
|
||||
container_registry = cura.CuraApplication.CuraApplication.getInstance().getContainerRegistry()
|
||||
unique_name = container_registry.uniqueName(base_name)
|
||||
|
||||
# Go through the active stacks and create quality_changes containers from the user containers.
|
||||
container_manager = ContainerManager.getInstance()
|
||||
stack_list = [global_stack] + list(global_stack.extruders.values())
|
||||
for stack in stack_list:
|
||||
quality_container = stack.quality
|
||||
quality_changes_container = stack.qualityChanges
|
||||
if not quality_container or not quality_changes_container:
|
||||
Logger.log("w", "No quality or quality changes container found in stack %s, ignoring it", stack.getId())
|
||||
continue
|
||||
|
||||
extruder_stack = None
|
||||
intent_category = None
|
||||
if stack.getMetaDataEntry("position") is not None:
|
||||
extruder_stack = stack
|
||||
intent_category = stack.intent.getMetaDataEntry("intent_category")
|
||||
new_changes = self._createQualityChanges(quality_container.getMetaDataEntry("quality_type"), intent_category, unique_name, global_stack, extruder_stack)
|
||||
container_manager._performMerge(new_changes, quality_changes_container, clear_settings = False)
|
||||
container_manager._performMerge(new_changes, stack.userChanges)
|
||||
|
||||
container_registry.addContainer(new_changes)
|
||||
|
||||
## Create a quality changes container with the given set-up.
|
||||
# \param quality_type The quality type of the new container.
|
||||
# \param intent_category The intent category of the new container.
|
||||
# \param new_name The name of the container. This name must be unique.
|
||||
# \param machine The global stack to create the profile for.
|
||||
# \param extruder_stack The extruder stack to create the profile for. If
|
||||
# not provided, only a global container will be created.
|
||||
def _createQualityChanges(self, quality_type: str, intent_category: Optional[str], new_name: str, machine: "GlobalStack", extruder_stack: Optional["ExtruderStack"]) -> "InstanceContainer":
|
||||
container_registry = cura.CuraApplication.CuraApplication.getInstance().getContainerRegistry()
|
||||
base_id = machine.definition.getId() if extruder_stack is None else extruder_stack.getId()
|
||||
new_id = base_id + "_" + new_name
|
||||
new_id = new_id.lower().replace(" ", "_")
|
||||
new_id = container_registry.uniqueName(new_id)
|
||||
|
||||
# Create a new quality_changes container for the quality.
|
||||
quality_changes = InstanceContainer(new_id)
|
||||
quality_changes.setName(new_name)
|
||||
quality_changes.setMetaDataEntry("type", "quality_changes")
|
||||
quality_changes.setMetaDataEntry("quality_type", quality_type)
|
||||
if intent_category is not None:
|
||||
quality_changes.setMetaDataEntry("intent_category", intent_category)
|
||||
|
||||
# If we are creating a container for an extruder, ensure we add that to the container.
|
||||
if extruder_stack is not None:
|
||||
quality_changes.setMetaDataEntry("position", extruder_stack.getMetaDataEntry("position"))
|
||||
|
||||
# If the machine specifies qualities should be filtered, ensure we match the current criteria.
|
||||
machine_definition_id = ContainerTree.getInstance().machines[machine.definition.getId()].quality_definition
|
||||
quality_changes.setDefinition(machine_definition_id)
|
||||
|
||||
quality_changes.setMetaDataEntry("setting_version", cura.CuraApplication.CuraApplication.getInstance().SettingVersion)
|
||||
return quality_changes
|
||||
|
||||
## Triggered when any container changed.
|
||||
#
|
||||
# This filters the updates to the container manager: When it applies to
|
||||
# the list of quality changes, we need to update our list.
|
||||
def _qualityChangesListChanged(self, container: "ContainerInterface") -> None:
|
||||
if container.getMetaDataEntry("type") == "quality_changes":
|
||||
self._update()
|
||||
|
||||
@pyqtSlot("QVariantMap", result = str)
|
||||
def getQualityItemDisplayName(self, quality_model_item: Dict[str, Any]) -> str:
|
||||
quality_group = quality_model_item["quality_group"]
|
||||
is_read_only = quality_model_item["is_read_only"]
|
||||
intent_category = quality_model_item["intent_category"]
|
||||
|
||||
quality_level_name = "Not Supported"
|
||||
if quality_group is not None:
|
||||
quality_level_name = quality_group.name
|
||||
|
||||
display_name = quality_level_name
|
||||
|
||||
if intent_category != "default":
|
||||
intent_display_name = catalog.i18nc("@label", intent_category.capitalize())
|
||||
display_name = "{intent_name} - {the_rest}".format(intent_name = intent_display_name,
|
||||
the_rest = display_name)
|
||||
|
||||
# A custom quality
|
||||
if not is_read_only:
|
||||
display_name = "{custom_profile_name} - {the_rest}".format(custom_profile_name = quality_model_item["name"],
|
||||
the_rest = display_name)
|
||||
|
||||
return display_name
|
||||
|
||||
def _update(self):
|
||||
Logger.log("d", "Updating {model_class_name}.".format(model_class_name = self.__class__.__name__))
|
||||
|
||||
|
@ -42,38 +293,71 @@ class QualityManagementModel(ListModel):
|
|||
self.setItems([])
|
||||
return
|
||||
|
||||
quality_group_dict = self._quality_manager.getQualityGroups(global_stack)
|
||||
quality_changes_group_dict = self._quality_manager.getQualityChangesGroups(global_stack)
|
||||
container_tree = ContainerTree.getInstance()
|
||||
quality_group_dict = container_tree.getCurrentQualityGroups()
|
||||
quality_changes_group_list = container_tree.getCurrentQualityChangesGroups()
|
||||
|
||||
available_quality_types = set(quality_type for quality_type, quality_group in quality_group_dict.items()
|
||||
if quality_group.is_available)
|
||||
if not available_quality_types and not quality_changes_group_dict:
|
||||
if not available_quality_types and not quality_changes_group_list:
|
||||
# Nothing to show
|
||||
self.setItems([])
|
||||
return
|
||||
|
||||
item_list = []
|
||||
# Create quality group items
|
||||
# Create quality group items (intent category = "default")
|
||||
for quality_group in quality_group_dict.values():
|
||||
if not quality_group.is_available:
|
||||
continue
|
||||
|
||||
layer_height = fetchLayerHeight(quality_group)
|
||||
|
||||
item = {"name": quality_group.name,
|
||||
"is_read_only": True,
|
||||
"quality_group": quality_group,
|
||||
"quality_changes_group": None}
|
||||
"quality_type": quality_group.quality_type,
|
||||
"quality_changes_group": None,
|
||||
"intent_category": "default",
|
||||
"section_name": catalog.i18nc("@label", "Default"),
|
||||
"layer_height": layer_height, # layer_height is only used for sorting
|
||||
}
|
||||
item_list.append(item)
|
||||
# Sort by quality names
|
||||
item_list = sorted(item_list, key = lambda x: x["name"].upper())
|
||||
# Sort by layer_height for built-in qualities
|
||||
item_list = sorted(item_list, key = lambda x: x["layer_height"])
|
||||
|
||||
# Create intent items (non-default)
|
||||
available_intent_list = IntentManager.getInstance().getCurrentAvailableIntents()
|
||||
available_intent_list = [i for i in available_intent_list if i[0] != "default"]
|
||||
result = []
|
||||
for intent_category, quality_type in available_intent_list:
|
||||
result.append({
|
||||
"name": quality_group_dict[quality_type].name, # Use the quality name as the display name
|
||||
"is_read_only": True,
|
||||
"quality_group": quality_group_dict[quality_type],
|
||||
"quality_type": quality_type,
|
||||
"quality_changes_group": None,
|
||||
"intent_category": intent_category,
|
||||
"section_name": catalog.i18nc("@label", intent_translations.get(intent_category, {}).get("name", catalog.i18nc("@label", "Unknown"))),
|
||||
})
|
||||
# Sort by quality_type for each intent category
|
||||
|
||||
result = sorted(result, key = lambda x: (list(intent_translations).index(x["intent_category"]), x["quality_type"]))
|
||||
item_list += result
|
||||
|
||||
# Create quality_changes group items
|
||||
quality_changes_item_list = []
|
||||
for quality_changes_group in quality_changes_group_dict.values():
|
||||
for quality_changes_group in quality_changes_group_list:
|
||||
# CURA-6913 Note that custom qualities can be based on "not supported", so the quality group can be None.
|
||||
quality_group = quality_group_dict.get(quality_changes_group.quality_type)
|
||||
quality_type = quality_changes_group.quality_type
|
||||
item = {"name": quality_changes_group.name,
|
||||
"is_read_only": False,
|
||||
"quality_group": quality_group,
|
||||
"quality_changes_group": quality_changes_group}
|
||||
"quality_type": quality_type,
|
||||
"quality_changes_group": quality_changes_group,
|
||||
"intent_category": quality_changes_group.intent_category,
|
||||
"section_name": catalog.i18nc("@label", "Custom profiles"),
|
||||
}
|
||||
quality_changes_item_list.append(item)
|
||||
|
||||
# Sort quality_changes items by names and append to the item list
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
# Copyright (c) 2019 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
from PyQt5.QtCore import Qt, QTimer
|
||||
|
||||
from UM.Application import Application
|
||||
import cura.CuraApplication # Imported this way to prevent circular dependencies.
|
||||
from UM.Logger import Logger
|
||||
from UM.Qt.ListModel import ListModel
|
||||
from UM.Settings.SettingFunction import SettingFunction
|
||||
from cura.Machines.ContainerTree import ContainerTree
|
||||
from cura.Machines.Models.MachineModelUtils import fetchLayerHeight
|
||||
|
||||
from cura.Machines.QualityManager import QualityGroup
|
||||
|
||||
#
|
||||
# QML Model for all built-in quality profiles. This model is used for the drop-down quality menu.
|
||||
|
@ -35,14 +35,17 @@ class QualityProfilesDropDownMenuModel(ListModel):
|
|||
self.addRoleName(self.QualityChangesGroupRole, "quality_changes_group")
|
||||
self.addRoleName(self.IsExperimentalRole, "is_experimental")
|
||||
|
||||
self._application = Application.getInstance()
|
||||
self._machine_manager = self._application.getMachineManager()
|
||||
self._quality_manager = Application.getInstance().getQualityManager()
|
||||
application = cura.CuraApplication.CuraApplication.getInstance()
|
||||
machine_manager = application.getMachineManager()
|
||||
|
||||
self._application.globalContainerStackChanged.connect(self._onChange)
|
||||
self._machine_manager.activeQualityGroupChanged.connect(self._onChange)
|
||||
self._machine_manager.extruderChanged.connect(self._onChange)
|
||||
self._quality_manager.qualitiesUpdated.connect(self._onChange)
|
||||
application.globalContainerStackChanged.connect(self._onChange)
|
||||
machine_manager.activeQualityGroupChanged.connect(self._onChange)
|
||||
machine_manager.activeMaterialChanged.connect(self._onChange)
|
||||
machine_manager.activeVariantChanged.connect(self._onChange)
|
||||
machine_manager.extruderChanged.connect(self._onChange)
|
||||
|
||||
extruder_manager = application.getExtruderManager()
|
||||
extruder_manager.extrudersChanged.connect(self._onChange)
|
||||
|
||||
self._layer_height_unit = "" # This is cached
|
||||
|
||||
|
@ -51,7 +54,7 @@ class QualityProfilesDropDownMenuModel(ListModel):
|
|||
self._update_timer.setSingleShot(True)
|
||||
self._update_timer.timeout.connect(self._update)
|
||||
|
||||
self._update()
|
||||
self._onChange()
|
||||
|
||||
def _onChange(self) -> None:
|
||||
self._update_timer.start()
|
||||
|
@ -59,25 +62,36 @@ class QualityProfilesDropDownMenuModel(ListModel):
|
|||
def _update(self):
|
||||
Logger.log("d", "Updating {model_class_name}.".format(model_class_name = self.__class__.__name__))
|
||||
|
||||
global_stack = self._machine_manager.activeMachine
|
||||
# CURA-6836
|
||||
# LabelBar is a repeater that creates labels for quality layer heights. Because of an optimization in
|
||||
# UM.ListModel, the model will not remove all items and recreate new ones every time there's an update.
|
||||
# Because LabelBar uses Repeater with Labels anchoring to "undefined" in certain cases, the anchoring will be
|
||||
# kept the same as before.
|
||||
self.setItems([])
|
||||
|
||||
global_stack = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack()
|
||||
if global_stack is None:
|
||||
self.setItems([])
|
||||
Logger.log("d", "No active GlobalStack, set quality profile model as empty.")
|
||||
return
|
||||
|
||||
if not self._layer_height_unit:
|
||||
unit = global_stack.definition.getProperty("layer_height", "unit")
|
||||
if not unit:
|
||||
unit = ""
|
||||
self._layer_height_unit = unit
|
||||
|
||||
# Check for material compatibility
|
||||
if not self._machine_manager.activeMaterialsCompatible():
|
||||
if not cura.CuraApplication.CuraApplication.getInstance().getMachineManager().activeMaterialsCompatible():
|
||||
Logger.log("d", "No active material compatibility, set quality profile model as empty.")
|
||||
self.setItems([])
|
||||
return
|
||||
|
||||
quality_group_dict = self._quality_manager.getQualityGroups(global_stack)
|
||||
quality_group_dict = ContainerTree.getInstance().getCurrentQualityGroups()
|
||||
|
||||
item_list = []
|
||||
for key in sorted(quality_group_dict):
|
||||
quality_group = quality_group_dict[key]
|
||||
|
||||
layer_height = self._fetchLayerHeight(quality_group)
|
||||
for quality_group in quality_group_dict.values():
|
||||
layer_height = fetchLayerHeight(quality_group)
|
||||
|
||||
item = {"name": quality_group.name,
|
||||
"quality_type": quality_group.quality_type,
|
||||
|
@ -93,32 +107,3 @@ class QualityProfilesDropDownMenuModel(ListModel):
|
|||
item_list = sorted(item_list, key = lambda x: x["layer_height"])
|
||||
|
||||
self.setItems(item_list)
|
||||
|
||||
def _fetchLayerHeight(self, quality_group: "QualityGroup") -> float:
|
||||
global_stack = self._machine_manager.activeMachine
|
||||
if not self._layer_height_unit:
|
||||
unit = global_stack.definition.getProperty("layer_height", "unit")
|
||||
if not unit:
|
||||
unit = ""
|
||||
self._layer_height_unit = unit
|
||||
|
||||
default_layer_height = global_stack.definition.getProperty("layer_height", "value")
|
||||
|
||||
# Get layer_height from the quality profile for the GlobalStack
|
||||
if quality_group.node_for_global is None:
|
||||
return float(default_layer_height)
|
||||
container = quality_group.node_for_global.getContainer()
|
||||
|
||||
layer_height = default_layer_height
|
||||
if container and container.hasProperty("layer_height", "value"):
|
||||
layer_height = container.getProperty("layer_height", "value")
|
||||
else:
|
||||
# Look for layer_height in the GlobalStack from material -> definition
|
||||
container = global_stack.definition
|
||||
if container and container.hasProperty("layer_height", "value"):
|
||||
layer_height = container.getProperty("layer_height", "value")
|
||||
|
||||
if isinstance(layer_height, SettingFunction):
|
||||
layer_height = layer_height(global_stack)
|
||||
|
||||
return float(layer_height)
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
# Copyright (c) 2019 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
from PyQt5.QtCore import pyqtProperty, pyqtSignal, Qt
|
||||
|
||||
from UM.Application import Application
|
||||
import cura.CuraApplication
|
||||
from UM.Logger import Logger
|
||||
from UM.Qt.ListModel import ListModel
|
||||
from UM.Settings.ContainerRegistry import ContainerRegistry
|
||||
|
@ -35,15 +35,13 @@ class QualitySettingsModel(ListModel):
|
|||
self.addRoleName(self.CategoryRole, "category")
|
||||
|
||||
self._container_registry = ContainerRegistry.getInstance()
|
||||
self._application = Application.getInstance()
|
||||
self._quality_manager = self._application.getQualityManager()
|
||||
self._application = cura.CuraApplication.CuraApplication.getInstance()
|
||||
self._application.getMachineManager().activeStackChanged.connect(self._update)
|
||||
|
||||
self._selected_position = self.GLOBAL_STACK_POSITION #Must be either GLOBAL_STACK_POSITION or an extruder position (0, 1, etc.)
|
||||
self._selected_quality_item = None # The selected quality in the quality management page
|
||||
self._i18n_catalog = None
|
||||
|
||||
self._quality_manager.qualitiesUpdated.connect(self._update)
|
||||
|
||||
self._update()
|
||||
|
||||
selectedPositionChanged = pyqtSignal()
|
||||
|
@ -93,21 +91,33 @@ class QualitySettingsModel(ListModel):
|
|||
quality_node = quality_group.nodes_for_extruders.get(str(self._selected_position))
|
||||
settings_keys = quality_group.getAllKeys()
|
||||
quality_containers = []
|
||||
if quality_node is not None and quality_node.getContainer() is not None:
|
||||
quality_containers.append(quality_node.getContainer())
|
||||
if quality_node is not None and quality_node.container is not None:
|
||||
quality_containers.append(quality_node.container)
|
||||
|
||||
# Here, if the user has selected a quality changes, then "quality_changes_group" will not be None, and we fetch
|
||||
# the settings in that quality_changes_group.
|
||||
if quality_changes_group is not None:
|
||||
if self._selected_position == self.GLOBAL_STACK_POSITION:
|
||||
quality_changes_node = quality_changes_group.node_for_global
|
||||
container_registry = ContainerRegistry.getInstance()
|
||||
global_containers = container_registry.findContainers(id = quality_changes_group.metadata_for_global["id"])
|
||||
global_container = None if len(global_containers) == 0 else global_containers[0]
|
||||
extruders_containers = {pos: container_registry.findContainers(id = quality_changes_group.metadata_per_extruder[pos]["id"]) for pos in quality_changes_group.metadata_per_extruder}
|
||||
extruders_container = {pos: None if not containers else containers[0] for pos, containers in extruders_containers.items()}
|
||||
if self._selected_position == self.GLOBAL_STACK_POSITION and global_container:
|
||||
quality_changes_metadata = global_container.getMetaData()
|
||||
else:
|
||||
quality_changes_node = quality_changes_group.nodes_for_extruders.get(str(self._selected_position))
|
||||
if quality_changes_node is not None and quality_changes_node.getContainer() is not None: # it can be None if number of extruders are changed during runtime
|
||||
quality_containers.insert(0, quality_changes_node.getContainer())
|
||||
settings_keys.update(quality_changes_group.getAllKeys())
|
||||
quality_changes_metadata = extruders_container.get(str(self._selected_position))
|
||||
if quality_changes_metadata is not None: # It can be None if number of extruders are changed during runtime.
|
||||
container = container_registry.findContainers(id = quality_changes_metadata["id"])
|
||||
if container:
|
||||
quality_containers.insert(0, container[0])
|
||||
|
||||
# We iterate over all definitions instead of settings in a quality/qualtiy_changes group is because in the GUI,
|
||||
if global_container:
|
||||
settings_keys.update(global_container.getAllKeys())
|
||||
for container in extruders_container.values():
|
||||
if container:
|
||||
settings_keys.update(container.getAllKeys())
|
||||
|
||||
# We iterate over all definitions instead of settings in a quality/quality_changes group is because in the GUI,
|
||||
# the settings are grouped together by categories, and we had to go over all the definitions to figure out
|
||||
# which setting belongs in which category.
|
||||
current_category = ""
|
||||
|
|
|
@ -10,7 +10,6 @@ from UM.Application import Application
|
|||
from UM.Settings.ContainerRegistry import ContainerRegistry
|
||||
from UM.i18n import i18nCatalog
|
||||
from UM.Settings.SettingFunction import SettingFunction
|
||||
|
||||
from UM.Qt.ListModel import ListModel
|
||||
|
||||
|
||||
|
@ -51,7 +50,7 @@ class UserChangesModel(ListModel):
|
|||
return
|
||||
|
||||
stacks = [global_stack]
|
||||
stacks.extend(global_stack.extruders.values())
|
||||
stacks.extend(global_stack.extruderList)
|
||||
|
||||
# Check if the definition container has a translation file and ensure it's loaded.
|
||||
definition = global_stack.getBottom()
|
|
@ -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.
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
from UM.Application import Application
|
||||
from UM.ConfigurationErrorMessage import ConfigurationErrorMessage
|
||||
|
||||
from .QualityGroup import QualityGroup
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from cura.Machines.QualityNode import QualityNode
|
||||
from PyQt5.QtCore import QObject, pyqtProperty, pyqtSignal
|
||||
|
||||
|
||||
class QualityChangesGroup(QualityGroup):
|
||||
def __init__(self, name: str, quality_type: str, parent = None) -> None:
|
||||
super().__init__(name, quality_type, parent)
|
||||
self._container_registry = Application.getInstance().getContainerRegistry()
|
||||
## Data struct to group several quality changes instance containers together.
|
||||
#
|
||||
# Each group represents one "custom profile" as the user sees it, which
|
||||
# contains an instance container for the global stack and one instance
|
||||
# container per extruder.
|
||||
class QualityChangesGroup(QObject):
|
||||
|
||||
def addNode(self, node: "QualityNode") -> None:
|
||||
extruder_position = node.getMetaDataEntry("position")
|
||||
def __init__(self, name: str, quality_type: str, intent_category: str, parent: Optional["QObject"] = None) -> None:
|
||||
super().__init__(parent)
|
||||
self._name = name
|
||||
self.quality_type = quality_type
|
||||
self.intent_category = intent_category
|
||||
self.is_available = False
|
||||
self.metadata_for_global = {} # type: Dict[str, Any]
|
||||
self.metadata_per_extruder = {} # type: Dict[int, Dict[str, Any]]
|
||||
|
||||
if extruder_position is None and self.node_for_global is not None or extruder_position in self.nodes_for_extruders: #We would be overwriting another node.
|
||||
ConfigurationErrorMessage.getInstance().addFaultyContainers(node.getMetaDataEntry("id"))
|
||||
return
|
||||
nameChanged = pyqtSignal()
|
||||
|
||||
if extruder_position is None: # Then we're a global quality changes profile.
|
||||
self.node_for_global = node
|
||||
else: # This is an extruder's quality changes profile.
|
||||
self.nodes_for_extruders[extruder_position] = node
|
||||
def setName(self, name: str) -> None:
|
||||
if self._name != name:
|
||||
self._name = name
|
||||
self.nameChanged.emit()
|
||||
|
||||
@pyqtProperty(str, fset = setName, notify = nameChanged)
|
||||
def name(self) -> str:
|
||||
return self._name
|
||||
|
||||
def __str__(self) -> str:
|
||||
return "%s[<%s>, available = %s]" % (self.__class__.__name__, self.name, self.is_available)
|
||||
return "{class_name}[{name}, available = {is_available}]".format(class_name = self.__class__.__name__, name = self.name, is_available = self.is_available)
|
||||
|
|
|
@ -1,32 +1,38 @@
|
|||
# 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 Dict, Optional, List, Set
|
||||
|
||||
from PyQt5.QtCore import QObject, pyqtSlot
|
||||
|
||||
from UM.Logger import Logger
|
||||
from UM.Util import parseBool
|
||||
|
||||
from cura.Machines.ContainerNode import ContainerNode
|
||||
|
||||
|
||||
## A QualityGroup represents a group of quality containers that must be applied
|
||||
# to each ContainerStack when it's used.
|
||||
#
|
||||
# A QualityGroup represents a group of containers that must be applied to each ContainerStack when it's used.
|
||||
# Some concrete examples are Quality and QualityChanges: when we select quality type "normal", this quality type
|
||||
# must be applied to all stacks in a machine, although each stack can have different containers. Use an Ultimaker 3
|
||||
# as an example, suppose we choose quality type "normal", the actual InstanceContainers on each stack may look
|
||||
# as below:
|
||||
# A concrete example: When there are two extruders and the user selects the
|
||||
# quality type "normal", this quality type must be applied to all stacks in a
|
||||
# machine, although each stack can have different containers. So one global
|
||||
# profile gets put on the global stack and one extruder profile gets put on
|
||||
# each extruder stack. This quality group then contains the following
|
||||
# profiles (for instance):
|
||||
# GlobalStack ExtruderStack 1 ExtruderStack 2
|
||||
# quality container: um3_global_normal um3_aa04_pla_normal um3_aa04_abs_normal
|
||||
#
|
||||
# This QualityGroup is mainly used in quality and quality_changes to group the containers that can be applied to
|
||||
# a machine, so when a quality/custom quality is selected, the container can be directly applied to each stack instead
|
||||
# of looking them up again.
|
||||
#
|
||||
class QualityGroup(QObject):
|
||||
|
||||
def __init__(self, name: str, quality_type: str, parent = None) -> None:
|
||||
super().__init__(parent)
|
||||
# The purpose of these quality groups is to group the containers that can be
|
||||
# applied to a configuration, so that when a quality level is selected, the
|
||||
# container can directly be applied to each stack instead of looking them up
|
||||
# again.
|
||||
class QualityGroup:
|
||||
## Constructs a new group.
|
||||
# \param name The user-visible name for the group.
|
||||
# \param quality_type The quality level that each profile in this group
|
||||
# has.
|
||||
def __init__(self, name: str, quality_type: str) -> None:
|
||||
self.name = name
|
||||
self.node_for_global = None # type: Optional[ContainerNode]
|
||||
self.nodes_for_extruders = {} # type: Dict[int, ContainerNode]
|
||||
|
@ -34,7 +40,6 @@ class QualityGroup(QObject):
|
|||
self.is_available = False
|
||||
self.is_experimental = False
|
||||
|
||||
@pyqtSlot(result = str)
|
||||
def getName(self) -> str:
|
||||
return self.name
|
||||
|
||||
|
@ -43,7 +48,7 @@ class QualityGroup(QObject):
|
|||
for node in [self.node_for_global] + list(self.nodes_for_extruders.values()):
|
||||
if node is None:
|
||||
continue
|
||||
container = node.getContainer()
|
||||
container = node.container
|
||||
if container:
|
||||
result.update(container.getAllKeys())
|
||||
return result
|
||||
|
@ -60,6 +65,9 @@ class QualityGroup(QObject):
|
|||
self.node_for_global = node
|
||||
|
||||
# Update is_experimental flag
|
||||
if not node.container:
|
||||
Logger.log("w", "Node {0} doesn't have a container.".format(node.container_id))
|
||||
return
|
||||
is_experimental = parseBool(node.getMetaDataEntry("is_experimental", False))
|
||||
self.is_experimental |= is_experimental
|
||||
|
||||
|
@ -67,5 +75,8 @@ class QualityGroup(QObject):
|
|||
self.nodes_for_extruders[position] = node
|
||||
|
||||
# Update is_experimental flag
|
||||
if not node.container:
|
||||
Logger.log("w", "Node {0} doesn't have a container.".format(node.container_id))
|
||||
return
|
||||
is_experimental = parseBool(node.getMetaDataEntry("is_experimental", False))
|
||||
self.is_experimental |= is_experimental
|
||||
|
|
|
@ -1,550 +0,0 @@
|
|||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
from typing import TYPE_CHECKING, Optional, cast, Dict, List, Set
|
||||
|
||||
from PyQt5.QtCore import QObject, QTimer, pyqtSignal, pyqtSlot
|
||||
|
||||
from UM.ConfigurationErrorMessage import ConfigurationErrorMessage
|
||||
from UM.Logger import Logger
|
||||
from UM.Util import parseBool
|
||||
from UM.Settings.InstanceContainer import InstanceContainer
|
||||
|
||||
from cura.Settings.ExtruderStack import ExtruderStack
|
||||
|
||||
from .QualityGroup import QualityGroup
|
||||
from .QualityNode import QualityNode
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from UM.Settings.Interfaces import DefinitionContainerInterface
|
||||
from cura.Settings.GlobalStack import GlobalStack
|
||||
from .QualityChangesGroup import QualityChangesGroup
|
||||
from cura.CuraApplication import CuraApplication
|
||||
|
||||
|
||||
#
|
||||
# Similar to MaterialManager, QualityManager maintains a number of maps and trees for quality profile lookup.
|
||||
# The models GUI and QML use are now only dependent on the QualityManager. That means as long as the data in
|
||||
# QualityManager gets updated correctly, the GUI models should be updated correctly too, and the same goes for GUI.
|
||||
#
|
||||
# For now, updating the lookup maps and trees here is very simple: we discard the old data completely and recreate them
|
||||
# again. This means the update is exactly the same as initialization. There are performance concerns about this approach
|
||||
# but so far the creation of the tables and maps is very fast and there is no noticeable slowness, we keep it like this
|
||||
# because it's simple.
|
||||
#
|
||||
class QualityManager(QObject):
|
||||
|
||||
qualitiesUpdated = pyqtSignal()
|
||||
|
||||
def __init__(self, application: "CuraApplication", parent = None) -> None:
|
||||
super().__init__(parent)
|
||||
self._application = application
|
||||
self._material_manager = self._application.getMaterialManager()
|
||||
self._container_registry = self._application.getContainerRegistry()
|
||||
|
||||
self._empty_quality_container = self._application.empty_quality_container
|
||||
self._empty_quality_changes_container = self._application.empty_quality_changes_container
|
||||
|
||||
# For quality lookup
|
||||
self._machine_nozzle_buildplate_material_quality_type_to_quality_dict = {} # type: Dict[str, QualityNode]
|
||||
|
||||
# For quality_changes lookup
|
||||
self._machine_quality_type_to_quality_changes_dict = {} # type: Dict[str, QualityNode]
|
||||
|
||||
self._default_machine_definition_id = "fdmprinter"
|
||||
|
||||
self._container_registry.containerMetaDataChanged.connect(self._onContainerMetadataChanged)
|
||||
self._container_registry.containerAdded.connect(self._onContainerMetadataChanged)
|
||||
self._container_registry.containerRemoved.connect(self._onContainerMetadataChanged)
|
||||
|
||||
# When a custom quality gets added/imported, there can be more than one InstanceContainers. In those cases,
|
||||
# we don't want to react on every container/metadata changed signal. The timer here is to buffer it a bit so
|
||||
# we don't react too many time.
|
||||
self._update_timer = QTimer(self)
|
||||
self._update_timer.setInterval(300)
|
||||
self._update_timer.setSingleShot(True)
|
||||
self._update_timer.timeout.connect(self._updateMaps)
|
||||
|
||||
def initialize(self) -> None:
|
||||
# Initialize the lookup tree for quality profiles with following structure:
|
||||
# <machine> -> <nozzle> -> <buildplate> -> <material>
|
||||
# <machine> -> <material>
|
||||
|
||||
self._machine_nozzle_buildplate_material_quality_type_to_quality_dict = {} # for quality lookup
|
||||
self._machine_quality_type_to_quality_changes_dict = {} # for quality_changes lookup
|
||||
|
||||
quality_metadata_list = self._container_registry.findContainersMetadata(type = "quality")
|
||||
for metadata in quality_metadata_list:
|
||||
if metadata["id"] == "empty_quality":
|
||||
continue
|
||||
|
||||
definition_id = metadata["definition"]
|
||||
quality_type = metadata["quality_type"]
|
||||
|
||||
root_material_id = metadata.get("material")
|
||||
nozzle_name = metadata.get("variant")
|
||||
buildplate_name = metadata.get("buildplate")
|
||||
is_global_quality = metadata.get("global_quality", False)
|
||||
is_global_quality = is_global_quality or (root_material_id is None and nozzle_name is None and buildplate_name is None)
|
||||
|
||||
# Sanity check: material+variant and is_global_quality cannot be present at the same time
|
||||
if is_global_quality and (root_material_id or nozzle_name):
|
||||
ConfigurationErrorMessage.getInstance().addFaultyContainers(metadata["id"])
|
||||
continue
|
||||
|
||||
if definition_id not in self._machine_nozzle_buildplate_material_quality_type_to_quality_dict:
|
||||
self._machine_nozzle_buildplate_material_quality_type_to_quality_dict[definition_id] = QualityNode()
|
||||
machine_node = cast(QualityNode, self._machine_nozzle_buildplate_material_quality_type_to_quality_dict[definition_id])
|
||||
|
||||
if is_global_quality:
|
||||
# For global qualities, save data in the machine node
|
||||
machine_node.addQualityMetadata(quality_type, metadata)
|
||||
continue
|
||||
|
||||
current_node = machine_node
|
||||
intermediate_node_info_list = [nozzle_name, buildplate_name, root_material_id]
|
||||
current_intermediate_node_info_idx = 0
|
||||
|
||||
while current_intermediate_node_info_idx < len(intermediate_node_info_list):
|
||||
node_name = intermediate_node_info_list[current_intermediate_node_info_idx]
|
||||
if node_name is not None:
|
||||
# There is specific information, update the current node to go deeper so we can add this quality
|
||||
# at the most specific branch in the lookup tree.
|
||||
if node_name not in current_node.children_map:
|
||||
current_node.children_map[node_name] = QualityNode()
|
||||
current_node = cast(QualityNode, current_node.children_map[node_name])
|
||||
|
||||
current_intermediate_node_info_idx += 1
|
||||
|
||||
current_node.addQualityMetadata(quality_type, metadata)
|
||||
|
||||
# Initialize the lookup tree for quality_changes profiles with following structure:
|
||||
# <machine> -> <quality_type> -> <name>
|
||||
quality_changes_metadata_list = self._container_registry.findContainersMetadata(type = "quality_changes")
|
||||
for metadata in quality_changes_metadata_list:
|
||||
if metadata["id"] == "empty_quality_changes":
|
||||
continue
|
||||
|
||||
machine_definition_id = metadata["definition"]
|
||||
quality_type = metadata["quality_type"]
|
||||
|
||||
if machine_definition_id not in self._machine_quality_type_to_quality_changes_dict:
|
||||
self._machine_quality_type_to_quality_changes_dict[machine_definition_id] = QualityNode()
|
||||
machine_node = self._machine_quality_type_to_quality_changes_dict[machine_definition_id]
|
||||
machine_node.addQualityChangesMetadata(quality_type, metadata)
|
||||
|
||||
Logger.log("d", "Lookup tables updated.")
|
||||
self.qualitiesUpdated.emit()
|
||||
|
||||
def _updateMaps(self) -> None:
|
||||
self.initialize()
|
||||
|
||||
def _onContainerMetadataChanged(self, container: InstanceContainer) -> None:
|
||||
self._onContainerChanged(container)
|
||||
|
||||
def _onContainerChanged(self, container: InstanceContainer) -> None:
|
||||
container_type = container.getMetaDataEntry("type")
|
||||
if container_type not in ("quality", "quality_changes"):
|
||||
return
|
||||
|
||||
# update the cache table
|
||||
self._update_timer.start()
|
||||
|
||||
# Updates the given quality groups' availabilities according to which extruders are being used/ enabled.
|
||||
def _updateQualityGroupsAvailability(self, machine: "GlobalStack", quality_group_list) -> None:
|
||||
used_extruders = set()
|
||||
for i in range(machine.getProperty("machine_extruder_count", "value")):
|
||||
if str(i) in machine.extruders and machine.extruders[str(i)].isEnabled:
|
||||
used_extruders.add(str(i))
|
||||
|
||||
# Update the "is_available" flag for each quality group.
|
||||
for quality_group in quality_group_list:
|
||||
is_available = True
|
||||
if quality_group.node_for_global is None:
|
||||
is_available = False
|
||||
if is_available:
|
||||
for position in used_extruders:
|
||||
if position not in quality_group.nodes_for_extruders:
|
||||
is_available = False
|
||||
break
|
||||
|
||||
quality_group.is_available = is_available
|
||||
|
||||
# Returns a dict of "custom profile name" -> QualityChangesGroup
|
||||
def getQualityChangesGroups(self, machine: "GlobalStack") -> dict:
|
||||
machine_definition_id = getMachineDefinitionIDForQualitySearch(machine.definition)
|
||||
|
||||
machine_node = self._machine_quality_type_to_quality_changes_dict.get(machine_definition_id)
|
||||
if not machine_node:
|
||||
Logger.log("i", "Cannot find node for machine def [%s] in QualityChanges lookup table", machine_definition_id)
|
||||
return dict()
|
||||
|
||||
# Update availability for each QualityChangesGroup:
|
||||
# A custom profile is always available as long as the quality_type it's based on is available
|
||||
quality_group_dict = self.getQualityGroups(machine)
|
||||
available_quality_type_list = [qt for qt, qg in quality_group_dict.items() if qg.is_available]
|
||||
|
||||
# Iterate over all quality_types in the machine node
|
||||
quality_changes_group_dict = dict()
|
||||
for quality_type, quality_changes_node in machine_node.quality_type_map.items():
|
||||
for quality_changes_name, quality_changes_group in quality_changes_node.children_map.items():
|
||||
quality_changes_group_dict[quality_changes_name] = quality_changes_group
|
||||
quality_changes_group.is_available = quality_type in available_quality_type_list
|
||||
|
||||
return quality_changes_group_dict
|
||||
|
||||
#
|
||||
# Gets all quality groups for the given machine. Both available and none available ones will be included.
|
||||
# It returns a dictionary with "quality_type"s as keys and "QualityGroup"s as values.
|
||||
# Whether a QualityGroup is available can be unknown via the field QualityGroup.is_available.
|
||||
# For more details, see QualityGroup.
|
||||
#
|
||||
def getQualityGroups(self, machine: "GlobalStack") -> Dict[str, QualityGroup]:
|
||||
machine_definition_id = getMachineDefinitionIDForQualitySearch(machine.definition)
|
||||
|
||||
# This determines if we should only get the global qualities for the global stack and skip the global qualities for the extruder stacks
|
||||
has_machine_specific_qualities = machine.getHasMachineQuality()
|
||||
|
||||
# To find the quality container for the GlobalStack, check in the following fall-back manner:
|
||||
# (1) the machine-specific node
|
||||
# (2) the generic node
|
||||
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
|
||||
# qualities, we should not fall back to use the global qualities.
|
||||
has_extruder_specific_qualities = False
|
||||
if machine_node:
|
||||
if machine_node.children_map:
|
||||
has_extruder_specific_qualities = True
|
||||
|
||||
default_machine_node = self._machine_nozzle_buildplate_material_quality_type_to_quality_dict.get(self._default_machine_definition_id)
|
||||
|
||||
nodes_to_check = [] # type: List[QualityNode]
|
||||
if machine_node is not None:
|
||||
nodes_to_check.append(machine_node)
|
||||
if default_machine_node is not None:
|
||||
nodes_to_check.append(default_machine_node)
|
||||
|
||||
# Iterate over all quality_types in the machine node
|
||||
quality_group_dict = {}
|
||||
for node in nodes_to_check:
|
||||
if node and node.quality_type_map:
|
||||
quality_node = list(node.quality_type_map.values())[0]
|
||||
is_global_quality = parseBool(quality_node.getMetaDataEntry("global_quality", False))
|
||||
if not is_global_quality:
|
||||
continue
|
||||
|
||||
for quality_type, quality_node in node.quality_type_map.items():
|
||||
quality_group = QualityGroup(quality_node.getMetaDataEntry("name", ""), quality_type)
|
||||
quality_group.setGlobalNode(quality_node)
|
||||
quality_group_dict[quality_type] = quality_group
|
||||
break
|
||||
|
||||
buildplate_name = machine.getBuildplateName()
|
||||
|
||||
# Iterate over all extruders to find quality containers for each extruder
|
||||
for position, extruder in machine.extruders.items():
|
||||
nozzle_name = None
|
||||
if extruder.variant.getId() != "empty_variant":
|
||||
nozzle_name = extruder.variant.getName()
|
||||
|
||||
# This is a list of root material IDs to use for searching for suitable quality profiles.
|
||||
# The root material IDs in this list are in prioritized order.
|
||||
root_material_id_list = []
|
||||
has_material = False # flag indicating whether this extruder has a material assigned
|
||||
root_material_id = None
|
||||
if extruder.material.getId() != "empty_material":
|
||||
has_material = True
|
||||
root_material_id = extruder.material.getMetaDataEntry("base_file")
|
||||
# Convert possible generic_pla_175 -> generic_pla
|
||||
root_material_id = self._material_manager.getRootMaterialIDWithoutDiameter(root_material_id)
|
||||
root_material_id_list.append(root_material_id)
|
||||
|
||||
# Also try to get the fallback materials
|
||||
fallback_ids = self._material_manager.getFallBackMaterialIdsByMaterial(extruder.material)
|
||||
|
||||
if fallback_ids:
|
||||
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.
|
||||
# The use case is that, when we look for qualities for a machine, we first want to search in the following
|
||||
# order:
|
||||
# 1. machine-nozzle-buildplate-and-material-specific qualities if exist
|
||||
# 2. machine-nozzle-and-material-specific qualities if exist
|
||||
# 3. machine-nozzle-specific qualities if exist
|
||||
# 4. machine-material-specific qualities if exist
|
||||
# 5. machine-specific global qualities if exist, otherwise generic global qualities
|
||||
# NOTE: We DO NOT fail back to generic global qualities if machine-specific global qualities exist.
|
||||
# This is because when a machine defines its own global qualities such as Normal, Fine, etc.,
|
||||
# it is intended to maintain those specific qualities ONLY. If we still fail back to the generic
|
||||
# global qualities, there can be unimplemented quality types e.g. "coarse", and this is not
|
||||
# correct.
|
||||
# Each points above can be represented as a node in the lookup tree, so here we simply put those nodes into
|
||||
# the list with priorities as the order. Later, we just need to loop over each node in this list and fetch
|
||||
# qualities from there.
|
||||
node_info_list_0 = [nozzle_name, buildplate_name, root_material_id] # type: List[Optional[str]]
|
||||
nodes_to_check = []
|
||||
|
||||
# This function tries to recursively find the deepest (the most specific) branch and add those nodes to
|
||||
# the search list in the order described above. So, by iterating over that search node list, we first look
|
||||
# in the more specific branches and then the less specific (generic) ones.
|
||||
def addNodesToCheck(node: Optional[QualityNode], nodes_to_check_list: List[QualityNode], node_info_list, node_info_idx: int) -> None:
|
||||
if node is None:
|
||||
return
|
||||
|
||||
if node_info_idx < len(node_info_list):
|
||||
node_name = node_info_list[node_info_idx]
|
||||
if node_name is not None:
|
||||
current_node = node.getChildNode(node_name)
|
||||
if current_node is not None and has_material:
|
||||
addNodesToCheck(current_node, nodes_to_check_list, node_info_list, node_info_idx + 1)
|
||||
|
||||
if has_material:
|
||||
for rmid in root_material_id_list:
|
||||
material_node = node.getChildNode(rmid)
|
||||
if material_node:
|
||||
nodes_to_check_list.append(material_node)
|
||||
break
|
||||
|
||||
nodes_to_check_list.append(node)
|
||||
|
||||
addNodesToCheck(machine_node, nodes_to_check, node_info_list_0, 0)
|
||||
|
||||
# The last fall back will be the global qualities (either from the machine-specific node or the generic
|
||||
# node), but we only use one. For details see the overview comments above.
|
||||
|
||||
if machine_node is not None and machine_node.quality_type_map:
|
||||
nodes_to_check += [machine_node]
|
||||
elif default_machine_node is not None:
|
||||
nodes_to_check += [default_machine_node]
|
||||
|
||||
for node_idx, node in enumerate(nodes_to_check):
|
||||
if node and node.quality_type_map:
|
||||
if has_extruder_specific_qualities:
|
||||
# Only include variant qualities; skip non global qualities
|
||||
quality_node = list(node.quality_type_map.values())[0]
|
||||
is_global_quality = parseBool(quality_node.getMetaDataEntry("global_quality", False))
|
||||
if is_global_quality:
|
||||
continue
|
||||
|
||||
for quality_type, quality_node in node.quality_type_map.items():
|
||||
if quality_type not in quality_group_dict:
|
||||
quality_group = QualityGroup(quality_node.getMetaDataEntry("name", ""), quality_type)
|
||||
quality_group_dict[quality_type] = quality_group
|
||||
|
||||
quality_group = quality_group_dict[quality_type]
|
||||
if position not in quality_group.nodes_for_extruders:
|
||||
quality_group.setExtruderNode(position, quality_node)
|
||||
|
||||
# If the machine has its own specific qualities, for extruders, it should skip the global qualities
|
||||
# and use the material/variant specific qualities.
|
||||
if has_extruder_specific_qualities:
|
||||
if node_idx == len(nodes_to_check) - 1:
|
||||
break
|
||||
|
||||
# Update availabilities for each quality group
|
||||
self._updateQualityGroupsAvailability(machine, quality_group_dict.values())
|
||||
|
||||
return quality_group_dict
|
||||
|
||||
def getQualityGroupsForMachineDefinition(self, machine: "GlobalStack") -> Dict[str, QualityGroup]:
|
||||
machine_definition_id = getMachineDefinitionIDForQualitySearch(machine.definition)
|
||||
|
||||
# To find the quality container for the GlobalStack, check in the following fall-back manner:
|
||||
# (1) the machine-specific node
|
||||
# (2) the generic node
|
||||
machine_node = self._machine_nozzle_buildplate_material_quality_type_to_quality_dict.get(machine_definition_id)
|
||||
default_machine_node = self._machine_nozzle_buildplate_material_quality_type_to_quality_dict.get(
|
||||
self._default_machine_definition_id)
|
||||
nodes_to_check = [machine_node, default_machine_node]
|
||||
|
||||
# Iterate over all quality_types in the machine node
|
||||
quality_group_dict = dict()
|
||||
for node in nodes_to_check:
|
||||
if node and node.quality_type_map:
|
||||
for quality_type, quality_node in node.quality_type_map.items():
|
||||
quality_group = QualityGroup(quality_node.getMetaDataEntry("name", ""), quality_type)
|
||||
quality_group.setGlobalNode(quality_node)
|
||||
quality_group_dict[quality_type] = quality_group
|
||||
break
|
||||
|
||||
return quality_group_dict
|
||||
|
||||
def getDefaultQualityType(self, machine: "GlobalStack") -> Optional[QualityGroup]:
|
||||
preferred_quality_type = machine.definition.getMetaDataEntry("preferred_quality_type")
|
||||
quality_group_dict = self.getQualityGroups(machine)
|
||||
quality_group = quality_group_dict.get(preferred_quality_type)
|
||||
return quality_group
|
||||
|
||||
|
||||
#
|
||||
# Methods for GUI
|
||||
#
|
||||
|
||||
#
|
||||
# Remove the given quality changes group.
|
||||
#
|
||||
@pyqtSlot(QObject)
|
||||
def removeQualityChangesGroup(self, quality_changes_group: "QualityChangesGroup") -> None:
|
||||
Logger.log("i", "Removing quality changes group [%s]", quality_changes_group.name)
|
||||
removed_quality_changes_ids = set()
|
||||
for node in quality_changes_group.getAllNodes():
|
||||
container_id = node.getMetaDataEntry("id")
|
||||
self._container_registry.removeContainer(container_id)
|
||||
removed_quality_changes_ids.add(container_id)
|
||||
|
||||
# Reset all machines that have activated this quality changes to empty.
|
||||
for global_stack in self._container_registry.findContainerStacks(type = "machine"):
|
||||
if global_stack.qualityChanges.getId() in removed_quality_changes_ids:
|
||||
global_stack.qualityChanges = self._empty_quality_changes_container
|
||||
for extruder_stack in self._container_registry.findContainerStacks(type = "extruder_train"):
|
||||
if extruder_stack.qualityChanges.getId() in removed_quality_changes_ids:
|
||||
extruder_stack.qualityChanges = self._empty_quality_changes_container
|
||||
|
||||
#
|
||||
# Rename a set of quality changes containers. Returns the new name.
|
||||
#
|
||||
@pyqtSlot(QObject, str, result = str)
|
||||
def renameQualityChangesGroup(self, quality_changes_group: "QualityChangesGroup", new_name: str) -> str:
|
||||
Logger.log("i", "Renaming QualityChangesGroup[%s] to [%s]", quality_changes_group.name, new_name)
|
||||
if new_name == quality_changes_group.name:
|
||||
Logger.log("i", "QualityChangesGroup name [%s] unchanged.", quality_changes_group.name)
|
||||
return new_name
|
||||
|
||||
new_name = self._container_registry.uniqueName(new_name)
|
||||
for node in quality_changes_group.getAllNodes():
|
||||
container = node.getContainer()
|
||||
if container:
|
||||
container.setName(new_name)
|
||||
|
||||
quality_changes_group.name = new_name
|
||||
|
||||
self._application.getMachineManager().activeQualityChanged.emit()
|
||||
self._application.getMachineManager().activeQualityGroupChanged.emit()
|
||||
|
||||
return new_name
|
||||
|
||||
#
|
||||
# Duplicates the given quality.
|
||||
#
|
||||
@pyqtSlot(str, "QVariantMap")
|
||||
def duplicateQualityChanges(self, quality_changes_name: str, quality_model_item) -> None:
|
||||
global_stack = self._application.getGlobalContainerStack()
|
||||
if not global_stack:
|
||||
Logger.log("i", "No active global stack, cannot duplicate quality changes.")
|
||||
return
|
||||
|
||||
quality_group = quality_model_item["quality_group"]
|
||||
quality_changes_group = quality_model_item["quality_changes_group"]
|
||||
if quality_changes_group is None:
|
||||
# create global quality changes only
|
||||
new_quality_changes = self._createQualityChanges(quality_group.quality_type, quality_changes_name,
|
||||
global_stack, None)
|
||||
self._container_registry.addContainer(new_quality_changes)
|
||||
else:
|
||||
new_name = self._container_registry.uniqueName(quality_changes_name)
|
||||
for node in quality_changes_group.getAllNodes():
|
||||
container = node.getContainer()
|
||||
if not container:
|
||||
continue
|
||||
new_id = self._container_registry.uniqueName(container.getId())
|
||||
self._container_registry.addContainer(container.duplicate(new_id, new_name))
|
||||
|
||||
## Create quality changes containers from the user containers in the active stacks.
|
||||
#
|
||||
# This will go through the global and extruder stacks and create quality_changes containers from
|
||||
# the user containers in each stack. These then replace the quality_changes containers in the
|
||||
# stack and clear the user settings.
|
||||
@pyqtSlot(str)
|
||||
def createQualityChanges(self, base_name: str) -> None:
|
||||
machine_manager = self._application.getMachineManager()
|
||||
|
||||
global_stack = machine_manager.activeMachine
|
||||
if not global_stack:
|
||||
return
|
||||
|
||||
active_quality_name = machine_manager.activeQualityOrQualityChangesName
|
||||
if active_quality_name == "":
|
||||
Logger.log("w", "No quality container found in stack %s, cannot create profile", global_stack.getId())
|
||||
return
|
||||
|
||||
machine_manager.blurSettings.emit()
|
||||
if base_name is None or base_name == "":
|
||||
base_name = active_quality_name
|
||||
unique_name = self._container_registry.uniqueName(base_name)
|
||||
|
||||
# Go through the active stacks and create quality_changes containers from the user containers.
|
||||
stack_list = [global_stack] + list(global_stack.extruders.values())
|
||||
for stack in stack_list:
|
||||
user_container = stack.userChanges
|
||||
quality_container = stack.quality
|
||||
quality_changes_container = stack.qualityChanges
|
||||
if not quality_container or not quality_changes_container:
|
||||
Logger.log("w", "No quality or quality changes container found in stack %s, ignoring it", stack.getId())
|
||||
continue
|
||||
|
||||
quality_type = quality_container.getMetaDataEntry("quality_type")
|
||||
extruder_stack = None
|
||||
if isinstance(stack, ExtruderStack):
|
||||
extruder_stack = stack
|
||||
new_changes = self._createQualityChanges(quality_type, unique_name, global_stack, extruder_stack)
|
||||
from cura.Settings.ContainerManager import ContainerManager
|
||||
ContainerManager.getInstance()._performMerge(new_changes, quality_changes_container, clear_settings = False)
|
||||
ContainerManager.getInstance()._performMerge(new_changes, user_container)
|
||||
|
||||
self._container_registry.addContainer(new_changes)
|
||||
|
||||
#
|
||||
# Create a quality changes container with the given setup.
|
||||
#
|
||||
def _createQualityChanges(self, quality_type: str, new_name: str, machine: "GlobalStack",
|
||||
extruder_stack: Optional["ExtruderStack"]) -> "InstanceContainer":
|
||||
base_id = machine.definition.getId() if extruder_stack is None else extruder_stack.getId()
|
||||
new_id = base_id + "_" + new_name
|
||||
new_id = new_id.lower().replace(" ", "_")
|
||||
new_id = self._container_registry.uniqueName(new_id)
|
||||
|
||||
# Create a new quality_changes container for the quality.
|
||||
quality_changes = InstanceContainer(new_id)
|
||||
quality_changes.setName(new_name)
|
||||
quality_changes.setMetaDataEntry("type", "quality_changes")
|
||||
quality_changes.setMetaDataEntry("quality_type", quality_type)
|
||||
|
||||
# If we are creating a container for an extruder, ensure we add that to the container
|
||||
if extruder_stack is not None:
|
||||
quality_changes.setMetaDataEntry("position", extruder_stack.getMetaDataEntry("position"))
|
||||
|
||||
# If the machine specifies qualities should be filtered, ensure we match the current criteria.
|
||||
machine_definition_id = getMachineDefinitionIDForQualitySearch(machine.definition)
|
||||
quality_changes.setDefinition(machine_definition_id)
|
||||
|
||||
quality_changes.setMetaDataEntry("setting_version", self._application.SettingVersion)
|
||||
return quality_changes
|
||||
|
||||
|
||||
#
|
||||
# Gets the machine definition ID that can be used to search for Quality containers that are suitable for the given
|
||||
# machine. The rule is as follows:
|
||||
# 1. By default, the machine definition ID for quality container search will be "fdmprinter", which is the generic
|
||||
# machine.
|
||||
# 2. If a machine has its own machine quality (with "has_machine_quality = True"), we should use the given machine's
|
||||
# own machine definition ID for quality search.
|
||||
# Example: for an Ultimaker 3, the definition ID should be "ultimaker3".
|
||||
# 3. When condition (2) is met, AND the machine has "quality_definition" defined in its definition file, then the
|
||||
# definition ID specified in "quality_definition" should be used.
|
||||
# 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.
|
||||
#
|
||||
def getMachineDefinitionIDForQualitySearch(machine_definition: "DefinitionContainerInterface",
|
||||
default_definition_id: str = "fdmprinter") -> str:
|
||||
machine_definition_id = default_definition_id
|
||||
if parseBool(machine_definition.getMetaDataEntry("has_machine_quality", False)):
|
||||
# Only use the machine's own quality definition ID if this machine has machine quality.
|
||||
machine_definition_id = machine_definition.getMetaDataEntry("quality_definition")
|
||||
if machine_definition_id is None:
|
||||
machine_definition_id = machine_definition.getId()
|
||||
|
||||
return machine_definition_id
|
|
@ -1,38 +1,44 @@
|
|||
# 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, Dict, cast, Any
|
||||
from typing import Union, TYPE_CHECKING
|
||||
|
||||
from .ContainerNode import ContainerNode
|
||||
from .QualityChangesGroup import QualityChangesGroup
|
||||
from UM.Settings.ContainerRegistry import ContainerRegistry
|
||||
from cura.Machines.ContainerNode import ContainerNode
|
||||
from cura.Machines.IntentNode import IntentNode
|
||||
import UM.FlameProfiler
|
||||
if TYPE_CHECKING:
|
||||
from typing import Dict
|
||||
from cura.Machines.MaterialNode import MaterialNode
|
||||
from cura.Machines.MachineNode import MachineNode
|
||||
|
||||
|
||||
## Represents a quality profile in the container tree.
|
||||
#
|
||||
# QualityNode is used for BOTH quality and quality_changes containers.
|
||||
# This may either be a normal quality profile or a global quality profile.
|
||||
#
|
||||
# Its subcontainers are intent profiles.
|
||||
class QualityNode(ContainerNode):
|
||||
def __init__(self, container_id: str, parent: Union["MaterialNode", "MachineNode"]) -> None:
|
||||
super().__init__(container_id)
|
||||
self.parent = parent
|
||||
self.intents = {} # type: Dict[str, IntentNode]
|
||||
|
||||
def __init__(self, metadata: Optional[Dict[str, Any]] = None) -> None:
|
||||
super().__init__(metadata = metadata)
|
||||
self.quality_type_map = {} # type: Dict[str, QualityNode] # quality_type -> QualityNode for InstanceContainer
|
||||
my_metadata = ContainerRegistry.getInstance().findContainersMetadata(id = container_id)[0]
|
||||
self.quality_type = my_metadata["quality_type"]
|
||||
# The material type of the parent doesn't need to be the same as this due to generic fallbacks.
|
||||
self._material = my_metadata.get("material")
|
||||
self._loadAll()
|
||||
|
||||
def getChildNode(self, child_key: str) -> Optional["QualityNode"]:
|
||||
return self.children_map.get(child_key)
|
||||
@UM.FlameProfiler.profile
|
||||
def _loadAll(self) -> None:
|
||||
container_registry = ContainerRegistry.getInstance()
|
||||
|
||||
def addQualityMetadata(self, quality_type: str, metadata: Dict[str, Any]):
|
||||
if quality_type not in self.quality_type_map:
|
||||
self.quality_type_map[quality_type] = QualityNode(metadata)
|
||||
# Find all intent profiles that fit the current configuration.
|
||||
from cura.Machines.MachineNode import MachineNode
|
||||
if not isinstance(self.parent, MachineNode): # Not a global profile.
|
||||
for intent in container_registry.findInstanceContainersMetadata(type = "intent", definition = self.parent.variant.machine.quality_definition, variant = self.parent.variant.variant_name, material = self._material, quality_type = self.quality_type):
|
||||
self.intents[intent["id"]] = IntentNode(intent["id"], quality = self)
|
||||
|
||||
def getQualityNode(self, quality_type: str) -> Optional["QualityNode"]:
|
||||
return self.quality_type_map.get(quality_type)
|
||||
|
||||
def addQualityChangesMetadata(self, quality_type: str, metadata: Dict[str, Any]):
|
||||
if quality_type not in self.quality_type_map:
|
||||
self.quality_type_map[quality_type] = QualityNode()
|
||||
quality_type_node = self.quality_type_map[quality_type]
|
||||
|
||||
name = metadata["name"]
|
||||
if name not in quality_type_node.children_map:
|
||||
quality_type_node.children_map[name] = QualityChangesGroup(name, quality_type)
|
||||
quality_changes_group = quality_type_node.children_map[name]
|
||||
cast(QualityChangesGroup, quality_changes_group).addNode(QualityNode(metadata))
|
||||
self.intents["empty_intent"] = IntentNode("empty_intent", quality = self)
|
||||
# Otherwise, there are no intents for global profiles.
|
|
@ -1,145 +0,0 @@
|
|||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
from collections import OrderedDict
|
||||
from typing import Optional, TYPE_CHECKING, Dict
|
||||
|
||||
from UM.ConfigurationErrorMessage import ConfigurationErrorMessage
|
||||
from UM.Logger import Logger
|
||||
from UM.Settings.ContainerRegistry import ContainerRegistry
|
||||
from UM.Util import parseBool
|
||||
|
||||
from cura.Machines.ContainerNode import ContainerNode
|
||||
from cura.Machines.VariantType import VariantType, ALL_VARIANT_TYPES
|
||||
from cura.Settings.GlobalStack import GlobalStack
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from UM.Settings.DefinitionContainer import DefinitionContainer
|
||||
|
||||
|
||||
#
|
||||
# VariantManager is THE place to look for a specific variant. It maintains two variant lookup tables with the following
|
||||
# structure:
|
||||
#
|
||||
# [machine_definition_id] -> [variant_type] -> [variant_name] -> ContainerNode(metadata / container)
|
||||
# Example: "ultimaker3" -> "buildplate" -> "Glass" (if present) -> ContainerNode
|
||||
# -> ...
|
||||
# -> "nozzle" -> "AA 0.4"
|
||||
# -> "BB 0.8"
|
||||
# -> ...
|
||||
#
|
||||
# [machine_definition_id] -> [machine_buildplate_type] -> ContainerNode(metadata / container)
|
||||
# Example: "ultimaker3" -> "glass" (this is different from the variant name) -> ContainerNode
|
||||
#
|
||||
# Note that the "container" field is not loaded in the beginning because it would defeat the purpose of lazy-loading.
|
||||
# A container is loaded when getVariant() is called to load a variant InstanceContainer.
|
||||
#
|
||||
class VariantManager:
|
||||
|
||||
def __init__(self, container_registry: ContainerRegistry) -> None:
|
||||
self._container_registry = container_registry
|
||||
|
||||
self._machine_to_variant_dict_map = dict() # type: Dict[str, Dict["VariantType", Dict[str, ContainerNode]]]
|
||||
self._machine_to_buildplate_dict_map = dict() # type: Dict[str, Dict[str, ContainerNode]]
|
||||
|
||||
self._exclude_variant_id_list = ["empty_variant"]
|
||||
|
||||
#
|
||||
# Initializes the VariantManager including:
|
||||
# - initializing the variant lookup table based on the metadata in ContainerRegistry.
|
||||
#
|
||||
def initialize(self) -> None:
|
||||
self._machine_to_variant_dict_map = OrderedDict()
|
||||
self._machine_to_buildplate_dict_map = OrderedDict()
|
||||
|
||||
# Cache all variants from the container registry to a variant map for better searching and organization.
|
||||
variant_metadata_list = self._container_registry.findContainersMetadata(type = "variant")
|
||||
for variant_metadata in variant_metadata_list:
|
||||
if variant_metadata["id"] in self._exclude_variant_id_list:
|
||||
Logger.log("d", "Exclude variant [%s]", variant_metadata["id"])
|
||||
continue
|
||||
|
||||
variant_name = variant_metadata["name"]
|
||||
variant_definition = variant_metadata["definition"]
|
||||
if variant_definition not in self._machine_to_variant_dict_map:
|
||||
self._machine_to_variant_dict_map[variant_definition] = OrderedDict()
|
||||
for variant_type in ALL_VARIANT_TYPES:
|
||||
self._machine_to_variant_dict_map[variant_definition][variant_type] = dict()
|
||||
|
||||
try:
|
||||
variant_type = variant_metadata["hardware_type"]
|
||||
except KeyError:
|
||||
Logger.log("w", "Variant %s does not specify a hardware_type; assuming 'nozzle'", variant_metadata["id"])
|
||||
variant_type = VariantType.NOZZLE
|
||||
variant_type = VariantType(variant_type)
|
||||
variant_dict = self._machine_to_variant_dict_map[variant_definition][variant_type]
|
||||
if variant_name in variant_dict:
|
||||
# ERROR: duplicated variant name.
|
||||
ConfigurationErrorMessage.getInstance().addFaultyContainers(variant_metadata["id"])
|
||||
continue #Then ignore this variant. This now chooses one of the two variants arbitrarily and deletes the other one! No guarantees!
|
||||
|
||||
variant_dict[variant_name] = ContainerNode(metadata = variant_metadata)
|
||||
|
||||
# If the variant is a buildplate then fill also the buildplate map
|
||||
if variant_type == VariantType.BUILD_PLATE:
|
||||
if variant_definition not in self._machine_to_buildplate_dict_map:
|
||||
self._machine_to_buildplate_dict_map[variant_definition] = OrderedDict()
|
||||
|
||||
variant_container = self._container_registry.findContainers(type = "variant", id = variant_metadata["id"])[0]
|
||||
buildplate_type = variant_container.getProperty("machine_buildplate_type", "value")
|
||||
if buildplate_type not in self._machine_to_buildplate_dict_map[variant_definition]:
|
||||
self._machine_to_variant_dict_map[variant_definition][buildplate_type] = dict()
|
||||
|
||||
self._machine_to_buildplate_dict_map[variant_definition][buildplate_type] = variant_dict[variant_name]
|
||||
|
||||
#
|
||||
# Gets the variant InstanceContainer with the given information.
|
||||
# Almost the same as getVariantMetadata() except that this returns an InstanceContainer if present.
|
||||
#
|
||||
def getVariantNode(self, machine_definition_id: str, variant_name: str,
|
||||
variant_type: Optional["VariantType"] = None) -> Optional["ContainerNode"]:
|
||||
if variant_type is None:
|
||||
variant_node = None
|
||||
variant_type_dict = self._machine_to_variant_dict_map[machine_definition_id]
|
||||
for variant_dict in variant_type_dict.values():
|
||||
if variant_name in variant_dict:
|
||||
variant_node = variant_dict[variant_name]
|
||||
break
|
||||
return variant_node
|
||||
|
||||
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]:
|
||||
machine_definition_id = machine.definition.getId()
|
||||
return self._machine_to_variant_dict_map.get(machine_definition_id, {}).get(variant_type, {})
|
||||
|
||||
#
|
||||
# Gets the default variant for the given machine definition.
|
||||
# If the optional GlobalStack is given, the metadata information will be fetched from the GlobalStack instead of
|
||||
# the DefinitionContainer. Because for machines such as UM2, you can enable Olsson Block, which will set
|
||||
# "has_variants" to True in the GlobalStack. In those cases, we need to fetch metadata from the GlobalStack or
|
||||
# it may not be correct.
|
||||
#
|
||||
def getDefaultVariantNode(self, machine_definition: "DefinitionContainer",
|
||||
variant_type: "VariantType",
|
||||
global_stack: Optional["GlobalStack"] = None) -> Optional["ContainerNode"]:
|
||||
machine_definition_id = machine_definition.getId()
|
||||
container_for_metadata_fetching = global_stack if global_stack is not None else machine_definition
|
||||
|
||||
preferred_variant_name = None
|
||||
if variant_type == VariantType.BUILD_PLATE:
|
||||
if parseBool(container_for_metadata_fetching.getMetaDataEntry("has_variant_buildplates", False)):
|
||||
preferred_variant_name = container_for_metadata_fetching.getMetaDataEntry("preferred_variant_buildplate_name")
|
||||
else:
|
||||
if parseBool(container_for_metadata_fetching.getMetaDataEntry("has_variants", False)):
|
||||
preferred_variant_name = container_for_metadata_fetching.getMetaDataEntry("preferred_variant_name")
|
||||
|
||||
node = None
|
||||
if preferred_variant_name:
|
||||
node = self.getVariantNode(machine_definition_id, preferred_variant_name, variant_type)
|
||||
return node
|
||||
|
||||
def getBuildplateVariantNode(self, machine_definition_id: str, buildplate_type: str) -> Optional["ContainerNode"]:
|
||||
if machine_definition_id in self._machine_to_buildplate_dict_map:
|
||||
return self._machine_to_buildplate_dict_map[machine_definition_id].get(buildplate_type)
|
||||
return None
|
182
cura/Machines/VariantNode.py
Normal file
182
cura/Machines/VariantNode.py
Normal file
|
@ -0,0 +1,182 @@
|
|||
# Copyright (c) 2019 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from UM.Logger import Logger
|
||||
from UM.Settings.ContainerRegistry import ContainerRegistry
|
||||
from UM.Settings.Interfaces import ContainerInterface
|
||||
from UM.Signal import Signal
|
||||
|
||||
from cura.Machines.ContainerNode import ContainerNode
|
||||
from cura.Machines.MaterialNode import MaterialNode
|
||||
|
||||
import UM.FlameProfiler
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Dict
|
||||
from cura.Machines.MachineNode import MachineNode
|
||||
|
||||
|
||||
## This class represents an extruder variant in the container tree.
|
||||
#
|
||||
# The subnodes of these nodes are materials.
|
||||
#
|
||||
# This node contains materials with ALL filament diameters underneath it. The
|
||||
# tree of this variant is not specific to one global stack, so because the
|
||||
# list of materials can be different per stack depending on the compatible
|
||||
# material diameter setting, we cannot filter them here. Filtering must be
|
||||
# done in the model.
|
||||
class VariantNode(ContainerNode):
|
||||
def __init__(self, container_id: str, machine: "MachineNode") -> None:
|
||||
super().__init__(container_id)
|
||||
self.machine = machine
|
||||
self.materials = {} # type: Dict[str, MaterialNode] # Mapping material base files to their nodes.
|
||||
self.materialsChanged = Signal()
|
||||
|
||||
container_registry = ContainerRegistry.getInstance()
|
||||
self.variant_name = container_registry.findContainersMetadata(id = container_id)[0]["name"] # Store our own name so that we can filter more easily.
|
||||
container_registry.containerAdded.connect(self._materialAdded)
|
||||
container_registry.containerRemoved.connect(self._materialRemoved)
|
||||
self._loadAll()
|
||||
|
||||
## (Re)loads all materials under this variant.
|
||||
@UM.FlameProfiler.profile
|
||||
def _loadAll(self) -> None:
|
||||
container_registry = ContainerRegistry.getInstance()
|
||||
|
||||
if not self.machine.has_materials:
|
||||
self.materials["empty_material"] = MaterialNode("empty_material", variant = self)
|
||||
return # There should not be any materials loaded for this printer.
|
||||
|
||||
# Find all the materials for this variant's name.
|
||||
else: # Printer has its own material profiles. Look for material profiles with this printer's definition.
|
||||
base_materials = container_registry.findInstanceContainersMetadata(type = "material", definition = "fdmprinter")
|
||||
printer_specific_materials = container_registry.findInstanceContainersMetadata(type = "material", definition = self.machine.container_id, variant_name = None)
|
||||
variant_specific_materials = container_registry.findInstanceContainersMetadata(type = "material", definition = self.machine.container_id, variant_name = self.variant_name) # If empty_variant, this won't return anything.
|
||||
materials_per_base_file = {material["base_file"]: material for material in base_materials}
|
||||
materials_per_base_file.update({material["base_file"]: material for material in printer_specific_materials}) # Printer-specific profiles override global ones.
|
||||
materials_per_base_file.update({material["base_file"]: material for material in variant_specific_materials}) # Variant-specific profiles override all of those.
|
||||
materials = list(materials_per_base_file.values())
|
||||
|
||||
# Filter materials based on the exclude_materials property.
|
||||
filtered_materials = [material for material in materials if material["id"] not in self.machine.exclude_materials]
|
||||
|
||||
for material in filtered_materials:
|
||||
base_file = material["base_file"]
|
||||
if base_file not in self.materials:
|
||||
self.materials[base_file] = MaterialNode(material["id"], variant = self)
|
||||
self.materials[base_file].materialChanged.connect(self.materialsChanged)
|
||||
if not self.materials:
|
||||
self.materials["empty_material"] = MaterialNode("empty_material", variant = self)
|
||||
|
||||
## Finds the preferred material for this printer with this nozzle in one of
|
||||
# the extruders.
|
||||
#
|
||||
# If the preferred material is not available, an arbitrary material is
|
||||
# returned. If there is a configuration mistake (like a typo in the
|
||||
# preferred material) this returns a random available material. If there
|
||||
# are no available materials, this will return the empty material node.
|
||||
# \param approximate_diameter The desired approximate diameter of the
|
||||
# material.
|
||||
# \return The node for the preferred material, or any arbitrary material
|
||||
# if there is no match.
|
||||
def preferredMaterial(self, approximate_diameter: int) -> MaterialNode:
|
||||
for base_material, material_node in self.materials.items():
|
||||
if self.machine.preferred_material == base_material and approximate_diameter == int(material_node.getMetaDataEntry("approximate_diameter")):
|
||||
return material_node
|
||||
|
||||
# First fallback: Check if we should be checking for the 175 variant.
|
||||
if approximate_diameter == 2:
|
||||
preferred_material = self.machine.preferred_material + "_175"
|
||||
for base_material, material_node in self.materials.items():
|
||||
if preferred_material == base_material and approximate_diameter == int(material_node.getMetaDataEntry("approximate_diameter")):
|
||||
return material_node
|
||||
|
||||
# Second fallback: Choose any material with matching diameter.
|
||||
for material_node in self.materials.values():
|
||||
if material_node.getMetaDataEntry("approximate_diameter") and approximate_diameter == int(material_node.getMetaDataEntry("approximate_diameter")):
|
||||
Logger.log("w", "Could not find preferred material %s, falling back to whatever works", self.machine.preferred_material)
|
||||
return material_node
|
||||
|
||||
fallback = next(iter(self.materials.values())) # Should only happen with empty material node.
|
||||
Logger.log("w", "Could not find preferred material {preferred_material} with diameter {diameter} for variant {variant_id}, falling back to {fallback}.".format(
|
||||
preferred_material = self.machine.preferred_material,
|
||||
diameter = approximate_diameter,
|
||||
variant_id = self.container_id,
|
||||
fallback = fallback.container_id
|
||||
))
|
||||
return fallback
|
||||
|
||||
## When a material gets added to the set of profiles, we need to update our
|
||||
# tree here.
|
||||
@UM.FlameProfiler.profile
|
||||
def _materialAdded(self, container: ContainerInterface) -> None:
|
||||
if container.getMetaDataEntry("type") != "material":
|
||||
return # Not interested.
|
||||
if not ContainerRegistry.getInstance().findContainersMetadata(id = container.getId()):
|
||||
# CURA-6889
|
||||
# containerAdded and removed signals may be triggered in the next event cycle. If a container gets added
|
||||
# and removed in the same event cycle, in the next cycle, the connections should just ignore the signals.
|
||||
# The check here makes sure that the container in the signal still exists.
|
||||
Logger.log("d", "Got container added signal for container [%s] but it no longer exists, do nothing.",
|
||||
container.getId())
|
||||
return
|
||||
if not self.machine.has_materials:
|
||||
return # We won't add any materials.
|
||||
material_definition = container.getMetaDataEntry("definition")
|
||||
|
||||
base_file = container.getMetaDataEntry("base_file")
|
||||
if base_file in self.machine.exclude_materials:
|
||||
return # Material is forbidden for this printer.
|
||||
if base_file not in self.materials: # Completely new base file. Always better than not having a file as long as it matches our set-up.
|
||||
if material_definition != "fdmprinter" and material_definition != self.machine.container_id:
|
||||
return
|
||||
material_variant = container.getMetaDataEntry("variant_name")
|
||||
if material_variant is not None and material_variant != self.variant_name:
|
||||
return
|
||||
else: # We already have this base profile. Replace the base profile if the new one is more specific.
|
||||
new_definition = container.getMetaDataEntry("definition")
|
||||
if new_definition == "fdmprinter":
|
||||
return # Just as unspecific or worse.
|
||||
material_variant = container.getMetaDataEntry("variant_name")
|
||||
if new_definition != self.machine.container_id or material_variant != self.variant_name:
|
||||
return # Doesn't match this set-up.
|
||||
original_metadata = ContainerRegistry.getInstance().findContainersMetadata(id = self.materials[base_file].container_id)[0]
|
||||
if "variant_name" in original_metadata or material_variant is None:
|
||||
return # Original was already specific or just as unspecific as the new one.
|
||||
|
||||
if "empty_material" in self.materials:
|
||||
del self.materials["empty_material"]
|
||||
self.materials[base_file] = MaterialNode(container.getId(), variant = self)
|
||||
self.materials[base_file].materialChanged.connect(self.materialsChanged)
|
||||
self.materialsChanged.emit(self.materials[base_file])
|
||||
|
||||
@UM.FlameProfiler.profile
|
||||
def _materialRemoved(self, container: ContainerInterface) -> None:
|
||||
if container.getMetaDataEntry("type") != "material":
|
||||
return # Only interested in materials.
|
||||
base_file = container.getMetaDataEntry("base_file")
|
||||
if base_file not in self.materials:
|
||||
return # We don't track this material anyway. No need to remove it.
|
||||
|
||||
original_node = self.materials[base_file]
|
||||
del self.materials[base_file]
|
||||
self.materialsChanged.emit(original_node)
|
||||
|
||||
# Now a different material from the same base file may have been hidden because it was not as specific as the one we deleted.
|
||||
# Search for any submaterials from that base file that are still left.
|
||||
materials_same_base_file = ContainerRegistry.getInstance().findContainersMetadata(base_file = base_file)
|
||||
if materials_same_base_file:
|
||||
most_specific_submaterial = materials_same_base_file[0]
|
||||
for submaterial in materials_same_base_file:
|
||||
if submaterial["definition"] == self.machine.container_id:
|
||||
if most_specific_submaterial["definition"] == "fdmprinter":
|
||||
most_specific_submaterial = submaterial
|
||||
if most_specific_submaterial.get("variant_name", "empty") == "empty" and submaterial.get("variant_name", "empty") == self.variant_name:
|
||||
most_specific_submaterial = submaterial
|
||||
self.materials[base_file] = MaterialNode(most_specific_submaterial["id"], variant = self)
|
||||
self.materialsChanged.emit(self.materials[base_file])
|
||||
|
||||
if not self.materials: # The last available material just got deleted and there is nothing with the same base file to replace it.
|
||||
self.materials["empty_material"] = MaterialNode("empty_material", variant = self)
|
||||
self.materialsChanged.emit(self.materials["empty_material"])
|
|
@ -2,10 +2,12 @@
|
|||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
import copy
|
||||
from typing import List
|
||||
|
||||
from UM.Job import Job
|
||||
from UM.Operations.GroupedOperation import GroupedOperation
|
||||
from UM.Message import Message
|
||||
from UM.Scene.SceneNode import SceneNode
|
||||
from UM.i18n import i18nCatalog
|
||||
i18n_catalog = i18nCatalog("cura")
|
||||
|
||||
|
@ -23,7 +25,7 @@ class MultiplyObjectsJob(Job):
|
|||
self._count = count
|
||||
self._min_offset = min_offset
|
||||
|
||||
def run(self):
|
||||
def run(self) -> None:
|
||||
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 Objects"))
|
||||
status_message.show()
|
||||
|
@ -33,13 +35,15 @@ class MultiplyObjectsJob(Job):
|
|||
current_progress = 0
|
||||
|
||||
global_container_stack = Application.getInstance().getGlobalContainerStack()
|
||||
if global_container_stack is None:
|
||||
return # We can't do anything in this case.
|
||||
machine_width = global_container_stack.getProperty("machine_width", "value")
|
||||
machine_depth = global_container_stack.getProperty("machine_depth", "value")
|
||||
|
||||
root = scene.getRoot()
|
||||
scale = 0.5
|
||||
arranger = Arrange.create(x = machine_width, y = machine_depth, scene_root = root, scale = scale, min_offset = self._min_offset)
|
||||
processed_nodes = []
|
||||
processed_nodes = [] # type: List[SceneNode]
|
||||
nodes = []
|
||||
|
||||
not_fit_count = 0
|
||||
|
@ -67,7 +71,11 @@ class MultiplyObjectsJob(Job):
|
|||
new_node = copy.deepcopy(node)
|
||||
solution_found = False
|
||||
if not node_too_big:
|
||||
if offset_shape_arr is not None and hull_shape_arr is not None:
|
||||
solution_found = arranger.findNodePlacement(new_node, offset_shape_arr, hull_shape_arr)
|
||||
else:
|
||||
# The node has no shape, so no need to arrange it. The solution is simple: Do nothing.
|
||||
solution_found = True
|
||||
|
||||
if node_too_big or not solution_found:
|
||||
found_solution_for_all = False
|
||||
|
|
|
@ -41,12 +41,16 @@ class AuthorizationHelpers:
|
|||
"code_verifier": verification_code,
|
||||
"scope": self._settings.CLIENT_SCOPES if self._settings.CLIENT_SCOPES is not None else "",
|
||||
}
|
||||
try:
|
||||
return self.parseTokenResponse(requests.post(self._token_url, data = data)) # type: ignore
|
||||
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.
|
||||
# \param refresh_token:
|
||||
# \return An AuthenticationResponse object.
|
||||
def getAccessTokenUsingRefreshToken(self, refresh_token: str) -> "AuthenticationResponse":
|
||||
Logger.log("d", "Refreshing the access token.")
|
||||
data = {
|
||||
"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 "",
|
||||
|
@ -54,7 +58,10 @@ class AuthorizationHelpers:
|
|||
"refresh_token": refresh_token,
|
||||
"scope": self._settings.CLIENT_SCOPES if self._settings.CLIENT_SCOPES is not None else "",
|
||||
}
|
||||
try:
|
||||
return self.parseTokenResponse(requests.post(self._token_url, data = data)) # type: ignore
|
||||
except requests.exceptions.ConnectionError:
|
||||
return AuthenticationResponse(success=False, err_message="Unable to connect to remote server")
|
||||
|
||||
@staticmethod
|
||||
## Parse the token response from the authorization server into an AuthenticationResponse object.
|
||||
|
@ -92,7 +99,7 @@ class AuthorizationHelpers:
|
|||
})
|
||||
except requests.exceptions.ConnectionError:
|
||||
# Connection was suddenly dropped. Nothing we can do about that.
|
||||
Logger.logException("e", "Something failed while attempting to parse the JWT token")
|
||||
Logger.logException("w", "Something failed while attempting to parse the JWT token")
|
||||
return None
|
||||
if token_request.status_code not in (200, 201):
|
||||
Logger.log("w", "Could not retrieve token data from auth server: %s", token_request.text)
|
||||
|
|
|
@ -25,6 +25,10 @@ class AuthorizationRequestHandler(BaseHTTPRequestHandler):
|
|||
self.authorization_callback = None # type: Optional[Callable[[AuthenticationResponse], None]]
|
||||
self.verification_code = None # type: Optional[str]
|
||||
|
||||
# CURA-6609: Some browser seems to issue a HEAD instead of GET request as the callback.
|
||||
def do_HEAD(self) -> None:
|
||||
self.do_GET()
|
||||
|
||||
def do_GET(self) -> None:
|
||||
# Extract values from the query string.
|
||||
parsed_url = urlparse(self.path)
|
||||
|
|
|
@ -2,20 +2,26 @@
|
|||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
import json
|
||||
import webbrowser
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Optional, TYPE_CHECKING
|
||||
from urllib.parse import urlencode
|
||||
|
||||
import requests.exceptions
|
||||
|
||||
from PyQt5.QtCore import QUrl
|
||||
from PyQt5.QtGui import QDesktopServices
|
||||
|
||||
from UM.Logger import Logger
|
||||
from UM.Message import Message
|
||||
from UM.Signal import Signal
|
||||
|
||||
from cura.OAuth2.LocalAuthorizationServer import LocalAuthorizationServer
|
||||
from cura.OAuth2.AuthorizationHelpers import AuthorizationHelpers, TOKEN_TIMESTAMP_FORMAT
|
||||
from cura.OAuth2.Models import AuthenticationResponse
|
||||
|
||||
from UM.i18n import i18nCatalog
|
||||
i18n_catalog = i18nCatalog("cura")
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from cura.OAuth2.Models import UserProfile, OAuth2Settings
|
||||
from UM.Preferences import Preferences
|
||||
|
@ -30,6 +36,8 @@ class AuthorizationService:
|
|||
# Emit signal when authentication failed.
|
||||
onAuthenticationError = Signal()
|
||||
|
||||
accessTokenChanged = Signal()
|
||||
|
||||
def __init__(self, settings: "OAuth2Settings", preferences: Optional["Preferences"] = None) -> None:
|
||||
self._settings = settings
|
||||
self._auth_helpers = AuthorizationHelpers(settings)
|
||||
|
@ -39,6 +47,14 @@ class AuthorizationService:
|
|||
self._preferences = preferences
|
||||
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:
|
||||
if preferences is not None:
|
||||
self._preferences = preferences
|
||||
|
@ -56,6 +72,7 @@ class AuthorizationService:
|
|||
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:
|
||||
|
@ -71,6 +88,7 @@ class AuthorizationService:
|
|||
def _parseJWT(self) -> Optional["UserProfile"]:
|
||||
if not self._auth_data or self._auth_data.access_token is None:
|
||||
# If no auth data exists, we should always log in again.
|
||||
Logger.log("d", "There was no auth data or access token")
|
||||
return None
|
||||
user_data = self._auth_helpers.parseJWT(self._auth_data.access_token)
|
||||
if user_data:
|
||||
|
@ -78,12 +96,16 @@ class AuthorizationService:
|
|||
return user_data
|
||||
# The JWT was expired or invalid and we should request a new one.
|
||||
if self._auth_data.refresh_token is None:
|
||||
Logger.log("w", "There was no refresh token in the auth data.")
|
||||
return None
|
||||
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:
|
||||
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.
|
||||
return None
|
||||
|
||||
# Ensure it gets stored as otherwise we only have it in memory. The stored refresh token has been deleted
|
||||
# from the server already.
|
||||
self._storeAuthData(self._auth_data)
|
||||
return self._auth_helpers.parseJWT(self._auth_data.access_token)
|
||||
|
||||
## Get the access token as provided by the repsonse data.
|
||||
|
@ -96,7 +118,7 @@ class AuthorizationService:
|
|||
# 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))
|
||||
expiry_date = received_at + timedelta(seconds = float(self._auth_data.expires_in or 0) - 60)
|
||||
if datetime.now() > expiry_date:
|
||||
self.refreshAccessToken()
|
||||
|
||||
|
@ -107,8 +129,13 @@ class AuthorizationService:
|
|||
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.")
|
||||
return
|
||||
self._storeAuthData(self._auth_helpers.getAccessTokenUsingRefreshToken(self._auth_data.refresh_token))
|
||||
response = self._auth_helpers.getAccessTokenUsingRefreshToken(self._auth_data.refresh_token)
|
||||
if response.success:
|
||||
self._storeAuthData(response)
|
||||
self.onAuthStateChanged.emit(logged_in = True)
|
||||
else:
|
||||
Logger.log("w", "Failed to get a new access token from the server.")
|
||||
self.onAuthStateChanged.emit(logged_in = False)
|
||||
|
||||
## Delete the authentication data that we have stored locally (eg; logout)
|
||||
def deleteAuthData(self) -> None:
|
||||
|
@ -138,7 +165,7 @@ class AuthorizationService:
|
|||
})
|
||||
|
||||
# Open the authorization page in a new browser window.
|
||||
webbrowser.open_new("{}?{}".format(self._auth_url, query_string))
|
||||
QDesktopServices.openUrl(QUrl("{}?{}".format(self._auth_url, query_string)))
|
||||
|
||||
# Start a local web server to receive the callback URL on.
|
||||
self._server.start(verification_code)
|
||||
|
@ -161,12 +188,22 @@ class AuthorizationService:
|
|||
preferences_data = json.loads(self._preferences.getValue(self._settings.AUTH_DATA_PREFERENCE_KEY))
|
||||
if preferences_data:
|
||||
self._auth_data = AuthenticationResponse(**preferences_data)
|
||||
# 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.show()
|
||||
except ValueError:
|
||||
Logger.logException("w", "Could not load auth data from preferences")
|
||||
|
||||
## Store authentication data in preferences.
|
||||
def _storeAuthData(self, auth_data: Optional[AuthenticationResponse] = None) -> None:
|
||||
Logger.log("d", "Attempting to store the auth data")
|
||||
if self._preferences is None:
|
||||
Logger.log("e", "Unable to save authentication data, since no preference has been set!")
|
||||
return
|
||||
|
@ -178,3 +215,6 @@ class AuthorizationService:
|
|||
else:
|
||||
self._user_profile = None
|
||||
self._preferences.resetPreference(self._settings.AUTH_DATA_PREFERENCE_KEY)
|
||||
|
||||
self.accessTokenChanged.emit()
|
||||
|
||||
|
|
|
@ -63,6 +63,10 @@ class LocalAuthorizationServer:
|
|||
Logger.log("d", "Stopping local oauth2 web server...")
|
||||
|
||||
if self._web_server:
|
||||
try:
|
||||
self._web_server.server_close()
|
||||
except OSError:
|
||||
# OS error can happen if the socket was already closed. We really don't care about that case.
|
||||
pass
|
||||
self._web_server = None
|
||||
self._web_server_thread = None
|
||||
|
|
|
@ -1,94 +0,0 @@
|
|||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
from PyQt5.QtCore import QTimer
|
||||
|
||||
from UM.Application import Application
|
||||
from UM.Qt.ListModel import ListModel
|
||||
from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
|
||||
from UM.Scene.SceneNode import SceneNode
|
||||
from UM.Scene.Selection import Selection
|
||||
from UM.i18n import i18nCatalog
|
||||
from collections import defaultdict
|
||||
|
||||
catalog = i18nCatalog("cura")
|
||||
|
||||
|
||||
## Keep track of all objects in the project
|
||||
class ObjectsModel(ListModel):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
Application.getInstance().getController().getScene().sceneChanged.connect(self._updateDelayed)
|
||||
Application.getInstance().getPreferences().preferenceChanged.connect(self._updateDelayed)
|
||||
|
||||
self._update_timer = QTimer()
|
||||
self._update_timer.setInterval(100)
|
||||
self._update_timer.setSingleShot(True)
|
||||
self._update_timer.timeout.connect(self._update)
|
||||
|
||||
self._build_plate_number = -1
|
||||
|
||||
def setActiveBuildPlate(self, nr):
|
||||
self._build_plate_number = nr
|
||||
self._update()
|
||||
|
||||
def _updateDelayed(self, *args):
|
||||
self._update_timer.start()
|
||||
|
||||
def _update(self, *args):
|
||||
nodes = []
|
||||
filter_current_build_plate = Application.getInstance().getPreferences().getValue("view/filter_current_build_plate")
|
||||
active_build_plate_number = self._build_plate_number
|
||||
group_nr = 1
|
||||
name_count_dict = defaultdict(int)
|
||||
|
||||
for node in DepthFirstIterator(Application.getInstance().getController().getScene().getRoot()):
|
||||
if not isinstance(node, SceneNode):
|
||||
continue
|
||||
if (not node.getMeshData() and not node.callDecoration("getLayerData")) and not node.callDecoration("isGroup"):
|
||||
continue
|
||||
if node.getParent() and node.getParent().callDecoration("isGroup"):
|
||||
continue # Grouped nodes don't need resetting as their parent (the group) is resetted)
|
||||
if not node.callDecoration("isSliceable") and not node.callDecoration("isGroup"):
|
||||
continue
|
||||
node_build_plate_number = node.callDecoration("getBuildPlateNumber")
|
||||
if filter_current_build_plate and node_build_plate_number != active_build_plate_number:
|
||||
continue
|
||||
|
||||
if not node.callDecoration("isGroup"):
|
||||
name = node.getName()
|
||||
|
||||
else:
|
||||
name = catalog.i18nc("@label", "Group #{group_nr}").format(group_nr = str(group_nr))
|
||||
group_nr += 1
|
||||
|
||||
if hasattr(node, "isOutsideBuildArea"):
|
||||
is_outside_build_area = node.isOutsideBuildArea()
|
||||
else:
|
||||
is_outside_build_area = False
|
||||
|
||||
#check if we already have an instance of the object based on name
|
||||
name_count_dict[name] += 1
|
||||
name_count = name_count_dict[name]
|
||||
|
||||
if name_count > 1:
|
||||
name = "{0}({1})".format(name, name_count-1)
|
||||
node.setName(name)
|
||||
|
||||
nodes.append({
|
||||
"name": name,
|
||||
"isSelected": Selection.isSelected(node),
|
||||
"isOutsideBuildArea": is_outside_build_area,
|
||||
"buildPlateNumber": node_build_plate_number,
|
||||
"node": node
|
||||
})
|
||||
|
||||
nodes = sorted(nodes, key=lambda n: n["name"])
|
||||
self.setItems(nodes)
|
||||
|
||||
self.itemsChanged.emit()
|
||||
|
||||
@staticmethod
|
||||
def createObjectsModel():
|
||||
return ObjectsModel()
|
|
@ -1,149 +1,127 @@
|
|||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
# Copyright (c) 2019 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
import sys
|
||||
from typing import List
|
||||
|
||||
from shapely import affinity
|
||||
from shapely.geometry import Polygon
|
||||
|
||||
from UM.Scene.Iterator.Iterator import Iterator
|
||||
from UM.Scene.Iterator import Iterator
|
||||
from UM.Scene.SceneNode import SceneNode
|
||||
from functools import cmp_to_key
|
||||
|
||||
|
||||
# Iterator that determines the object print order when one-at a time mode is enabled.
|
||||
#
|
||||
# In one-at-a-time mode, only one extruder can be enabled to print. In order to maximize the number of objects we can
|
||||
# print, we need to print from the corner that's closest to the extruder that's being used. Here is an illustration:
|
||||
#
|
||||
# +--------------------------------+
|
||||
# | |
|
||||
# | |
|
||||
# | | - Rectangle represents the complete print head including fans, etc.
|
||||
# | X X | y - X's are the nozzles
|
||||
# | (1) (2) | ^
|
||||
# | | |
|
||||
# +--------------------------------+ +--> x
|
||||
#
|
||||
# In this case, the nozzles are symmetric, nozzle (1) is closer to the bottom left corner while (2) is closer to the
|
||||
# bottom right. If we use nozzle (1) to print, then we better off printing from the bottom left corner so the print
|
||||
# head will not collide into an object on its top-right side, which is a very large unused area. Following the same
|
||||
# logic, if we are printing with nozzle (2), then it's better to print from the bottom-right side.
|
||||
#
|
||||
# This iterator determines the print order following the rules above.
|
||||
#
|
||||
class OneAtATimeIterator(Iterator):
|
||||
|
||||
def __init__(self, scene_node):
|
||||
from cura.CuraApplication import CuraApplication
|
||||
self._global_stack = CuraApplication.getInstance().getGlobalContainerStack()
|
||||
self._original_node_list = []
|
||||
|
||||
## Iterator that returns a list of nodes in the order that they need to be printed
|
||||
# If there is no solution an empty list is returned.
|
||||
# Take note that the list of nodes can have children (that may or may not contain mesh data)
|
||||
class OneAtATimeIterator(Iterator.Iterator):
|
||||
def __init__(self, scene_node) -> None:
|
||||
super().__init__(scene_node) # Call super to make multiple inheritance work.
|
||||
self._hit_map = [[]] # type: List[List[bool]] # For each node, which other nodes this hits. A grid of booleans on which nodes hit which.
|
||||
self._original_node_list = [] # type: List[SceneNode] # The nodes that need to be checked for collisions.
|
||||
|
||||
def getMachineNearestCornerToExtruder(self, global_stack):
|
||||
head_and_fans_coordinates = global_stack.getHeadAndFansCoordinates()
|
||||
|
||||
used_extruder = None
|
||||
for extruder in global_stack.extruders.values():
|
||||
if extruder.isEnabled:
|
||||
used_extruder = extruder
|
||||
break
|
||||
|
||||
extruder_offsets = [used_extruder.getProperty("machine_nozzle_offset_x", "value"),
|
||||
used_extruder.getProperty("machine_nozzle_offset_y", "value")]
|
||||
|
||||
# find the corner that's closest to the origin
|
||||
min_distance2 = sys.maxsize
|
||||
min_coord = None
|
||||
for coord in head_and_fans_coordinates:
|
||||
x = coord[0] - extruder_offsets[0]
|
||||
y = coord[1] - extruder_offsets[1]
|
||||
|
||||
distance2 = x**2 + y**2
|
||||
if distance2 <= min_distance2:
|
||||
min_distance2 = distance2
|
||||
min_coord = coord
|
||||
|
||||
return min_coord
|
||||
|
||||
def _checkForCollisions(self) -> bool:
|
||||
all_nodes = []
|
||||
for node in self._scene_node.getChildren():
|
||||
if not issubclass(type(node), SceneNode):
|
||||
continue
|
||||
convex_hull = node.callDecoration("getConvexHullHead")
|
||||
if not convex_hull:
|
||||
continue
|
||||
|
||||
bounding_box = node.getBoundingBox()
|
||||
if not bounding_box:
|
||||
continue
|
||||
from UM.Math.Polygon import Polygon
|
||||
bounding_box_polygon = Polygon([[bounding_box.left, bounding_box.front],
|
||||
[bounding_box.left, bounding_box.back],
|
||||
[bounding_box.right, bounding_box.back],
|
||||
[bounding_box.right, bounding_box.front]])
|
||||
|
||||
all_nodes.append({"node": node,
|
||||
"bounding_box": bounding_box_polygon,
|
||||
"convex_hull": convex_hull})
|
||||
|
||||
has_collisions = False
|
||||
for i, node_dict in enumerate(all_nodes):
|
||||
for j, other_node_dict in enumerate(all_nodes):
|
||||
if i == j:
|
||||
continue
|
||||
if node_dict["bounding_box"].intersectsPolygon(other_node_dict["convex_hull"]):
|
||||
has_collisions = True
|
||||
break
|
||||
|
||||
if has_collisions:
|
||||
break
|
||||
|
||||
return has_collisions
|
||||
|
||||
def _fillStack(self):
|
||||
min_coord = self.getMachineNearestCornerToExtruder(self._global_stack)
|
||||
transform_x = -int(round(min_coord[0] / abs(min_coord[0])))
|
||||
transform_y = -int(round(min_coord[1] / abs(min_coord[1])))
|
||||
|
||||
machine_size = [self._global_stack.getProperty("machine_width", "value"),
|
||||
self._global_stack.getProperty("machine_depth", "value")]
|
||||
|
||||
def flip_x(polygon):
|
||||
tm2 = [-1, 0, 0, 1, 0, 0]
|
||||
return affinity.affine_transform(affinity.translate(polygon, xoff = -machine_size[0]), tm2)
|
||||
|
||||
def flip_y(polygon):
|
||||
tm2 = [1, 0, 0, -1, 0, 0]
|
||||
return affinity.affine_transform(affinity.translate(polygon, yoff = -machine_size[1]), tm2)
|
||||
|
||||
if self._checkForCollisions():
|
||||
self._node_stack = []
|
||||
return
|
||||
|
||||
## Fills the ``_node_stack`` with a list of scene nodes that need to be
|
||||
# printed in order.
|
||||
def _fillStack(self) -> None:
|
||||
node_list = []
|
||||
for node in self._scene_node.getChildren():
|
||||
if not issubclass(type(node), SceneNode):
|
||||
continue
|
||||
|
||||
convex_hull = node.callDecoration("getConvexHull")
|
||||
if convex_hull:
|
||||
xmin = min(x for x, _ in convex_hull._points)
|
||||
xmax = max(x for x, _ in convex_hull._points)
|
||||
ymin = min(y for _, y in convex_hull._points)
|
||||
ymax = max(y for _, y in convex_hull._points)
|
||||
if node.callDecoration("getConvexHull"):
|
||||
node_list.append(node)
|
||||
|
||||
convex_hull_polygon = Polygon.from_bounds(xmin, ymin, xmax, ymax)
|
||||
if transform_x < 0:
|
||||
convex_hull_polygon = flip_x(convex_hull_polygon)
|
||||
if transform_y < 0:
|
||||
convex_hull_polygon = flip_y(convex_hull_polygon)
|
||||
|
||||
node_list.append({"node": node,
|
||||
"min_coord": [convex_hull_polygon.bounds[0], convex_hull_polygon.bounds[1]],
|
||||
})
|
||||
if len(node_list) < 2:
|
||||
self._node_stack = node_list[:]
|
||||
return
|
||||
|
||||
node_list = sorted(node_list, key = lambda d: d["min_coord"])
|
||||
# Copy the list
|
||||
self._original_node_list = node_list[:]
|
||||
|
||||
self._node_stack = [d["node"] for d in node_list]
|
||||
## Initialise the hit map (pre-compute all hits between all objects)
|
||||
self._hit_map = [[self._checkHit(i,j) for i in node_list] for j in node_list]
|
||||
|
||||
# Check if we have to files that block each other. If this is the case, there is no solution!
|
||||
for a in range(0, len(node_list)):
|
||||
for b in range(0, len(node_list)):
|
||||
if a != b and self._hit_map[a][b] and self._hit_map[b][a]:
|
||||
return
|
||||
|
||||
# Sort the original list so that items that block the most other objects are at the beginning.
|
||||
# This does not decrease the worst case running time, but should improve it in most cases.
|
||||
sorted(node_list, key = cmp_to_key(self._calculateScore))
|
||||
|
||||
todo_node_list = [_ObjectOrder([], node_list)]
|
||||
while len(todo_node_list) > 0:
|
||||
current = todo_node_list.pop()
|
||||
for node in current.todo:
|
||||
# Check if the object can be placed with what we have and still allows for a solution in the future
|
||||
if not self._checkHitMultiple(node, current.order) and not self._checkBlockMultiple(node, current.todo):
|
||||
# We found a possible result. Create new todo & order list.
|
||||
new_todo_list = current.todo[:]
|
||||
new_todo_list.remove(node)
|
||||
new_order = current.order[:] + [node]
|
||||
if len(new_todo_list) == 0:
|
||||
# We have no more nodes to check, so quit looking.
|
||||
self._node_stack = new_order
|
||||
return
|
||||
todo_node_list.append(_ObjectOrder(new_order, new_todo_list))
|
||||
self._node_stack = [] #No result found!
|
||||
|
||||
|
||||
# Check if first object can be printed before the provided list (using the hit map)
|
||||
def _checkHitMultiple(self, node: SceneNode, other_nodes: List[SceneNode]) -> bool:
|
||||
node_index = self._original_node_list.index(node)
|
||||
for other_node in other_nodes:
|
||||
other_node_index = self._original_node_list.index(other_node)
|
||||
if self._hit_map[node_index][other_node_index]:
|
||||
return True
|
||||
return False
|
||||
|
||||
## Check for a node whether it hits any of the other nodes.
|
||||
# \param node The node to check whether it collides with the other nodes.
|
||||
# \param other_nodes The nodes to check for collisions.
|
||||
def _checkBlockMultiple(self, node: SceneNode, other_nodes: List[SceneNode]) -> bool:
|
||||
node_index = self._original_node_list.index(node)
|
||||
for other_node in other_nodes:
|
||||
other_node_index = self._original_node_list.index(other_node)
|
||||
if self._hit_map[other_node_index][node_index] and node_index != other_node_index:
|
||||
return True
|
||||
return False
|
||||
|
||||
## Calculate score simply sums the number of other objects it 'blocks'
|
||||
def _calculateScore(self, a: SceneNode, b: SceneNode) -> int:
|
||||
score_a = sum(self._hit_map[self._original_node_list.index(a)])
|
||||
score_b = sum(self._hit_map[self._original_node_list.index(b)])
|
||||
return score_a - score_b
|
||||
|
||||
## Checks if A can be printed before B
|
||||
def _checkHit(self, a: SceneNode, b: SceneNode) -> bool:
|
||||
if a == b:
|
||||
return False
|
||||
|
||||
a_hit_hull = a.callDecoration("getConvexHullBoundary")
|
||||
b_hit_hull = b.callDecoration("getConvexHullHeadFull")
|
||||
overlap = a_hit_hull.intersectsPolygon(b_hit_hull)
|
||||
|
||||
if overlap:
|
||||
return True
|
||||
|
||||
# Adhesion areas must never overlap, regardless of printing order
|
||||
# This would cause over-extrusion
|
||||
a_hit_hull = a.callDecoration("getAdhesionArea")
|
||||
b_hit_hull = b.callDecoration("getAdhesionArea")
|
||||
overlap = a_hit_hull.intersectsPolygon(b_hit_hull)
|
||||
|
||||
if overlap:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
## Internal object used to keep track of a possible order in which to print objects.
|
||||
class _ObjectOrder:
|
||||
## Creates the _ObjectOrder instance.
|
||||
# \param order List of indices in which to print objects, ordered by printing
|
||||
# order.
|
||||
# \param todo: List of indices which are not yet inserted into the order list.
|
||||
def __init__(self, order: List[SceneNode], todo: List[SceneNode]):
|
||||
self.order = order
|
||||
self.todo = todo
|
||||
|
|
|
@ -29,4 +29,4 @@ class PlatformPhysicsOperation(Operation):
|
|||
return group
|
||||
|
||||
def __repr__(self):
|
||||
return "PlatformPhysicsOperation(translation = {0})".format(self._translation)
|
||||
return "PlatformPhysicsOp.(trans.={0})".format(self._translation)
|
||||
|
|
|
@ -40,8 +40,9 @@ class PlatformPhysics:
|
|||
Application.getInstance().getPreferences().addPreference("physics/automatic_drop_down", True)
|
||||
|
||||
def _onSceneChanged(self, source):
|
||||
if not source.getMeshData():
|
||||
if not source.callDecoration("isSliceable"):
|
||||
return
|
||||
|
||||
self._change_timer.start()
|
||||
|
||||
def _onChangeTimerFinished(self):
|
||||
|
@ -49,18 +50,20 @@ class PlatformPhysics:
|
|||
return
|
||||
|
||||
root = self._controller.getScene().getRoot()
|
||||
build_volume = Application.getInstance().getBuildVolume()
|
||||
build_volume.updateNodeBoundaryCheck()
|
||||
|
||||
# Keep a list of nodes that are moving. We use this so that we don't move two intersecting objects in the
|
||||
# same direction.
|
||||
transformed_nodes = []
|
||||
|
||||
# We try to shuffle all the nodes to prevent "locked" situations, where iteration B inverts iteration A.
|
||||
# By shuffling the order of the nodes, this might happen a few times, but at some point it will resolve.
|
||||
nodes = list(BreadthFirstIterator(root))
|
||||
|
||||
# Only check nodes inside build area.
|
||||
nodes = [node for node in nodes if (hasattr(node, "_outside_buildarea") and not node._outside_buildarea)]
|
||||
|
||||
# We try to shuffle all the nodes to prevent "locked" situations, where iteration B inverts iteration A.
|
||||
# By shuffling the order of the nodes, this might happen a few times, but at some point it will resolve.
|
||||
random.shuffle(nodes)
|
||||
for node in nodes:
|
||||
if node is root or not isinstance(node, SceneNode) or node.getBoundingBox() is None:
|
||||
|
@ -76,7 +79,7 @@ class PlatformPhysics:
|
|||
move_vector = move_vector.set(y = -bbox.bottom + z_offset)
|
||||
|
||||
# If there is no convex hull for the node, start calculating it and continue.
|
||||
if not node.getDecorator(ConvexHullDecorator):
|
||||
if not node.getDecorator(ConvexHullDecorator) and not node.callDecoration("isNonPrintingMesh"):
|
||||
node.addDecorator(ConvexHullDecorator())
|
||||
|
||||
# only push away objects if this node is a printing mesh
|
||||
|
@ -160,7 +163,6 @@ class PlatformPhysics:
|
|||
op.push()
|
||||
|
||||
# After moving, we have to evaluate the boundary checks for nodes
|
||||
build_volume = Application.getInstance().getBuildVolume()
|
||||
build_volume.updateNodeBoundaryCheck()
|
||||
|
||||
def _onToolOperationStarted(self, tool):
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
from typing import Optional, TYPE_CHECKING
|
||||
from typing import Optional, TYPE_CHECKING, cast
|
||||
|
||||
|
||||
from UM.Application import Application
|
||||
from UM.Resources import Resources
|
||||
|
@ -12,6 +13,7 @@ from UM.View.RenderBatch import RenderBatch
|
|||
|
||||
|
||||
from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
|
||||
from cura.Scene.CuraSceneNode import CuraSceneNode
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from UM.View.GL.ShaderProgram import ShaderProgram
|
||||
|
@ -44,9 +46,9 @@ class PreviewPass(RenderPass):
|
|||
|
||||
self._renderer = Application.getInstance().getRenderer()
|
||||
|
||||
self._shader = None #type: Optional[ShaderProgram]
|
||||
self._non_printing_shader = None #type: Optional[ShaderProgram]
|
||||
self._support_mesh_shader = None #type: Optional[ShaderProgram]
|
||||
self._shader = None # type: Optional[ShaderProgram]
|
||||
self._non_printing_shader = None # type: Optional[ShaderProgram]
|
||||
self._support_mesh_shader = None # type: Optional[ShaderProgram]
|
||||
self._scene = Application.getInstance().getController().getScene()
|
||||
|
||||
# Set the camera to be used by this render pass
|
||||
|
@ -62,6 +64,7 @@ class PreviewPass(RenderPass):
|
|||
self._shader.setUniformValue("u_ambientColor", [0.1, 0.1, 0.1, 1.0])
|
||||
self._shader.setUniformValue("u_specularColor", [0.6, 0.6, 0.6, 1.0])
|
||||
self._shader.setUniformValue("u_shininess", 20.0)
|
||||
self._shader.setUniformValue("u_faceId", -1) # Don't render any selected faces in the preview.
|
||||
|
||||
if not self._non_printing_shader:
|
||||
if self._non_printing_shader:
|
||||
|
@ -83,7 +86,8 @@ class PreviewPass(RenderPass):
|
|||
batch_support_mesh = RenderBatch(self._support_mesh_shader)
|
||||
|
||||
# Fill up the batch with objects that can be sliced.
|
||||
for node in DepthFirstIterator(self._scene.getRoot()): #type: ignore #Ignore type error because iter() should get called automatically by Python syntax.
|
||||
for node in DepthFirstIterator(self._scene.getRoot()):
|
||||
if hasattr(node, "_outside_buildarea") and not getattr(node, "_outside_buildarea"):
|
||||
if node.callDecoration("isSliceable") and node.getMeshData() and node.isVisible():
|
||||
per_mesh_stack = node.callDecoration("getStack")
|
||||
if node.callDecoration("isNonThumbnailVisibleMesh"):
|
||||
|
@ -93,7 +97,7 @@ class PreviewPass(RenderPass):
|
|||
# Support mesh
|
||||
uniforms = {}
|
||||
shade_factor = 0.6
|
||||
diffuse_color = node.getDiffuseColor()
|
||||
diffuse_color = cast(CuraSceneNode, node).getDiffuseColor()
|
||||
diffuse_color2 = [
|
||||
diffuse_color[0] * shade_factor,
|
||||
diffuse_color[1] * shade_factor,
|
||||
|
@ -105,7 +109,7 @@ class PreviewPass(RenderPass):
|
|||
else:
|
||||
# Normal scene node
|
||||
uniforms = {}
|
||||
uniforms["diffuse_color"] = prettier_color(node.getDiffuseColor())
|
||||
uniforms["diffuse_color"] = prettier_color(cast(CuraSceneNode, node).getDiffuseColor())
|
||||
batch.addItem(node.getWorldTransformation(), node.getMeshData(), uniforms = uniforms)
|
||||
|
||||
self.bind()
|
||||
|
|
|
@ -9,7 +9,7 @@ from typing import Union
|
|||
|
||||
MYPY = False
|
||||
if MYPY:
|
||||
from cura.PrinterOutputDevice import PrinterOutputDevice
|
||||
from cura.PrinterOutput.PrinterOutputDevice import PrinterOutputDevice
|
||||
|
||||
class FirmwareUpdater(QObject):
|
||||
firmwareProgressChanged = pyqtSignal()
|
||||
|
@ -20,7 +20,7 @@ class FirmwareUpdater(QObject):
|
|||
|
||||
self._output_device = output_device
|
||||
|
||||
self._update_firmware_thread = Thread(target=self._updateFirmware, daemon=True)
|
||||
self._update_firmware_thread = Thread(target=self._updateFirmware, daemon=True, name = "FirmwareUpdateThread")
|
||||
|
||||
self._firmware_file = ""
|
||||
self._firmware_progress = 0
|
||||
|
@ -43,7 +43,7 @@ class FirmwareUpdater(QObject):
|
|||
## Cleanup after a succesful update
|
||||
def _cleanupAfterUpdate(self) -> None:
|
||||
# Clean up for next attempt.
|
||||
self._update_firmware_thread = Thread(target=self._updateFirmware, daemon=True)
|
||||
self._update_firmware_thread = Thread(target=self._updateFirmware, daemon=True, name = "FirmwareUpdateThread")
|
||||
self._firmware_file = ""
|
||||
self._onFirmwareProgress(100)
|
||||
self._setFirmwareUpdateState(FirmwareUpdateState.completed)
|
||||
|
|
|
@ -3,14 +3,15 @@
|
|||
|
||||
from typing import TYPE_CHECKING, Set, Union, Optional
|
||||
|
||||
from cura.PrinterOutput.PrinterOutputController import PrinterOutputController
|
||||
from PyQt5.QtCore import QTimer
|
||||
|
||||
from .PrinterOutputController import PrinterOutputController
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from cura.PrinterOutput.PrintJobOutputModel import PrintJobOutputModel
|
||||
from cura.PrinterOutput.PrinterOutputModel import PrinterOutputModel
|
||||
from cura.PrinterOutput.PrinterOutputDevice import PrinterOutputDevice
|
||||
from cura.PrinterOutput.ExtruderOutputModel import ExtruderOutputModel
|
||||
from .Models.PrintJobOutputModel import PrintJobOutputModel
|
||||
from .Models.PrinterOutputModel import PrinterOutputModel
|
||||
from .PrinterOutputDevice import PrinterOutputDevice
|
||||
from .Models.ExtruderOutputModel import ExtruderOutputModel
|
||||
|
||||
|
||||
class GenericOutputController(PrinterOutputController):
|
||||
|
@ -54,7 +55,7 @@ class GenericOutputController(PrinterOutputController):
|
|||
self._preheat_hotends_timer.stop()
|
||||
for extruder in self._preheat_hotends:
|
||||
extruder.updateIsPreheating(False)
|
||||
self._preheat_hotends = set() # type: Set[ExtruderOutputModel]
|
||||
self._preheat_hotends = set()
|
||||
|
||||
def moveHead(self, printer: "PrinterOutputModel", x, y, z, speed) -> None:
|
||||
self._output_device.sendCommand("G91")
|
||||
|
@ -158,7 +159,7 @@ class GenericOutputController(PrinterOutputController):
|
|||
def _onPreheatHotendsTimerFinished(self) -> None:
|
||||
for extruder in self._preheat_hotends:
|
||||
self.setTargetHotendTemperature(extruder.getPrinter(), extruder.getPosition(), 0)
|
||||
self._preheat_hotends = set() #type: Set[ExtruderOutputModel]
|
||||
self._preheat_hotends = set()
|
||||
|
||||
# Cancel any ongoing preheating timers, without setting back the temperature to 0
|
||||
# This can be used eg at the start of a print
|
||||
|
@ -166,7 +167,7 @@ class GenericOutputController(PrinterOutputController):
|
|||
if self._preheat_hotends_timer.isActive():
|
||||
for extruder in self._preheat_hotends:
|
||||
extruder.updateIsPreheating(False)
|
||||
self._preheat_hotends = set() #type: Set[ExtruderOutputModel]
|
||||
self._preheat_hotends = set()
|
||||
|
||||
self._preheat_hotends_timer.stop()
|
||||
|
||||
|
|
|
@ -1,34 +0,0 @@
|
|||
# Copyright (c) 2017 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
from PyQt5.QtCore import pyqtSignal, pyqtProperty, QObject, QVariant, pyqtSlot
|
||||
|
||||
|
||||
class MaterialOutputModel(QObject):
|
||||
def __init__(self, guid, type, color, brand, name, parent = None):
|
||||
super().__init__(parent)
|
||||
self._guid = guid
|
||||
self._type = type
|
||||
self._color = color
|
||||
self._brand = brand
|
||||
self._name = name
|
||||
|
||||
@pyqtProperty(str, constant = True)
|
||||
def guid(self):
|
||||
return self._guid
|
||||
|
||||
@pyqtProperty(str, constant=True)
|
||||
def type(self):
|
||||
return self._type
|
||||
|
||||
@pyqtProperty(str, constant=True)
|
||||
def brand(self):
|
||||
return self._brand
|
||||
|
||||
@pyqtProperty(str, constant=True)
|
||||
def color(self):
|
||||
return self._color
|
||||
|
||||
@pyqtProperty(str, constant=True)
|
||||
def name(self):
|
||||
return self._name
|
|
@ -4,7 +4,7 @@ from typing import Optional
|
|||
|
||||
from PyQt5.QtCore import pyqtProperty, QObject, pyqtSignal
|
||||
|
||||
from cura.PrinterOutput.MaterialOutputModel import MaterialOutputModel
|
||||
from .MaterialOutputModel import MaterialOutputModel
|
||||
|
||||
|
||||
class ExtruderConfigurationModel(QObject):
|
||||
|
@ -25,7 +25,8 @@ class ExtruderConfigurationModel(QObject):
|
|||
return self._position
|
||||
|
||||
def setMaterial(self, material: Optional[MaterialOutputModel]) -> None:
|
||||
if self._hotend_id != material:
|
||||
if material is None or self._material == material:
|
||||
return
|
||||
self._material = material
|
||||
self.extruderConfigurationChanged.emit()
|
||||
|
||||
|
@ -33,7 +34,7 @@ class ExtruderConfigurationModel(QObject):
|
|||
def activeMaterial(self) -> Optional[MaterialOutputModel]:
|
||||
return self._material
|
||||
|
||||
@pyqtProperty(QObject, fset=setMaterial, notify=extruderConfigurationChanged)
|
||||
@pyqtProperty(QObject, fset = setMaterial, notify = extruderConfigurationChanged)
|
||||
def material(self) -> Optional[MaterialOutputModel]:
|
||||
return self._material
|
||||
|
||||
|
@ -62,7 +63,24 @@ class ExtruderConfigurationModel(QObject):
|
|||
return " ".join(message_chunks)
|
||||
|
||||
def __eq__(self, other) -> bool:
|
||||
return hash(self) == hash(other)
|
||||
if not isinstance(other, ExtruderConfigurationModel):
|
||||
return False
|
||||
|
||||
if self._position != other.position:
|
||||
return False
|
||||
# Empty materials should be ignored for comparison
|
||||
if self.activeMaterial is not None and other.activeMaterial is not None:
|
||||
if self.activeMaterial.guid != other.activeMaterial.guid:
|
||||
if self.activeMaterial.guid != "" and other.activeMaterial.guid != "":
|
||||
return False
|
||||
else:
|
||||
# At this point there is no material, so it doesn't matter what the hotend is.
|
||||
return True
|
||||
|
||||
if self.hotendID != other.hotendID:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
# Calculating a hash function using the position of the extruder, the material GUID and the hotend id to check if is
|
||||
# unique within a set
|
|
@ -1,14 +1,15 @@
|
|||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
# 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 PyQt5.QtCore import pyqtSignal, pyqtProperty, QObject, pyqtSlot
|
||||
|
||||
from .ExtruderConfigurationModel import ExtruderConfigurationModel
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from cura.PrinterOutput.PrinterOutputModel import PrinterOutputModel
|
||||
from cura.PrinterOutput.MaterialOutputModel import MaterialOutputModel
|
||||
from .MaterialOutputModel import MaterialOutputModel
|
||||
from .PrinterOutputModel import PrinterOutputModel
|
||||
|
||||
|
||||
class ExtruderOutputModel(QObject):
|
44
cura/PrinterOutput/Models/MaterialOutputModel.py
Normal file
44
cura/PrinterOutput/Models/MaterialOutputModel.py
Normal file
|
@ -0,0 +1,44 @@
|
|||
# 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
|
||||
|
||||
def __eq__(self, other):
|
||||
if self is other:
|
||||
return True
|
||||
if type(other) is not MaterialOutputModel:
|
||||
return False
|
||||
|
||||
return self.guid == other.guid and self.type == other.type and self.brand == other.brand and self.color == other.color and self.name == other.name
|
171
cura/PrinterOutput/Models/PrintJobOutputModel.py
Normal file
171
cura/PrinterOutput/Models/PrintJobOutputModel.py
Normal file
|
@ -0,0 +1,171 @@
|
|||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
from typing import Optional, TYPE_CHECKING, List
|
||||
|
||||
from PyQt5.QtCore import pyqtSignal, pyqtProperty, QObject, pyqtSlot, QUrl
|
||||
from PyQt5.QtGui import QImage
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from cura.PrinterOutput.PrinterOutputController import PrinterOutputController
|
||||
from cura.PrinterOutput.Models.PrinterOutputModel import PrinterOutputModel
|
||||
from cura.PrinterOutput.Models.PrinterConfigurationModel import PrinterConfigurationModel
|
||||
|
||||
|
||||
class PrintJobOutputModel(QObject):
|
||||
stateChanged = pyqtSignal()
|
||||
timeTotalChanged = pyqtSignal()
|
||||
timeElapsedChanged = pyqtSignal()
|
||||
nameChanged = pyqtSignal()
|
||||
keyChanged = pyqtSignal()
|
||||
assignedPrinterChanged = pyqtSignal()
|
||||
ownerChanged = pyqtSignal()
|
||||
configurationChanged = pyqtSignal()
|
||||
previewImageChanged = pyqtSignal()
|
||||
compatibleMachineFamiliesChanged = pyqtSignal()
|
||||
|
||||
def __init__(self, output_controller: "PrinterOutputController", key: str = "", name: str = "", parent = None) -> None:
|
||||
super().__init__(parent)
|
||||
self._output_controller = output_controller
|
||||
self._state = ""
|
||||
self._time_total = 0
|
||||
self._time_elapsed = 0
|
||||
self._name = name # Human readable name
|
||||
self._key = key # Unique identifier
|
||||
self._assigned_printer = None # type: Optional[PrinterOutputModel]
|
||||
self._owner = "" # Who started/owns the print job?
|
||||
|
||||
self._configuration = None # type: Optional[PrinterConfigurationModel]
|
||||
self._compatible_machine_families = [] # type: List[str]
|
||||
self._preview_image_id = 0
|
||||
|
||||
self._preview_image = None # type: Optional[QImage]
|
||||
|
||||
@pyqtProperty("QStringList", notify=compatibleMachineFamiliesChanged)
|
||||
def compatibleMachineFamilies(self):
|
||||
# Hack; Some versions of cluster will return a family more than once...
|
||||
return list(set(self._compatible_machine_families))
|
||||
|
||||
def setCompatibleMachineFamilies(self, compatible_machine_families: List[str]) -> None:
|
||||
if self._compatible_machine_families != compatible_machine_families:
|
||||
self._compatible_machine_families = compatible_machine_families
|
||||
self.compatibleMachineFamiliesChanged.emit()
|
||||
|
||||
@pyqtProperty(QUrl, notify=previewImageChanged)
|
||||
def previewImageUrl(self):
|
||||
self._preview_image_id += 1
|
||||
# There is an image provider that is called "print_job_preview". In order to ensure that the image qml object, that
|
||||
# requires a QUrl to function, updates correctly we add an increasing number. This causes to see the QUrl
|
||||
# as new (instead of relying on cached version and thus forces an update.
|
||||
temp = "image://print_job_preview/" + str(self._preview_image_id) + "/" + self._key
|
||||
return QUrl(temp, QUrl.TolerantMode)
|
||||
|
||||
def getPreviewImage(self) -> Optional[QImage]:
|
||||
return self._preview_image
|
||||
|
||||
def updatePreviewImage(self, preview_image: Optional[QImage]) -> None:
|
||||
if self._preview_image != preview_image:
|
||||
self._preview_image = preview_image
|
||||
self.previewImageChanged.emit()
|
||||
|
||||
@pyqtProperty(QObject, notify=configurationChanged)
|
||||
def configuration(self) -> Optional["PrinterConfigurationModel"]:
|
||||
return self._configuration
|
||||
|
||||
def updateConfiguration(self, configuration: Optional["PrinterConfigurationModel"]) -> None:
|
||||
if self._configuration != configuration:
|
||||
self._configuration = configuration
|
||||
self.configurationChanged.emit()
|
||||
|
||||
@pyqtProperty(str, notify=ownerChanged)
|
||||
def owner(self):
|
||||
return self._owner
|
||||
|
||||
def updateOwner(self, owner):
|
||||
if self._owner != owner:
|
||||
self._owner = owner
|
||||
self.ownerChanged.emit()
|
||||
|
||||
@pyqtProperty(QObject, notify=assignedPrinterChanged)
|
||||
def assignedPrinter(self):
|
||||
return self._assigned_printer
|
||||
|
||||
def updateAssignedPrinter(self, assigned_printer: Optional["PrinterOutputModel"]) -> None:
|
||||
if self._assigned_printer != assigned_printer:
|
||||
old_printer = self._assigned_printer
|
||||
self._assigned_printer = assigned_printer
|
||||
if old_printer is not None:
|
||||
# If the previously assigned printer is set, this job is moved away from it.
|
||||
old_printer.updateActivePrintJob(None)
|
||||
self.assignedPrinterChanged.emit()
|
||||
|
||||
@pyqtProperty(str, notify=keyChanged)
|
||||
def key(self):
|
||||
return self._key
|
||||
|
||||
def updateKey(self, key: str):
|
||||
if self._key != key:
|
||||
self._key = key
|
||||
self.keyChanged.emit()
|
||||
|
||||
@pyqtProperty(str, notify = nameChanged)
|
||||
def name(self):
|
||||
return self._name
|
||||
|
||||
def updateName(self, name: str):
|
||||
if self._name != name:
|
||||
self._name = name
|
||||
self.nameChanged.emit()
|
||||
|
||||
@pyqtProperty(int, notify = timeTotalChanged)
|
||||
def timeTotal(self) -> int:
|
||||
return self._time_total
|
||||
|
||||
@pyqtProperty(int, notify = timeElapsedChanged)
|
||||
def timeElapsed(self) -> int:
|
||||
return self._time_elapsed
|
||||
|
||||
@pyqtProperty(int, notify = timeElapsedChanged)
|
||||
def timeRemaining(self) -> int:
|
||||
# Never get a negative time remaining
|
||||
return max(self.timeTotal - self.timeElapsed, 0)
|
||||
|
||||
@pyqtProperty(float, notify = timeElapsedChanged)
|
||||
def progress(self) -> float:
|
||||
result = float(self.timeElapsed) / max(self.timeTotal, 1.0) # Prevent a division by zero exception.
|
||||
return min(result, 1.0) # Never get a progress past 1.0
|
||||
|
||||
@pyqtProperty(str, notify=stateChanged)
|
||||
def state(self) -> str:
|
||||
return self._state
|
||||
|
||||
@pyqtProperty(bool, notify=stateChanged)
|
||||
def isActive(self) -> bool:
|
||||
inactive_states = [
|
||||
"pausing",
|
||||
"paused",
|
||||
"resuming",
|
||||
"wait_cleanup"
|
||||
]
|
||||
if self.state in inactive_states and self.timeRemaining > 0:
|
||||
return False
|
||||
return True
|
||||
|
||||
def updateTimeTotal(self, new_time_total):
|
||||
if self._time_total != new_time_total:
|
||||
self._time_total = new_time_total
|
||||
self.timeTotalChanged.emit()
|
||||
|
||||
def updateTimeElapsed(self, new_time_elapsed):
|
||||
if self._time_elapsed != new_time_elapsed:
|
||||
self._time_elapsed = new_time_elapsed
|
||||
self.timeElapsedChanged.emit()
|
||||
|
||||
def updateState(self, new_state):
|
||||
if self._state != new_state:
|
||||
self._state = new_state
|
||||
self.stateChanged.emit()
|
||||
|
||||
@pyqtSlot(str)
|
||||
def setState(self, state):
|
||||
self._output_controller.setJobState(self, state)
|
|
@ -6,10 +6,10 @@ from typing import List
|
|||
|
||||
MYPY = False
|
||||
if MYPY:
|
||||
from cura.PrinterOutput.ExtruderConfigurationModel import ExtruderConfigurationModel
|
||||
from cura.PrinterOutput.Models.ExtruderConfigurationModel import ExtruderConfigurationModel
|
||||
|
||||
|
||||
class ConfigurationModel(QObject):
|
||||
class PrinterConfigurationModel(QObject):
|
||||
|
||||
configurationChanged = pyqtSignal()
|
||||
|
||||
|
@ -19,14 +19,14 @@ class ConfigurationModel(QObject):
|
|||
self._extruder_configurations = [] # type: List[ExtruderConfigurationModel]
|
||||
self._buildplate_configuration = ""
|
||||
|
||||
def setPrinterType(self, printer_type):
|
||||
def setPrinterType(self, printer_type: str) -> None:
|
||||
self._printer_type = printer_type
|
||||
|
||||
@pyqtProperty(str, fset = setPrinterType, notify = configurationChanged)
|
||||
def printerType(self) -> str:
|
||||
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:
|
||||
self._extruder_configurations = extruder_configurations
|
||||
|
||||
|
@ -58,6 +58,14 @@ class ConfigurationModel(QObject):
|
|||
return False
|
||||
return self._printer_type != ""
|
||||
|
||||
def hasAnyMaterialLoaded(self) -> bool:
|
||||
if not self.isValid():
|
||||
return False
|
||||
for configuration in self._extruder_configurations:
|
||||
if configuration.activeMaterial and configuration.activeMaterial.type != "empty":
|
||||
return True
|
||||
return False
|
||||
|
||||
def __str__(self):
|
||||
message_chunks = []
|
||||
message_chunks.append("Printer type: " + self._printer_type)
|
||||
|
@ -71,7 +79,23 @@ class ConfigurationModel(QObject):
|
|||
return "\n".join(message_chunks)
|
||||
|
||||
def __eq__(self, other):
|
||||
return hash(self) == hash(other)
|
||||
if not isinstance(other, PrinterConfigurationModel):
|
||||
return False
|
||||
|
||||
if self.printerType != other.printerType:
|
||||
return False
|
||||
|
||||
if self.buildplateConfiguration != other.buildplateConfiguration:
|
||||
return False
|
||||
|
||||
if len(self.extruderConfigurations) != len(other.extruderConfigurations):
|
||||
return False
|
||||
|
||||
for self_extruder, other_extruder in zip(sorted(self._extruder_configurations, key=lambda x: x.position), sorted(other.extruderConfigurations, key=lambda x: x.position)):
|
||||
if self_extruder != other_extruder:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
## The hash function is used to compare and create unique sets. The configuration is unique if the configuration
|
||||
# of the extruders is unique (the order of the extruders matters), and the type and buildplate is the same.
|
350
cura/PrinterOutput/Models/PrinterOutputModel.py
Normal file
350
cura/PrinterOutput/Models/PrinterOutputModel.py
Normal file
|
@ -0,0 +1,350 @@
|
|||
# Copyright (c) 2019 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
from PyQt5.QtCore import pyqtSignal, pyqtProperty, QObject, QVariant, pyqtSlot, QUrl
|
||||
from typing import List, Dict, Optional, TYPE_CHECKING
|
||||
from UM.Math.Vector import Vector
|
||||
from cura.PrinterOutput.Peripheral import Peripheral
|
||||
from cura.PrinterOutput.Models.PrinterConfigurationModel import PrinterConfigurationModel
|
||||
from cura.PrinterOutput.Models.ExtruderOutputModel import ExtruderOutputModel
|
||||
from UM.Logger import Logger
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from cura.PrinterOutput.Models.PrintJobOutputModel import PrintJobOutputModel
|
||||
from cura.PrinterOutput.PrinterOutputController import PrinterOutputController
|
||||
|
||||
|
||||
class PrinterOutputModel(QObject):
|
||||
bedTemperatureChanged = pyqtSignal()
|
||||
targetBedTemperatureChanged = pyqtSignal()
|
||||
isPreheatingChanged = pyqtSignal()
|
||||
stateChanged = pyqtSignal()
|
||||
activePrintJobChanged = pyqtSignal()
|
||||
nameChanged = pyqtSignal()
|
||||
headPositionChanged = pyqtSignal()
|
||||
keyChanged = pyqtSignal()
|
||||
typeChanged = pyqtSignal()
|
||||
buildplateChanged = pyqtSignal()
|
||||
cameraUrlChanged = pyqtSignal()
|
||||
configurationChanged = pyqtSignal()
|
||||
canUpdateFirmwareChanged = pyqtSignal()
|
||||
|
||||
def __init__(self, output_controller: "PrinterOutputController", number_of_extruders: int = 1, parent=None, firmware_version = "") -> None:
|
||||
super().__init__(parent)
|
||||
self._bed_temperature = -1 # type: float # Use -1 for no heated bed.
|
||||
self._target_bed_temperature = 0 # type: float
|
||||
self._name = ""
|
||||
self._key = "" # Unique identifier
|
||||
self._unique_name = "" # Unique name (used in Connect)
|
||||
self._controller = output_controller
|
||||
self._controller.canUpdateFirmwareChanged.connect(self._onControllerCanUpdateFirmwareChanged)
|
||||
self._extruders = [ExtruderOutputModel(printer = self, position = i) for i in range(number_of_extruders)]
|
||||
self._active_printer_configuration = PrinterConfigurationModel() # Indicates the current configuration setup in this printer
|
||||
self._head_position = Vector(0, 0, 0)
|
||||
self._active_print_job = None # type: Optional[PrintJobOutputModel]
|
||||
self._firmware_version = firmware_version
|
||||
self._printer_state = "unknown"
|
||||
self._is_preheating = False
|
||||
self._printer_type = ""
|
||||
self._buildplate = ""
|
||||
self._peripherals = [] # type: List[Peripheral]
|
||||
|
||||
self._active_printer_configuration.extruderConfigurations = [extruder.extruderConfiguration for extruder in
|
||||
self._extruders]
|
||||
self._active_printer_configuration.configurationChanged.connect(self.configurationChanged)
|
||||
self._available_printer_configurations = [] # type: List[PrinterConfigurationModel]
|
||||
|
||||
self._camera_url = QUrl() # type: QUrl
|
||||
|
||||
@pyqtProperty(str, constant = True)
|
||||
def firmwareVersion(self) -> str:
|
||||
return self._firmware_version
|
||||
|
||||
def setCameraUrl(self, camera_url: "QUrl") -> None:
|
||||
if self._camera_url != camera_url:
|
||||
self._camera_url = camera_url
|
||||
self.cameraUrlChanged.emit()
|
||||
|
||||
@pyqtProperty(QUrl, fset = setCameraUrl, notify = cameraUrlChanged)
|
||||
def cameraUrl(self) -> "QUrl":
|
||||
return self._camera_url
|
||||
|
||||
def updateIsPreheating(self, pre_heating: bool) -> None:
|
||||
if self._is_preheating != pre_heating:
|
||||
self._is_preheating = pre_heating
|
||||
self.isPreheatingChanged.emit()
|
||||
|
||||
@pyqtProperty(bool, notify=isPreheatingChanged)
|
||||
def isPreheating(self) -> bool:
|
||||
return self._is_preheating
|
||||
|
||||
@pyqtProperty(str, notify = typeChanged)
|
||||
def type(self) -> str:
|
||||
return self._printer_type
|
||||
|
||||
def updateType(self, printer_type: str) -> None:
|
||||
if self._printer_type != printer_type:
|
||||
self._printer_type = printer_type
|
||||
self._active_printer_configuration.printerType = self._printer_type
|
||||
self.typeChanged.emit()
|
||||
self.configurationChanged.emit()
|
||||
|
||||
@pyqtProperty(str, notify = buildplateChanged)
|
||||
def buildplate(self) -> str:
|
||||
return self._buildplate
|
||||
|
||||
def updateBuildplate(self, buildplate: str) -> None:
|
||||
if self._buildplate != buildplate:
|
||||
self._buildplate = buildplate
|
||||
self._active_printer_configuration.buildplateConfiguration = self._buildplate
|
||||
self.buildplateChanged.emit()
|
||||
self.configurationChanged.emit()
|
||||
|
||||
@pyqtProperty(str, notify=keyChanged)
|
||||
def key(self) -> str:
|
||||
return self._key
|
||||
|
||||
def updateKey(self, key: str) -> None:
|
||||
if self._key != key:
|
||||
self._key = key
|
||||
self.keyChanged.emit()
|
||||
|
||||
@pyqtSlot()
|
||||
def homeHead(self) -> None:
|
||||
self._controller.homeHead(self)
|
||||
|
||||
@pyqtSlot()
|
||||
def homeBed(self) -> None:
|
||||
self._controller.homeBed(self)
|
||||
|
||||
@pyqtSlot(str)
|
||||
def sendRawCommand(self, command: str) -> None:
|
||||
self._controller.sendRawCommand(self, command)
|
||||
|
||||
@pyqtProperty("QVariantList", constant = True)
|
||||
def extruders(self) -> List["ExtruderOutputModel"]:
|
||||
return self._extruders
|
||||
|
||||
@pyqtProperty(QVariant, notify = headPositionChanged)
|
||||
def headPosition(self) -> Dict[str, float]:
|
||||
return {"x": self._head_position.x, "y": self._head_position.y, "z": self.head_position.z}
|
||||
|
||||
def updateHeadPosition(self, x: float, y: float, z: float) -> None:
|
||||
if self._head_position.x != x or self._head_position.y != y or self._head_position.z != z:
|
||||
self._head_position = Vector(x, y, z)
|
||||
self.headPositionChanged.emit()
|
||||
|
||||
@pyqtProperty(float, float, float)
|
||||
@pyqtProperty(float, float, float, float)
|
||||
def setHeadPosition(self, x: float, y: float, z: float, speed: float = 3000) -> None:
|
||||
self.updateHeadPosition(x, y, z)
|
||||
self._controller.setHeadPosition(self, x, y, z, speed)
|
||||
|
||||
@pyqtProperty(float)
|
||||
@pyqtProperty(float, float)
|
||||
def setHeadX(self, x: float, speed: float = 3000) -> None:
|
||||
self.updateHeadPosition(x, self._head_position.y, self._head_position.z)
|
||||
self._controller.setHeadPosition(self, x, self._head_position.y, self._head_position.z, speed)
|
||||
|
||||
@pyqtProperty(float)
|
||||
@pyqtProperty(float, float)
|
||||
def setHeadY(self, y: float, speed: float = 3000) -> None:
|
||||
self.updateHeadPosition(self._head_position.x, y, self._head_position.z)
|
||||
self._controller.setHeadPosition(self, self._head_position.x, y, self._head_position.z, speed)
|
||||
|
||||
@pyqtProperty(float)
|
||||
@pyqtProperty(float, float)
|
||||
def setHeadZ(self, z: float, speed:float = 3000) -> None:
|
||||
self.updateHeadPosition(self._head_position.x, self._head_position.y, z)
|
||||
self._controller.setHeadPosition(self, self._head_position.x, self._head_position.y, z, speed)
|
||||
|
||||
@pyqtSlot(float, float, float)
|
||||
@pyqtSlot(float, float, float, float)
|
||||
def moveHead(self, x: float = 0, y: float = 0, z: float = 0, speed: float = 3000) -> None:
|
||||
self._controller.moveHead(self, x, y, z, speed)
|
||||
|
||||
## Pre-heats the heated bed of the printer.
|
||||
#
|
||||
# \param temperature The temperature to heat the bed to, in degrees
|
||||
# Celsius.
|
||||
# \param duration How long the bed should stay warm, in seconds.
|
||||
@pyqtSlot(float, float)
|
||||
def preheatBed(self, temperature: float, duration: float) -> None:
|
||||
self._controller.preheatBed(self, temperature, duration)
|
||||
|
||||
@pyqtSlot()
|
||||
def cancelPreheatBed(self) -> None:
|
||||
self._controller.cancelPreheatBed(self)
|
||||
|
||||
def getController(self) -> "PrinterOutputController":
|
||||
return self._controller
|
||||
|
||||
@pyqtProperty(str, notify = nameChanged)
|
||||
def name(self) -> str:
|
||||
return self._name
|
||||
|
||||
def setName(self, name: str) -> None:
|
||||
self.updateName(name)
|
||||
|
||||
def updateName(self, name: str) -> None:
|
||||
if self._name != name:
|
||||
self._name = name
|
||||
self.nameChanged.emit()
|
||||
|
||||
@pyqtProperty(str, notify = nameChanged)
|
||||
def uniqueName(self) -> str:
|
||||
return self._unique_name
|
||||
|
||||
def updateUniqueName(self, unique_name: str) -> None:
|
||||
if self._unique_name != unique_name:
|
||||
self._unique_name = unique_name
|
||||
self.nameChanged.emit()
|
||||
|
||||
## Update the bed temperature. This only changes it locally.
|
||||
def updateBedTemperature(self, temperature: float) -> None:
|
||||
if self._bed_temperature != temperature:
|
||||
self._bed_temperature = temperature
|
||||
self.bedTemperatureChanged.emit()
|
||||
|
||||
def updateTargetBedTemperature(self, temperature: float) -> None:
|
||||
if self._target_bed_temperature != temperature:
|
||||
self._target_bed_temperature = temperature
|
||||
self.targetBedTemperatureChanged.emit()
|
||||
|
||||
## Set the target bed temperature. This ensures that it's actually sent to the remote.
|
||||
@pyqtSlot(float)
|
||||
def setTargetBedTemperature(self, temperature: float) -> None:
|
||||
self._controller.setTargetBedTemperature(self, temperature)
|
||||
self.updateTargetBedTemperature(temperature)
|
||||
|
||||
def updateActivePrintJob(self, print_job: Optional["PrintJobOutputModel"]) -> None:
|
||||
if self._active_print_job != print_job:
|
||||
old_print_job = self._active_print_job
|
||||
|
||||
if print_job is not None:
|
||||
print_job.updateAssignedPrinter(self)
|
||||
self._active_print_job = print_job
|
||||
|
||||
if old_print_job is not None:
|
||||
old_print_job.updateAssignedPrinter(None)
|
||||
self.activePrintJobChanged.emit()
|
||||
|
||||
def updateState(self, printer_state: str) -> None:
|
||||
if self._printer_state != printer_state:
|
||||
self._printer_state = printer_state
|
||||
self.stateChanged.emit()
|
||||
|
||||
@pyqtProperty(QObject, notify = activePrintJobChanged)
|
||||
def activePrintJob(self) -> Optional["PrintJobOutputModel"]:
|
||||
return self._active_print_job
|
||||
|
||||
@pyqtProperty(str, notify = stateChanged)
|
||||
def state(self) -> str:
|
||||
return self._printer_state
|
||||
|
||||
@pyqtProperty(float, notify = bedTemperatureChanged)
|
||||
def bedTemperature(self) -> float:
|
||||
return self._bed_temperature
|
||||
|
||||
@pyqtProperty(float, notify = targetBedTemperatureChanged)
|
||||
def targetBedTemperature(self) -> float:
|
||||
return self._target_bed_temperature
|
||||
|
||||
# Does the printer support pre-heating the bed at all
|
||||
@pyqtProperty(bool, constant = True)
|
||||
def canPreHeatBed(self) -> bool:
|
||||
if self._controller:
|
||||
return self._controller.can_pre_heat_bed
|
||||
return False
|
||||
|
||||
# Does the printer support pre-heating the bed at all
|
||||
@pyqtProperty(bool, constant = True)
|
||||
def canPreHeatHotends(self) -> bool:
|
||||
if self._controller:
|
||||
return self._controller.can_pre_heat_hotends
|
||||
return False
|
||||
|
||||
# Does the printer support sending raw G-code at all
|
||||
@pyqtProperty(bool, constant = True)
|
||||
def canSendRawGcode(self) -> bool:
|
||||
if self._controller:
|
||||
return self._controller.can_send_raw_gcode
|
||||
return False
|
||||
|
||||
# Does the printer support pause at all
|
||||
@pyqtProperty(bool, constant = True)
|
||||
def canPause(self) -> bool:
|
||||
if self._controller:
|
||||
return self._controller.can_pause
|
||||
return False
|
||||
|
||||
# Does the printer support abort at all
|
||||
@pyqtProperty(bool, constant = True)
|
||||
def canAbort(self) -> bool:
|
||||
if self._controller:
|
||||
return self._controller.can_abort
|
||||
return False
|
||||
|
||||
# Does the printer support manual control at all
|
||||
@pyqtProperty(bool, constant = True)
|
||||
def canControlManually(self) -> bool:
|
||||
if self._controller:
|
||||
return self._controller.can_control_manually
|
||||
return False
|
||||
|
||||
# Does the printer support upgrading firmware
|
||||
@pyqtProperty(bool, notify = canUpdateFirmwareChanged)
|
||||
def canUpdateFirmware(self) -> bool:
|
||||
if self._controller:
|
||||
return self._controller.can_update_firmware
|
||||
return False
|
||||
|
||||
# Stub to connect UM.Signal to pyqtSignal
|
||||
def _onControllerCanUpdateFirmwareChanged(self) -> None:
|
||||
self.canUpdateFirmwareChanged.emit()
|
||||
|
||||
# Returns the active configuration (material, variant and buildplate) of the current printer
|
||||
@pyqtProperty(QObject, notify = configurationChanged)
|
||||
def printerConfiguration(self) -> Optional[PrinterConfigurationModel]:
|
||||
if self._active_printer_configuration.isValid():
|
||||
return self._active_printer_configuration
|
||||
return None
|
||||
|
||||
peripheralsChanged = pyqtSignal()
|
||||
|
||||
@pyqtProperty(str, notify = peripheralsChanged)
|
||||
def peripherals(self) -> str:
|
||||
return ", ".join([peripheral.name for peripheral in self._peripherals])
|
||||
|
||||
def addPeripheral(self, peripheral: Peripheral) -> None:
|
||||
self._peripherals.append(peripheral)
|
||||
self.peripheralsChanged.emit()
|
||||
|
||||
def removePeripheral(self, peripheral: Peripheral) -> None:
|
||||
self._peripherals.remove(peripheral)
|
||||
self.peripheralsChanged.emit()
|
||||
|
||||
availableConfigurationsChanged = pyqtSignal()
|
||||
|
||||
# The availableConfigurations are configuration options that a printer can switch to, but doesn't currently have
|
||||
# active (eg; Automatic tool changes, material loaders, etc).
|
||||
@pyqtProperty("QVariantList", notify = availableConfigurationsChanged)
|
||||
def availableConfigurations(self) -> List[PrinterConfigurationModel]:
|
||||
return self._available_printer_configurations
|
||||
|
||||
def addAvailableConfiguration(self, new_configuration: PrinterConfigurationModel) -> None:
|
||||
if new_configuration not in self._available_printer_configurations:
|
||||
self._available_printer_configurations.append(new_configuration)
|
||||
self.availableConfigurationsChanged.emit()
|
||||
|
||||
def removeAvailableConfiguration(self, config_to_remove: PrinterConfigurationModel) -> None:
|
||||
try:
|
||||
self._available_printer_configurations.remove(config_to_remove)
|
||||
except ValueError:
|
||||
Logger.log("w", "Unable to remove configuration that isn't in the list of available configurations")
|
||||
else:
|
||||
self.availableConfigurationsChanged.emit()
|
||||
|
||||
def setAvailableConfigurations(self, new_configurations: List[PrinterConfigurationModel]) -> None:
|
||||
self._available_printer_configurations = new_configurations
|
||||
self.availableConfigurationsChanged.emit()
|
0
cura/PrinterOutput/Models/__init__.py
Normal file
0
cura/PrinterOutput/Models/__init__.py
Normal file
|
@ -7,7 +7,7 @@ from UM.Scene.SceneNode import SceneNode #For typing.
|
|||
from cura.API import Account
|
||||
from cura.CuraApplication import CuraApplication
|
||||
|
||||
from cura.PrinterOutputDevice import PrinterOutputDevice, ConnectionState, ConnectionType
|
||||
from cura.PrinterOutput.PrinterOutputDevice import PrinterOutputDevice, ConnectionState, ConnectionType
|
||||
|
||||
from PyQt5.QtNetwork import QHttpMultiPart, QHttpPart, QNetworkRequest, QNetworkAccessManager, QNetworkReply, QAuthenticator
|
||||
from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject, QUrl, QCoreApplication
|
||||
|
@ -18,6 +18,8 @@ from enum import IntEnum
|
|||
import os # To get the username
|
||||
import gzip
|
||||
|
||||
from cura.Settings.CuraContainerRegistry import CuraContainerRegistry
|
||||
|
||||
|
||||
class AuthState(IntEnum):
|
||||
NotAuthenticated = 1
|
||||
|
@ -33,8 +35,6 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice):
|
|||
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, connection_type = connection_type, parent = parent)
|
||||
self._manager = None # type: Optional[QNetworkAccessManager]
|
||||
self._last_manager_create_time = None # type: Optional[float]
|
||||
self._recreate_network_manager_time = 30
|
||||
self._timeout_time = 10 # After how many seconds of no response should a timeout occur?
|
||||
|
||||
self._last_response_time = None # type: Optional[float]
|
||||
|
@ -58,8 +58,8 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice):
|
|||
self._gcode = [] # type: List[str]
|
||||
self._connection_state_before_timeout = None # type: Optional[ConnectionState]
|
||||
|
||||
def requestWrite(self, nodes: List[SceneNode], file_name: Optional[str] = None, limit_mimetypes: bool = False,
|
||||
file_handler: Optional[FileHandler] = None, **kwargs: str) -> None:
|
||||
def requestWrite(self, nodes: List["SceneNode"], file_name: Optional[str] = None, limit_mimetypes: bool = False,
|
||||
file_handler: Optional["FileHandler"] = None, filter_by_machine: bool = False, **kwargs) -> None:
|
||||
raise NotImplementedError("requestWrite needs to be implemented")
|
||||
|
||||
def setAuthenticationState(self, authentication_state: AuthState) -> None:
|
||||
|
@ -131,12 +131,6 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice):
|
|||
|
||||
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
|
||||
# sleep.
|
||||
if time_since_last_response > 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()
|
||||
assert(self._manager is not None)
|
||||
elif self._connection_state == ConnectionState.Closed:
|
||||
# Go out of timeout.
|
||||
if self._connection_state_before_timeout is not None: # sanity check, but it should never be None here
|
||||
|
@ -160,7 +154,7 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice):
|
|||
part = QHttpPart()
|
||||
|
||||
if not content_header.startswith("form-data;"):
|
||||
content_header = "form_data; " + content_header
|
||||
content_header = "form-data; " + content_header
|
||||
part.setHeader(QNetworkRequest.ContentDispositionHeader, content_header)
|
||||
|
||||
if content_type is not None:
|
||||
|
@ -310,22 +304,36 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice):
|
|||
def _createNetworkManager(self) -> None:
|
||||
Logger.log("d", "Creating network manager")
|
||||
if self._manager:
|
||||
self._manager.finished.disconnect(self.__handleOnFinished)
|
||||
self._manager.finished.disconnect(self._handleOnFinished)
|
||||
self._manager.authenticationRequired.disconnect(self._onAuthenticationRequired)
|
||||
|
||||
self._manager = QNetworkAccessManager()
|
||||
self._manager.finished.connect(self.__handleOnFinished)
|
||||
self._last_manager_create_time = time()
|
||||
self._manager.finished.connect(self._handleOnFinished)
|
||||
self._manager.authenticationRequired.connect(self._onAuthenticationRequired)
|
||||
|
||||
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:
|
||||
if on_finished is not None:
|
||||
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.
|
||||
# As we don't want to keep them around forever, delete them if we get a reply.
|
||||
if reply.operation() == QNetworkAccessManager.PostOperation:
|
||||
|
|
16
cura/PrinterOutput/Peripheral.py
Normal file
16
cura/PrinterOutput/Peripheral.py
Normal file
|
@ -0,0 +1,16 @@
|
|||
# Copyright (c) 2019 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
|
||||
## Data class that represents a peripheral for a printer.
|
||||
#
|
||||
# Output device plug-ins may specify that the printer has a certain set of
|
||||
# peripherals. This set is then possibly shown in the interface of the monitor
|
||||
# stage.
|
||||
class Peripheral:
|
||||
## Constructs the peripheral.
|
||||
# \param type A unique ID for the type of peripheral.
|
||||
# \param name A human-readable name for the peripheral.
|
||||
def __init__(self, peripheral_type: str, name: str) -> None:
|
||||
self.type = peripheral_type
|
||||
self.name = name
|
|
@ -1,172 +1,4 @@
|
|||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
# 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 PyQt5.QtCore import QUrl
|
||||
from PyQt5.QtGui import QImage
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from cura.PrinterOutput.PrinterOutputController import PrinterOutputController
|
||||
from cura.PrinterOutput.PrinterOutputModel import PrinterOutputModel
|
||||
from cura.PrinterOutput.ConfigurationModel import ConfigurationModel
|
||||
|
||||
|
||||
class PrintJobOutputModel(QObject):
|
||||
stateChanged = pyqtSignal()
|
||||
timeTotalChanged = pyqtSignal()
|
||||
timeElapsedChanged = pyqtSignal()
|
||||
nameChanged = pyqtSignal()
|
||||
keyChanged = pyqtSignal()
|
||||
assignedPrinterChanged = pyqtSignal()
|
||||
ownerChanged = pyqtSignal()
|
||||
configurationChanged = pyqtSignal()
|
||||
previewImageChanged = pyqtSignal()
|
||||
compatibleMachineFamiliesChanged = pyqtSignal()
|
||||
|
||||
def __init__(self, output_controller: "PrinterOutputController", key: str = "", name: str = "", parent=None) -> None:
|
||||
super().__init__(parent)
|
||||
self._output_controller = output_controller
|
||||
self._state = ""
|
||||
self._time_total = 0
|
||||
self._time_elapsed = 0
|
||||
self._name = name # Human readable name
|
||||
self._key = key # Unique identifier
|
||||
self._assigned_printer = None # type: Optional[PrinterOutputModel]
|
||||
self._owner = "" # Who started/owns the print job?
|
||||
|
||||
self._configuration = None # type: Optional[ConfigurationModel]
|
||||
self._compatible_machine_families = [] # type: List[str]
|
||||
self._preview_image_id = 0
|
||||
|
||||
self._preview_image = None # type: Optional[QImage]
|
||||
|
||||
@pyqtProperty("QStringList", notify=compatibleMachineFamiliesChanged)
|
||||
def compatibleMachineFamilies(self):
|
||||
# Hack; Some versions of cluster will return a family more than once...
|
||||
return list(set(self._compatible_machine_families))
|
||||
|
||||
def setCompatibleMachineFamilies(self, compatible_machine_families: List[str]) -> None:
|
||||
if self._compatible_machine_families != compatible_machine_families:
|
||||
self._compatible_machine_families = compatible_machine_families
|
||||
self.compatibleMachineFamiliesChanged.emit()
|
||||
|
||||
@pyqtProperty(QUrl, notify=previewImageChanged)
|
||||
def previewImageUrl(self):
|
||||
self._preview_image_id += 1
|
||||
# There is an image provider that is called "print_job_preview". In order to ensure that the image qml object, that
|
||||
# requires a QUrl to function, updates correctly we add an increasing number. This causes to see the QUrl
|
||||
# as new (instead of relying on cached version and thus forces an update.
|
||||
temp = "image://print_job_preview/" + str(self._preview_image_id) + "/" + self._key
|
||||
return QUrl(temp, QUrl.TolerantMode)
|
||||
|
||||
def getPreviewImage(self) -> Optional[QImage]:
|
||||
return self._preview_image
|
||||
|
||||
def updatePreviewImage(self, preview_image: Optional[QImage]) -> None:
|
||||
if self._preview_image != preview_image:
|
||||
self._preview_image = preview_image
|
||||
self.previewImageChanged.emit()
|
||||
|
||||
@pyqtProperty(QObject, notify=configurationChanged)
|
||||
def configuration(self) -> Optional["ConfigurationModel"]:
|
||||
return self._configuration
|
||||
|
||||
def updateConfiguration(self, configuration: Optional["ConfigurationModel"]) -> None:
|
||||
if self._configuration != configuration:
|
||||
self._configuration = configuration
|
||||
self.configurationChanged.emit()
|
||||
|
||||
@pyqtProperty(str, notify=ownerChanged)
|
||||
def owner(self):
|
||||
return self._owner
|
||||
|
||||
def updateOwner(self, owner):
|
||||
if self._owner != owner:
|
||||
self._owner = owner
|
||||
self.ownerChanged.emit()
|
||||
|
||||
@pyqtProperty(QObject, notify=assignedPrinterChanged)
|
||||
def assignedPrinter(self):
|
||||
return self._assigned_printer
|
||||
|
||||
def updateAssignedPrinter(self, assigned_printer: Optional["PrinterOutputModel"]) -> None:
|
||||
if self._assigned_printer != assigned_printer:
|
||||
old_printer = self._assigned_printer
|
||||
self._assigned_printer = assigned_printer
|
||||
if old_printer is not None:
|
||||
# If the previously assigned printer is set, this job is moved away from it.
|
||||
old_printer.updateActivePrintJob(None)
|
||||
self.assignedPrinterChanged.emit()
|
||||
|
||||
@pyqtProperty(str, notify=keyChanged)
|
||||
def key(self):
|
||||
return self._key
|
||||
|
||||
def updateKey(self, key: str):
|
||||
if self._key != key:
|
||||
self._key = key
|
||||
self.keyChanged.emit()
|
||||
|
||||
@pyqtProperty(str, notify = nameChanged)
|
||||
def name(self):
|
||||
return self._name
|
||||
|
||||
def updateName(self, name: str):
|
||||
if self._name != name:
|
||||
self._name = name
|
||||
self.nameChanged.emit()
|
||||
|
||||
@pyqtProperty(int, notify = timeTotalChanged)
|
||||
def timeTotal(self) -> int:
|
||||
return self._time_total
|
||||
|
||||
@pyqtProperty(int, notify = timeElapsedChanged)
|
||||
def timeElapsed(self) -> int:
|
||||
return self._time_elapsed
|
||||
|
||||
@pyqtProperty(int, notify = timeElapsedChanged)
|
||||
def timeRemaining(self) -> int:
|
||||
# Never get a negative time remaining
|
||||
return max(self.timeTotal - self.timeElapsed, 0)
|
||||
|
||||
@pyqtProperty(float, notify = timeElapsedChanged)
|
||||
def progress(self) -> float:
|
||||
result = float(self.timeElapsed) / max(self.timeTotal, 1.0) # Prevent a division by zero exception.
|
||||
return min(result, 1.0) # Never get a progress past 1.0
|
||||
|
||||
@pyqtProperty(str, notify=stateChanged)
|
||||
def state(self) -> str:
|
||||
return self._state
|
||||
|
||||
@pyqtProperty(bool, notify=stateChanged)
|
||||
def isActive(self) -> bool:
|
||||
inactiveStates = [
|
||||
"pausing",
|
||||
"paused",
|
||||
"resuming",
|
||||
"wait_cleanup"
|
||||
]
|
||||
if self.state in inactiveStates and self.timeRemaining > 0:
|
||||
return False
|
||||
return True
|
||||
|
||||
def updateTimeTotal(self, new_time_total):
|
||||
if self._time_total != new_time_total:
|
||||
self._time_total = new_time_total
|
||||
self.timeTotalChanged.emit()
|
||||
|
||||
def updateTimeElapsed(self, new_time_elapsed):
|
||||
if self._time_elapsed != new_time_elapsed:
|
||||
self._time_elapsed = new_time_elapsed
|
||||
self.timeElapsedChanged.emit()
|
||||
|
||||
def updateState(self, new_state):
|
||||
if self._state != new_state:
|
||||
self._state = new_state
|
||||
self.stateChanged.emit()
|
||||
|
||||
@pyqtSlot(str)
|
||||
def setState(self, state):
|
||||
self._output_controller.setJobState(self, state)
|
||||
import warnings
|
||||
warnings.warn("Importing cura.PrinterOutput.PrintJobOutputModel has been deprecated since 4.1, use cura.PrinterOutput.Models.PrintJobOutputModel instead", DeprecationWarning, stacklevel=2)
|
||||
# We moved the the models to one submodule deeper
|
||||
from cura.PrinterOutput.Models.PrintJobOutputModel import PrintJobOutputModel
|
|
@ -4,14 +4,12 @@
|
|||
from UM.Logger import Logger
|
||||
from UM.Signal import Signal
|
||||
|
||||
from typing import Union
|
||||
|
||||
MYPY = False
|
||||
if MYPY:
|
||||
from cura.PrinterOutput.PrintJobOutputModel import PrintJobOutputModel
|
||||
from cura.PrinterOutput.ExtruderOutputModel import ExtruderOutputModel
|
||||
from cura.PrinterOutput.PrinterOutputModel import PrinterOutputModel
|
||||
from cura.PrinterOutput.PrinterOutputDevice import PrinterOutputDevice
|
||||
from .Models.PrintJobOutputModel import PrintJobOutputModel
|
||||
from .Models.ExtruderOutputModel import ExtruderOutputModel
|
||||
from .Models.PrinterOutputModel import PrinterOutputModel
|
||||
from .PrinterOutputDevice import PrinterOutputDevice
|
||||
|
||||
|
||||
class PrinterOutputController:
|
||||
|
|
264
cura/PrinterOutput/PrinterOutputDevice.py
Normal file
264
cura/PrinterOutput/PrinterOutputDevice.py
Normal file
|
@ -0,0 +1,264 @@
|
|||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
from enum import IntEnum
|
||||
from typing import Callable, List, Optional, Union
|
||||
|
||||
from PyQt5.QtCore import pyqtProperty, pyqtSignal, QObject, QTimer, QUrl
|
||||
from PyQt5.QtWidgets import QMessageBox
|
||||
|
||||
from UM.Logger import Logger
|
||||
from UM.Signal import signalemitter
|
||||
from UM.Qt.QtApplication import QtApplication
|
||||
from UM.FlameProfiler import pyqtSlot
|
||||
from UM.i18n import i18nCatalog
|
||||
from UM.OutputDevice.OutputDevice import OutputDevice
|
||||
|
||||
MYPY = False
|
||||
if MYPY:
|
||||
from UM.FileHandler.FileHandler import FileHandler
|
||||
from UM.Scene.SceneNode import SceneNode
|
||||
from .Models.PrinterOutputModel import PrinterOutputModel
|
||||
from .Models.PrinterConfigurationModel import PrinterConfigurationModel
|
||||
from .FirmwareUpdater import FirmwareUpdater
|
||||
|
||||
i18n_catalog = i18nCatalog("cura")
|
||||
|
||||
|
||||
## The current processing state of the backend.
|
||||
class ConnectionState(IntEnum):
|
||||
Closed = 0
|
||||
Connecting = 1
|
||||
Connected = 2
|
||||
Busy = 3
|
||||
Error = 4
|
||||
|
||||
|
||||
class ConnectionType(IntEnum):
|
||||
NotConnected = 0
|
||||
UsbConnection = 1
|
||||
NetworkConnection = 2
|
||||
CloudConnection = 3
|
||||
|
||||
|
||||
## Printer output device adds extra interface options on top of output device.
|
||||
#
|
||||
# The assumption is made the printer is a FDM printer.
|
||||
#
|
||||
# Note that a number of settings are marked as "final". This is because decorators
|
||||
# are not inherited by children. To fix this we use the private counter part of those
|
||||
# functions to actually have the implementation.
|
||||
#
|
||||
# For all other uses it should be used in the same way as a "regular" OutputDevice.
|
||||
@signalemitter
|
||||
class PrinterOutputDevice(QObject, OutputDevice):
|
||||
|
||||
printersChanged = pyqtSignal()
|
||||
connectionStateChanged = pyqtSignal(str)
|
||||
acceptsCommandsChanged = pyqtSignal()
|
||||
|
||||
# Signal to indicate that the material of the active printer on the remote changed.
|
||||
materialIdChanged = pyqtSignal()
|
||||
|
||||
# # Signal to indicate that the hotend of the active printer on the remote changed.
|
||||
hotendIdChanged = pyqtSignal()
|
||||
|
||||
# Signal to indicate that the info text about the connection has changed.
|
||||
connectionTextChanged = pyqtSignal()
|
||||
|
||||
# Signal to indicate that the configuration of one of the printers has changed.
|
||||
uniqueConfigurationsChanged = pyqtSignal()
|
||||
|
||||
def __init__(self, device_id: str, connection_type: "ConnectionType" = ConnectionType.NotConnected, parent: QObject = None) -> None:
|
||||
super().__init__(device_id = device_id, parent = parent) # type: ignore # MyPy complains with the multiple inheritance
|
||||
|
||||
self._printers = [] # type: List[PrinterOutputModel]
|
||||
self._unique_configurations = [] # type: List[PrinterConfigurationModel]
|
||||
|
||||
self._monitor_view_qml_path = "" # type: str
|
||||
self._monitor_component = None # type: Optional[QObject]
|
||||
self._monitor_item = None # type: Optional[QObject]
|
||||
|
||||
self._control_view_qml_path = "" # type: str
|
||||
self._control_component = None # type: Optional[QObject]
|
||||
self._control_item = None # type: Optional[QObject]
|
||||
|
||||
self._accepts_commands = False # type: bool
|
||||
|
||||
self._update_timer = QTimer() # type: QTimer
|
||||
self._update_timer.setInterval(2000) # TODO; Add preference for update interval
|
||||
self._update_timer.setSingleShot(False)
|
||||
self._update_timer.timeout.connect(self._update)
|
||||
|
||||
self._connection_state = ConnectionState.Closed # type: ConnectionState
|
||||
self._connection_type = connection_type # type: ConnectionType
|
||||
|
||||
self._firmware_updater = None # type: Optional[FirmwareUpdater]
|
||||
self._firmware_name = None # type: Optional[str]
|
||||
self._address = "" # type: str
|
||||
self._connection_text = "" # type: str
|
||||
self.printersChanged.connect(self._onPrintersChanged)
|
||||
QtApplication.getInstance().getOutputDeviceManager().outputDevicesChanged.connect(self._updateUniqueConfigurations)
|
||||
|
||||
@pyqtProperty(str, notify = connectionTextChanged)
|
||||
def address(self) -> str:
|
||||
return self._address
|
||||
|
||||
def setConnectionText(self, connection_text):
|
||||
if self._connection_text != connection_text:
|
||||
self._connection_text = connection_text
|
||||
self.connectionTextChanged.emit()
|
||||
|
||||
@pyqtProperty(str, constant=True)
|
||||
def connectionText(self) -> str:
|
||||
return self._connection_text
|
||||
|
||||
def materialHotendChangedMessage(self, callback: Callable[[int], None]) -> None:
|
||||
Logger.log("w", "materialHotendChangedMessage needs to be implemented, returning 'Yes'")
|
||||
callback(QMessageBox.Yes)
|
||||
|
||||
def isConnected(self) -> bool:
|
||||
return self._connection_state != ConnectionState.Closed and self._connection_state != ConnectionState.Error
|
||||
|
||||
def setConnectionState(self, connection_state: "ConnectionState") -> None:
|
||||
if self._connection_state != connection_state:
|
||||
self._connection_state = connection_state
|
||||
self.connectionStateChanged.emit(self._id)
|
||||
|
||||
@pyqtProperty(int, constant = True)
|
||||
def connectionType(self) -> "ConnectionType":
|
||||
return self._connection_type
|
||||
|
||||
@pyqtProperty(int, notify = connectionStateChanged)
|
||||
def connectionState(self) -> "ConnectionState":
|
||||
return self._connection_state
|
||||
|
||||
def _update(self) -> None:
|
||||
pass
|
||||
|
||||
def _getPrinterByKey(self, key: str) -> Optional["PrinterOutputModel"]:
|
||||
for printer in self._printers:
|
||||
if printer.key == key:
|
||||
return printer
|
||||
|
||||
return None
|
||||
|
||||
def requestWrite(self, nodes: List["SceneNode"], file_name: Optional[str] = None, limit_mimetypes: bool = False,
|
||||
file_handler: Optional["FileHandler"] = None, filter_by_machine: bool = False, **kwargs) -> None:
|
||||
raise NotImplementedError("requestWrite needs to be implemented")
|
||||
|
||||
@pyqtProperty(QObject, notify = printersChanged)
|
||||
def activePrinter(self) -> Optional["PrinterOutputModel"]:
|
||||
if len(self._printers):
|
||||
return self._printers[0]
|
||||
return None
|
||||
|
||||
@pyqtProperty("QVariantList", notify = printersChanged)
|
||||
def printers(self) -> List["PrinterOutputModel"]:
|
||||
return self._printers
|
||||
|
||||
@pyqtProperty(QObject, constant = True)
|
||||
def monitorItem(self) -> QObject:
|
||||
# Note that we specifically only check if the monitor component is created.
|
||||
# It could be that it failed to actually create the qml item! If we check if the item was created, it will try to
|
||||
# create the item (and fail) every time.
|
||||
if not self._monitor_component:
|
||||
self._createMonitorViewFromQML()
|
||||
return self._monitor_item
|
||||
|
||||
@pyqtProperty(QObject, constant = True)
|
||||
def controlItem(self) -> QObject:
|
||||
if not self._control_component:
|
||||
self._createControlViewFromQML()
|
||||
return self._control_item
|
||||
|
||||
def _createControlViewFromQML(self) -> None:
|
||||
if not self._control_view_qml_path:
|
||||
return
|
||||
if self._control_item is None:
|
||||
self._control_item = QtApplication.getInstance().createQmlComponent(self._control_view_qml_path, {"OutputDevice": self})
|
||||
|
||||
def _createMonitorViewFromQML(self) -> None:
|
||||
if not self._monitor_view_qml_path:
|
||||
return
|
||||
|
||||
if self._monitor_item is None:
|
||||
self._monitor_item = QtApplication.getInstance().createQmlComponent(self._monitor_view_qml_path, {"OutputDevice": self})
|
||||
|
||||
## Attempt to establish connection
|
||||
def connect(self) -> None:
|
||||
self.setConnectionState(ConnectionState.Connecting)
|
||||
self._update_timer.start()
|
||||
|
||||
## Attempt to close the connection
|
||||
def close(self) -> None:
|
||||
self._update_timer.stop()
|
||||
self.setConnectionState(ConnectionState.Closed)
|
||||
|
||||
## Ensure that close gets called when object is destroyed
|
||||
def __del__(self) -> None:
|
||||
self.close()
|
||||
|
||||
@pyqtProperty(bool, notify = acceptsCommandsChanged)
|
||||
def acceptsCommands(self) -> bool:
|
||||
return self._accepts_commands
|
||||
|
||||
## Set a flag to signal the UI that the printer is not (yet) ready to receive commands
|
||||
def _setAcceptsCommands(self, accepts_commands: bool) -> None:
|
||||
if self._accepts_commands != accepts_commands:
|
||||
self._accepts_commands = accepts_commands
|
||||
|
||||
self.acceptsCommandsChanged.emit()
|
||||
|
||||
# Returns the unique configurations of the printers within this output device
|
||||
@pyqtProperty("QVariantList", notify = uniqueConfigurationsChanged)
|
||||
def uniqueConfigurations(self) -> List["PrinterConfigurationModel"]:
|
||||
return self._unique_configurations
|
||||
|
||||
def _updateUniqueConfigurations(self) -> None:
|
||||
all_configurations = set()
|
||||
for printer in self._printers:
|
||||
if printer.printerConfiguration is not None and printer.printerConfiguration.hasAnyMaterialLoaded():
|
||||
all_configurations.add(printer.printerConfiguration)
|
||||
all_configurations.update(printer.availableConfigurations)
|
||||
if None in all_configurations: # Shouldn't happen, but it does. I don't see how it could ever happen. Skip adding that configuration. List could end up empty!
|
||||
Logger.log("e", "Found a broken configuration in the synced list!")
|
||||
all_configurations.remove(None)
|
||||
new_configurations = sorted(all_configurations, key = lambda config: config.printerType or "")
|
||||
if new_configurations != self._unique_configurations:
|
||||
self._unique_configurations = new_configurations
|
||||
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 or "" for configuration in self._unique_configurations])))
|
||||
|
||||
def _onPrintersChanged(self) -> None:
|
||||
for printer in self._printers:
|
||||
printer.configurationChanged.connect(self._updateUniqueConfigurations)
|
||||
printer.availableConfigurationsChanged.connect(self._updateUniqueConfigurations)
|
||||
|
||||
# At this point there may be non-updated configurations
|
||||
self._updateUniqueConfigurations()
|
||||
|
||||
## Set the device firmware name
|
||||
#
|
||||
# \param name The name of the firmware.
|
||||
def _setFirmwareName(self, name: str) -> None:
|
||||
self._firmware_name = name
|
||||
|
||||
## Get the name of device firmware
|
||||
#
|
||||
# This name can be used to define device type
|
||||
def getFirmwareName(self) -> Optional[str]:
|
||||
return self._firmware_name
|
||||
|
||||
def getFirmwareUpdater(self) -> Optional["FirmwareUpdater"]:
|
||||
return self._firmware_updater
|
||||
|
||||
@pyqtSlot(str)
|
||||
def updateFirmware(self, firmware_file: Union[str, QUrl]) -> None:
|
||||
if not self._firmware_updater:
|
||||
return
|
||||
|
||||
self._firmware_updater.updateFirmware(firmware_file)
|
|
@ -1,297 +1,4 @@
|
|||
# Copyright (c) 2019 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
from PyQt5.QtCore import pyqtSignal, pyqtProperty, QObject, QVariant, pyqtSlot, QUrl
|
||||
from typing import List, Dict, Optional
|
||||
from UM.Math.Vector import Vector
|
||||
from cura.PrinterOutput.ConfigurationModel import ConfigurationModel
|
||||
from cura.PrinterOutput.ExtruderOutputModel import ExtruderOutputModel
|
||||
|
||||
MYPY = False
|
||||
if MYPY:
|
||||
from cura.PrinterOutput.PrintJobOutputModel import PrintJobOutputModel
|
||||
from cura.PrinterOutput.PrinterOutputController import PrinterOutputController
|
||||
|
||||
|
||||
class PrinterOutputModel(QObject):
|
||||
bedTemperatureChanged = pyqtSignal()
|
||||
targetBedTemperatureChanged = pyqtSignal()
|
||||
isPreheatingChanged = pyqtSignal()
|
||||
stateChanged = pyqtSignal()
|
||||
activePrintJobChanged = pyqtSignal()
|
||||
nameChanged = pyqtSignal()
|
||||
headPositionChanged = pyqtSignal()
|
||||
keyChanged = pyqtSignal()
|
||||
typeChanged = pyqtSignal()
|
||||
buildplateChanged = pyqtSignal()
|
||||
cameraUrlChanged = pyqtSignal()
|
||||
configurationChanged = pyqtSignal()
|
||||
canUpdateFirmwareChanged = pyqtSignal()
|
||||
|
||||
def __init__(self, output_controller: "PrinterOutputController", number_of_extruders: int = 1, parent=None, firmware_version = "") -> None:
|
||||
super().__init__(parent)
|
||||
self._bed_temperature = -1 # type: float # Use -1 for no heated bed.
|
||||
self._target_bed_temperature = 0 # type: float
|
||||
self._name = ""
|
||||
self._key = "" # Unique identifier
|
||||
self._controller = output_controller
|
||||
self._controller.canUpdateFirmwareChanged.connect(self._onControllerCanUpdateFirmwareChanged)
|
||||
self._extruders = [ExtruderOutputModel(printer = self, position = i) for i in range(number_of_extruders)]
|
||||
self._printer_configuration = ConfigurationModel() # Indicates the current configuration setup in this printer
|
||||
self._head_position = Vector(0, 0, 0)
|
||||
self._active_print_job = None # type: Optional[PrintJobOutputModel]
|
||||
self._firmware_version = firmware_version
|
||||
self._printer_state = "unknown"
|
||||
self._is_preheating = False
|
||||
self._printer_type = ""
|
||||
self._buildplate = ""
|
||||
|
||||
self._printer_configuration.extruderConfigurations = [extruder.extruderConfiguration for extruder in
|
||||
self._extruders]
|
||||
|
||||
self._camera_url = QUrl() # type: QUrl
|
||||
|
||||
@pyqtProperty(str, constant = True)
|
||||
def firmwareVersion(self) -> str:
|
||||
return self._firmware_version
|
||||
|
||||
def setCameraUrl(self, camera_url: "QUrl") -> None:
|
||||
if self._camera_url != camera_url:
|
||||
self._camera_url = camera_url
|
||||
self.cameraUrlChanged.emit()
|
||||
|
||||
@pyqtProperty(QUrl, fset = setCameraUrl, notify = cameraUrlChanged)
|
||||
def cameraUrl(self) -> "QUrl":
|
||||
return self._camera_url
|
||||
|
||||
def updateIsPreheating(self, pre_heating: bool) -> None:
|
||||
if self._is_preheating != pre_heating:
|
||||
self._is_preheating = pre_heating
|
||||
self.isPreheatingChanged.emit()
|
||||
|
||||
@pyqtProperty(bool, notify=isPreheatingChanged)
|
||||
def isPreheating(self) -> bool:
|
||||
return self._is_preheating
|
||||
|
||||
@pyqtProperty(str, notify = typeChanged)
|
||||
def type(self) -> str:
|
||||
return self._printer_type
|
||||
|
||||
def updateType(self, printer_type: str) -> None:
|
||||
if self._printer_type != printer_type:
|
||||
self._printer_type = printer_type
|
||||
self._printer_configuration.printerType = self._printer_type
|
||||
self.typeChanged.emit()
|
||||
self.configurationChanged.emit()
|
||||
|
||||
@pyqtProperty(str, notify = buildplateChanged)
|
||||
def buildplate(self) -> str:
|
||||
return self._buildplate
|
||||
|
||||
def updateBuildplate(self, buildplate: str) -> None:
|
||||
if self._buildplate != buildplate:
|
||||
self._buildplate = buildplate
|
||||
self._printer_configuration.buildplateConfiguration = self._buildplate
|
||||
self.buildplateChanged.emit()
|
||||
self.configurationChanged.emit()
|
||||
|
||||
@pyqtProperty(str, notify=keyChanged)
|
||||
def key(self) -> str:
|
||||
return self._key
|
||||
|
||||
def updateKey(self, key: str) -> None:
|
||||
if self._key != key:
|
||||
self._key = key
|
||||
self.keyChanged.emit()
|
||||
|
||||
@pyqtSlot()
|
||||
def homeHead(self) -> None:
|
||||
self._controller.homeHead(self)
|
||||
|
||||
@pyqtSlot()
|
||||
def homeBed(self) -> None:
|
||||
self._controller.homeBed(self)
|
||||
|
||||
@pyqtSlot(str)
|
||||
def sendRawCommand(self, command: str) -> None:
|
||||
self._controller.sendRawCommand(self, command)
|
||||
|
||||
@pyqtProperty("QVariantList", constant = True)
|
||||
def extruders(self) -> List["ExtruderOutputModel"]:
|
||||
return self._extruders
|
||||
|
||||
@pyqtProperty(QVariant, notify = headPositionChanged)
|
||||
def headPosition(self) -> Dict[str, float]:
|
||||
return {"x": self._head_position.x, "y": self._head_position.y, "z": self.head_position.z}
|
||||
|
||||
def updateHeadPosition(self, x: float, y: float, z: float) -> None:
|
||||
if self._head_position.x != x or self._head_position.y != y or self._head_position.z != z:
|
||||
self._head_position = Vector(x, y, z)
|
||||
self.headPositionChanged.emit()
|
||||
|
||||
@pyqtProperty(float, float, float)
|
||||
@pyqtProperty(float, float, float, float)
|
||||
def setHeadPosition(self, x: float, y: float, z: float, speed: float = 3000) -> None:
|
||||
self.updateHeadPosition(x, y, z)
|
||||
self._controller.setHeadPosition(self, x, y, z, speed)
|
||||
|
||||
@pyqtProperty(float)
|
||||
@pyqtProperty(float, float)
|
||||
def setHeadX(self, x: float, speed: float = 3000) -> None:
|
||||
self.updateHeadPosition(x, self._head_position.y, self._head_position.z)
|
||||
self._controller.setHeadPosition(self, x, self._head_position.y, self._head_position.z, speed)
|
||||
|
||||
@pyqtProperty(float)
|
||||
@pyqtProperty(float, float)
|
||||
def setHeadY(self, y: float, speed: float = 3000) -> None:
|
||||
self.updateHeadPosition(self._head_position.x, y, self._head_position.z)
|
||||
self._controller.setHeadPosition(self, self._head_position.x, y, self._head_position.z, speed)
|
||||
|
||||
@pyqtProperty(float)
|
||||
@pyqtProperty(float, float)
|
||||
def setHeadZ(self, z: float, speed:float = 3000) -> None:
|
||||
self.updateHeadPosition(self._head_position.x, self._head_position.y, z)
|
||||
self._controller.setHeadPosition(self, self._head_position.x, self._head_position.y, z, speed)
|
||||
|
||||
@pyqtSlot(float, float, float)
|
||||
@pyqtSlot(float, float, float, float)
|
||||
def moveHead(self, x: float = 0, y: float = 0, z: float = 0, speed: float = 3000) -> None:
|
||||
self._controller.moveHead(self, x, y, z, speed)
|
||||
|
||||
## Pre-heats the heated bed of the printer.
|
||||
#
|
||||
# \param temperature The temperature to heat the bed to, in degrees
|
||||
# Celsius.
|
||||
# \param duration How long the bed should stay warm, in seconds.
|
||||
@pyqtSlot(float, float)
|
||||
def preheatBed(self, temperature: float, duration: float) -> None:
|
||||
self._controller.preheatBed(self, temperature, duration)
|
||||
|
||||
@pyqtSlot()
|
||||
def cancelPreheatBed(self) -> None:
|
||||
self._controller.cancelPreheatBed(self)
|
||||
|
||||
def getController(self) -> "PrinterOutputController":
|
||||
return self._controller
|
||||
|
||||
@pyqtProperty(str, notify = nameChanged)
|
||||
def name(self) -> str:
|
||||
return self._name
|
||||
|
||||
def setName(self, name: str) -> None:
|
||||
self.updateName(name)
|
||||
|
||||
def updateName(self, name: str) -> None:
|
||||
if self._name != name:
|
||||
self._name = name
|
||||
self.nameChanged.emit()
|
||||
|
||||
## Update the bed temperature. This only changes it locally.
|
||||
def updateBedTemperature(self, temperature: float) -> None:
|
||||
if self._bed_temperature != temperature:
|
||||
self._bed_temperature = temperature
|
||||
self.bedTemperatureChanged.emit()
|
||||
|
||||
def updateTargetBedTemperature(self, temperature: float) -> None:
|
||||
if self._target_bed_temperature != temperature:
|
||||
self._target_bed_temperature = temperature
|
||||
self.targetBedTemperatureChanged.emit()
|
||||
|
||||
## Set the target bed temperature. This ensures that it's actually sent to the remote.
|
||||
@pyqtSlot(float)
|
||||
def setTargetBedTemperature(self, temperature: float) -> None:
|
||||
self._controller.setTargetBedTemperature(self, temperature)
|
||||
self.updateTargetBedTemperature(temperature)
|
||||
|
||||
def updateActivePrintJob(self, print_job: Optional["PrintJobOutputModel"]) -> None:
|
||||
if self._active_print_job != print_job:
|
||||
old_print_job = self._active_print_job
|
||||
|
||||
if print_job is not None:
|
||||
print_job.updateAssignedPrinter(self)
|
||||
self._active_print_job = print_job
|
||||
|
||||
if old_print_job is not None:
|
||||
old_print_job.updateAssignedPrinter(None)
|
||||
self.activePrintJobChanged.emit()
|
||||
|
||||
def updateState(self, printer_state: str) -> None:
|
||||
if self._printer_state != printer_state:
|
||||
self._printer_state = printer_state
|
||||
self.stateChanged.emit()
|
||||
|
||||
@pyqtProperty(QObject, notify = activePrintJobChanged)
|
||||
def activePrintJob(self) -> Optional["PrintJobOutputModel"]:
|
||||
return self._active_print_job
|
||||
|
||||
@pyqtProperty(str, notify = stateChanged)
|
||||
def state(self) -> str:
|
||||
return self._printer_state
|
||||
|
||||
@pyqtProperty(float, notify = bedTemperatureChanged)
|
||||
def bedTemperature(self) -> float:
|
||||
return self._bed_temperature
|
||||
|
||||
@pyqtProperty(float, notify = targetBedTemperatureChanged)
|
||||
def targetBedTemperature(self) -> float:
|
||||
return self._target_bed_temperature
|
||||
|
||||
# Does the printer support pre-heating the bed at all
|
||||
@pyqtProperty(bool, constant = True)
|
||||
def canPreHeatBed(self) -> bool:
|
||||
if self._controller:
|
||||
return self._controller.can_pre_heat_bed
|
||||
return False
|
||||
|
||||
# Does the printer support pre-heating the bed at all
|
||||
@pyqtProperty(bool, constant = True)
|
||||
def canPreHeatHotends(self) -> bool:
|
||||
if self._controller:
|
||||
return self._controller.can_pre_heat_hotends
|
||||
return False
|
||||
|
||||
# Does the printer support sending raw G-code at all
|
||||
@pyqtProperty(bool, constant = True)
|
||||
def canSendRawGcode(self) -> bool:
|
||||
if self._controller:
|
||||
return self._controller.can_send_raw_gcode
|
||||
return False
|
||||
|
||||
# Does the printer support pause at all
|
||||
@pyqtProperty(bool, constant = True)
|
||||
def canPause(self) -> bool:
|
||||
if self._controller:
|
||||
return self._controller.can_pause
|
||||
return False
|
||||
|
||||
# Does the printer support abort at all
|
||||
@pyqtProperty(bool, constant = True)
|
||||
def canAbort(self) -> bool:
|
||||
if self._controller:
|
||||
return self._controller.can_abort
|
||||
return False
|
||||
|
||||
# Does the printer support manual control at all
|
||||
@pyqtProperty(bool, constant = True)
|
||||
def canControlManually(self) -> bool:
|
||||
if self._controller:
|
||||
return self._controller.can_control_manually
|
||||
return False
|
||||
|
||||
# Does the printer support upgrading firmware
|
||||
@pyqtProperty(bool, notify = canUpdateFirmwareChanged)
|
||||
def canUpdateFirmware(self) -> bool:
|
||||
if self._controller:
|
||||
return self._controller.can_update_firmware
|
||||
return False
|
||||
|
||||
# Stub to connect UM.Signal to pyqtSignal
|
||||
def _onControllerCanUpdateFirmwareChanged(self) -> None:
|
||||
self.canUpdateFirmwareChanged.emit()
|
||||
|
||||
# Returns the configuration (material, variant and buildplate) of the current printer
|
||||
@pyqtProperty(QObject, notify = configurationChanged)
|
||||
def printerConfiguration(self) -> Optional[ConfigurationModel]:
|
||||
if self._printer_configuration.isValid():
|
||||
return self._printer_configuration
|
||||
return None
|
||||
import warnings
|
||||
warnings.warn("Importing cura.PrinterOutput.PrinterOutputModel has been deprecated since 4.1, use cura.PrinterOutput.Models.PrinterOutputModel instead", DeprecationWarning, stacklevel=2)
|
||||
# We moved the the models to one submodule deeper
|
||||
from cura.PrinterOutput.Models.PrinterOutputModel import PrinterOutputModel
|
|
@ -1,261 +1,4 @@
|
|||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
from enum import IntEnum
|
||||
from typing import Callable, List, Optional, Union
|
||||
|
||||
from 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.QtWidgets import QMessageBox
|
||||
|
||||
from UM.Logger import Logger
|
||||
from UM.Signal import signalemitter
|
||||
from UM.Qt.QtApplication import QtApplication
|
||||
from UM.FlameProfiler import pyqtSlot
|
||||
|
||||
MYPY = False
|
||||
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.Scene.SceneNode import SceneNode
|
||||
|
||||
i18n_catalog = i18nCatalog("cura")
|
||||
|
||||
|
||||
## The current processing state of the backend.
|
||||
class ConnectionState(IntEnum):
|
||||
Closed = 0
|
||||
Connecting = 1
|
||||
Connected = 2
|
||||
Busy = 3
|
||||
Error = 4
|
||||
|
||||
|
||||
class ConnectionType(IntEnum):
|
||||
NotConnected = 0
|
||||
UsbConnection = 1
|
||||
NetworkConnection = 2
|
||||
CloudConnection = 3
|
||||
|
||||
|
||||
## Printer output device adds extra interface options on top of output device.
|
||||
#
|
||||
# The assumption is made the printer is a FDM printer.
|
||||
#
|
||||
# Note that a number of settings are marked as "final". This is because decorators
|
||||
# are not inherited by children. To fix this we use the private counter part of those
|
||||
# functions to actually have the implementation.
|
||||
#
|
||||
# For all other uses it should be used in the same way as a "regular" OutputDevice.
|
||||
@signalemitter
|
||||
class PrinterOutputDevice(QObject, OutputDevice):
|
||||
|
||||
printersChanged = pyqtSignal()
|
||||
connectionStateChanged = pyqtSignal(str)
|
||||
acceptsCommandsChanged = pyqtSignal()
|
||||
|
||||
# Signal to indicate that the material of the active printer on the remote changed.
|
||||
materialIdChanged = pyqtSignal()
|
||||
|
||||
# # Signal to indicate that the hotend of the active printer on the remote changed.
|
||||
hotendIdChanged = pyqtSignal()
|
||||
|
||||
# Signal to indicate that the info text about the connection has changed.
|
||||
connectionTextChanged = pyqtSignal()
|
||||
|
||||
# Signal to indicate that the configuration of one of the printers has changed.
|
||||
uniqueConfigurationsChanged = pyqtSignal()
|
||||
|
||||
def __init__(self, device_id: str, connection_type: "ConnectionType" = ConnectionType.NotConnected, parent: QObject = None) -> None:
|
||||
super().__init__(device_id = device_id, parent = parent) # type: ignore # MyPy complains with the multiple inheritance
|
||||
|
||||
self._printers = [] # type: List[PrinterOutputModel]
|
||||
self._unique_configurations = [] # type: List[ConfigurationModel]
|
||||
|
||||
self._monitor_view_qml_path = "" # type: str
|
||||
self._monitor_component = None # type: Optional[QObject]
|
||||
self._monitor_item = None # type: Optional[QObject]
|
||||
|
||||
self._control_view_qml_path = "" # type: str
|
||||
self._control_component = None # type: Optional[QObject]
|
||||
self._control_item = None # type: Optional[QObject]
|
||||
|
||||
self._accepts_commands = False # type: bool
|
||||
|
||||
self._update_timer = QTimer() # type: QTimer
|
||||
self._update_timer.setInterval(2000) # TODO; Add preference for update interval
|
||||
self._update_timer.setSingleShot(False)
|
||||
self._update_timer.timeout.connect(self._update)
|
||||
|
||||
self._connection_state = ConnectionState.Closed # type: ConnectionState
|
||||
self._connection_type = connection_type # type: ConnectionType
|
||||
|
||||
self._firmware_updater = None # type: Optional[FirmwareUpdater]
|
||||
self._firmware_name = None # type: Optional[str]
|
||||
self._address = "" # type: str
|
||||
self._connection_text = "" # type: str
|
||||
self.printersChanged.connect(self._onPrintersChanged)
|
||||
QtApplication.getInstance().getOutputDeviceManager().outputDevicesChanged.connect(self._updateUniqueConfigurations)
|
||||
|
||||
@pyqtProperty(str, notify = connectionTextChanged)
|
||||
def address(self) -> str:
|
||||
return self._address
|
||||
|
||||
def setConnectionText(self, connection_text):
|
||||
if self._connection_text != connection_text:
|
||||
self._connection_text = connection_text
|
||||
self.connectionTextChanged.emit()
|
||||
|
||||
@pyqtProperty(str, constant=True)
|
||||
def connectionText(self) -> str:
|
||||
return self._connection_text
|
||||
|
||||
def materialHotendChangedMessage(self, callback: Callable[[int], None]) -> None:
|
||||
Logger.log("w", "materialHotendChangedMessage needs to be implemented, returning 'Yes'")
|
||||
callback(QMessageBox.Yes)
|
||||
|
||||
def isConnected(self) -> bool:
|
||||
return self._connection_state != ConnectionState.Closed and self._connection_state != ConnectionState.Error
|
||||
|
||||
def setConnectionState(self, connection_state: "ConnectionState") -> None:
|
||||
if self._connection_state != connection_state:
|
||||
self._connection_state = connection_state
|
||||
self.connectionStateChanged.emit(self._id)
|
||||
|
||||
@pyqtProperty(int, constant = True)
|
||||
def connectionType(self) -> "ConnectionType":
|
||||
return self._connection_type
|
||||
|
||||
@pyqtProperty(int, notify = connectionStateChanged)
|
||||
def connectionState(self) -> "ConnectionState":
|
||||
return self._connection_state
|
||||
|
||||
def _update(self) -> None:
|
||||
pass
|
||||
|
||||
def _getPrinterByKey(self, key: str) -> Optional["PrinterOutputModel"]:
|
||||
for printer in self._printers:
|
||||
if printer.key == key:
|
||||
return printer
|
||||
|
||||
return None
|
||||
|
||||
def requestWrite(self, nodes: List["SceneNode"], file_name: Optional[str] = None, limit_mimetypes: bool = False,
|
||||
file_handler: Optional["FileHandler"] = None, **kwargs: str) -> None:
|
||||
raise NotImplementedError("requestWrite needs to be implemented")
|
||||
|
||||
@pyqtProperty(QObject, notify = printersChanged)
|
||||
def activePrinter(self) -> Optional["PrinterOutputModel"]:
|
||||
if len(self._printers):
|
||||
return self._printers[0]
|
||||
return None
|
||||
|
||||
@pyqtProperty("QVariantList", notify = printersChanged)
|
||||
def printers(self) -> List["PrinterOutputModel"]:
|
||||
return self._printers
|
||||
|
||||
@pyqtProperty(QObject, constant = True)
|
||||
def monitorItem(self) -> QObject:
|
||||
# Note that we specifically only check if the monitor component is created.
|
||||
# It could be that it failed to actually create the qml item! If we check if the item was created, it will try to
|
||||
# create the item (and fail) every time.
|
||||
if not self._monitor_component:
|
||||
self._createMonitorViewFromQML()
|
||||
return self._monitor_item
|
||||
|
||||
@pyqtProperty(QObject, constant = True)
|
||||
def controlItem(self) -> QObject:
|
||||
if not self._control_component:
|
||||
self._createControlViewFromQML()
|
||||
return self._control_item
|
||||
|
||||
def _createControlViewFromQML(self) -> None:
|
||||
if not self._control_view_qml_path:
|
||||
return
|
||||
if self._control_item is None:
|
||||
self._control_item = QtApplication.getInstance().createQmlComponent(self._control_view_qml_path, {"OutputDevice": self})
|
||||
|
||||
def _createMonitorViewFromQML(self) -> None:
|
||||
if not self._monitor_view_qml_path:
|
||||
return
|
||||
|
||||
if self._monitor_item is None:
|
||||
self._monitor_item = QtApplication.getInstance().createQmlComponent(self._monitor_view_qml_path, {"OutputDevice": self})
|
||||
|
||||
## Attempt to establish connection
|
||||
def connect(self) -> None:
|
||||
self.setConnectionState(ConnectionState.Connecting)
|
||||
self._update_timer.start()
|
||||
|
||||
## Attempt to close the connection
|
||||
def close(self) -> None:
|
||||
self._update_timer.stop()
|
||||
self.setConnectionState(ConnectionState.Closed)
|
||||
|
||||
## Ensure that close gets called when object is destroyed
|
||||
def __del__(self) -> None:
|
||||
self.close()
|
||||
|
||||
@pyqtProperty(bool, notify = acceptsCommandsChanged)
|
||||
def acceptsCommands(self) -> bool:
|
||||
return self._accepts_commands
|
||||
|
||||
@deprecated("Please use the protected function instead", "3.2")
|
||||
def setAcceptsCommands(self, accepts_commands: bool) -> None:
|
||||
self._setAcceptsCommands(accepts_commands)
|
||||
|
||||
## Set a flag to signal the UI that the printer is not (yet) ready to receive commands
|
||||
def _setAcceptsCommands(self, accepts_commands: bool) -> None:
|
||||
if self._accepts_commands != accepts_commands:
|
||||
self._accepts_commands = accepts_commands
|
||||
|
||||
self.acceptsCommandsChanged.emit()
|
||||
|
||||
# Returns the unique configurations of the printers within this output device
|
||||
@pyqtProperty("QVariantList", notify = uniqueConfigurationsChanged)
|
||||
def uniqueConfigurations(self) -> List["ConfigurationModel"]:
|
||||
return self._unique_configurations
|
||||
|
||||
def _updateUniqueConfigurations(self) -> None:
|
||||
self._unique_configurations = sorted(
|
||||
{printer.printerConfiguration for printer in self._printers if printer.printerConfiguration is not None},
|
||||
key=lambda config: config.printerType,
|
||||
)
|
||||
self.uniqueConfigurationsChanged.emit()
|
||||
|
||||
# Returns the unique configurations of the printers within this output device
|
||||
@pyqtProperty("QStringList", notify = uniqueConfigurationsChanged)
|
||||
def uniquePrinterTypes(self) -> List[str]:
|
||||
return list(sorted(set([configuration.printerType for configuration in self._unique_configurations])))
|
||||
|
||||
def _onPrintersChanged(self) -> None:
|
||||
for printer in self._printers:
|
||||
printer.configurationChanged.connect(self._updateUniqueConfigurations)
|
||||
|
||||
# At this point there may be non-updated configurations
|
||||
self._updateUniqueConfigurations()
|
||||
|
||||
## Set the device firmware name
|
||||
#
|
||||
# \param name The name of the firmware.
|
||||
def _setFirmwareName(self, name: str) -> None:
|
||||
self._firmware_name = name
|
||||
|
||||
## Get the name of device firmware
|
||||
#
|
||||
# This name can be used to define device type
|
||||
def getFirmwareName(self) -> Optional[str]:
|
||||
return self._firmware_name
|
||||
|
||||
def getFirmwareUpdater(self) -> Optional["FirmwareUpdater"]:
|
||||
return self._firmware_updater
|
||||
|
||||
@pyqtSlot(str)
|
||||
def updateFirmware(self, firmware_file: Union[str, QUrl]) -> None:
|
||||
if not self._firmware_updater:
|
||||
return
|
||||
|
||||
self._firmware_updater.updateFirmware(firmware_file)
|
||||
import warnings
|
||||
warnings.warn("Importing cura.PrinterOutputDevice has been deprecated since 4.1, use cura.PrinterOutput.PrinterOutputDevice instead", DeprecationWarning, stacklevel=2)
|
||||
# We moved the PrinterOutput device to it's own submodule.
|
||||
from cura.PrinterOutput.PrinterOutputDevice import PrinterOutputDevice, ConnectionState
|
|
@ -4,12 +4,12 @@ from cura.Scene.CuraSceneNode import CuraSceneNode
|
|||
|
||||
## Make a SceneNode build plate aware CuraSceneNode objects all have this decorator.
|
||||
class BuildPlateDecorator(SceneNodeDecorator):
|
||||
def __init__(self, build_plate_number = -1):
|
||||
def __init__(self, build_plate_number: int = -1) -> None:
|
||||
super().__init__()
|
||||
self._build_plate_number = None
|
||||
self._build_plate_number = build_plate_number
|
||||
self.setBuildPlateNumber(build_plate_number)
|
||||
|
||||
def setBuildPlateNumber(self, nr):
|
||||
def setBuildPlateNumber(self, nr: int) -> None:
|
||||
# Make sure that groups are set correctly
|
||||
# setBuildPlateForSelection in CuraActions makes sure that no single childs are set.
|
||||
self._build_plate_number = nr
|
||||
|
@ -19,7 +19,7 @@ class BuildPlateDecorator(SceneNodeDecorator):
|
|||
for child in self._node.getChildren():
|
||||
child.callDecoration("setBuildPlateNumber", nr)
|
||||
|
||||
def getBuildPlateNumber(self):
|
||||
def getBuildPlateNumber(self) -> int:
|
||||
return self._build_plate_number
|
||||
|
||||
def __deepcopy__(self, memo):
|
||||
|
|
|
@ -60,13 +60,15 @@ class ConvexHullDecorator(SceneNodeDecorator):
|
|||
previous_node = self._node
|
||||
# Disconnect from previous node signals
|
||||
if previous_node is not None and node is not previous_node:
|
||||
previous_node.transformationChanged.disconnect(self._onChanged)
|
||||
previous_node.parentChanged.disconnect(self._onChanged)
|
||||
previous_node.boundingBoxChanged.disconnect(self._onChanged)
|
||||
|
||||
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.parentChanged.connect(self._onChanged)
|
||||
|
||||
node.boundingBoxChanged.connect(self._onChanged)
|
||||
|
||||
per_object_stack = node.callDecoration("getStack")
|
||||
if per_object_stack:
|
||||
per_object_stack.propertyChanged.connect(self._onSettingValueChanged)
|
||||
|
||||
self._onChanged()
|
||||
|
||||
|
@ -74,27 +76,47 @@ class ConvexHullDecorator(SceneNodeDecorator):
|
|||
def __deepcopy__(self, memo):
|
||||
return ConvexHullDecorator()
|
||||
|
||||
## Get the unmodified 2D projected convex hull of the node (if any)
|
||||
def getConvexHull(self) -> Optional[Polygon]:
|
||||
## The polygon representing the 2D adhesion area.
|
||||
# If no adhesion is used, the regular convex hull is returned
|
||||
def getAdhesionArea(self) -> Optional[Polygon]:
|
||||
if self._node is None:
|
||||
return None
|
||||
|
||||
hull = self._compute2DConvexHull()
|
||||
if hull is None:
|
||||
return None
|
||||
|
||||
return self._add2DAdhesionMargin(hull)
|
||||
|
||||
## Get the unmodified 2D projected convex hull of the node (if any)
|
||||
# In case of one-at-a-time, this includes adhesion and head+fans clearance
|
||||
def getConvexHull(self) -> Optional[Polygon]:
|
||||
if self._node is None:
|
||||
return None
|
||||
if self._node.callDecoration("isNonPrintingMesh"):
|
||||
return None
|
||||
|
||||
if self._global_stack and self._node is not None and hull is not None:
|
||||
# Parent can be None if node is just loaded.
|
||||
if self._global_stack.getProperty("print_sequence", "value") == "one_at_a_time" and not self.hasGroupAsParent(self._node):
|
||||
hull = hull.getMinkowskiHull(Polygon(numpy.array(self._global_stack.getProperty("machine_head_polygon", "value"), numpy.float32)))
|
||||
if self._isSingularOneAtATimeNode():
|
||||
hull = self.getConvexHullHeadFull()
|
||||
if hull is None:
|
||||
return None
|
||||
hull = self._add2DAdhesionMargin(hull)
|
||||
return hull
|
||||
|
||||
## Get the convex hull of the node with the full head size
|
||||
return self._compute2DConvexHull()
|
||||
|
||||
## For one at the time this is the convex hull of the node with the full head size
|
||||
# In case of printing all at once this is None.
|
||||
def getConvexHullHeadFull(self) -> Optional[Polygon]:
|
||||
if self._node is None:
|
||||
return None
|
||||
|
||||
if self._isSingularOneAtATimeNode():
|
||||
return self._compute2DConvexHeadFull()
|
||||
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def hasGroupAsParent(node: "SceneNode") -> bool:
|
||||
parent = node.getParent()
|
||||
|
@ -103,14 +125,14 @@ class ConvexHullDecorator(SceneNodeDecorator):
|
|||
return bool(parent.callDecoration("isGroup"))
|
||||
|
||||
## Get convex hull of the object + head size
|
||||
# In case of printing all at once this is the same as the convex hull.
|
||||
# In case of printing all at once this is None.
|
||||
# For one at the time this is area with intersection of mirrored head
|
||||
def getConvexHullHead(self) -> Optional[Polygon]:
|
||||
if self._node is None:
|
||||
return None
|
||||
|
||||
if self._global_stack:
|
||||
if self._global_stack.getProperty("print_sequence", "value") == "one_at_a_time" and not self.hasGroupAsParent(self._node):
|
||||
if self._node.callDecoration("isNonPrintingMesh"):
|
||||
return None
|
||||
if self._isSingularOneAtATimeNode():
|
||||
head_with_fans = self._compute2DConvexHeadMin()
|
||||
if head_with_fans is None:
|
||||
return None
|
||||
|
@ -119,18 +141,31 @@ class ConvexHullDecorator(SceneNodeDecorator):
|
|||
return None
|
||||
|
||||
## Get convex hull of the node
|
||||
# In case of printing all at once this is the same as the convex hull.
|
||||
# In case of printing all at once this None??
|
||||
# For one at the time this is the area without the head.
|
||||
def getConvexHullBoundary(self) -> Optional[Polygon]:
|
||||
if self._node is None:
|
||||
return None
|
||||
|
||||
if self._global_stack:
|
||||
if self._global_stack.getProperty("print_sequence", "value") == "one_at_a_time" and not self.hasGroupAsParent(self._node):
|
||||
if self._node.callDecoration("isNonPrintingMesh"):
|
||||
return None
|
||||
|
||||
if self._isSingularOneAtATimeNode():
|
||||
# Printing one at a time and it's not an object in a group
|
||||
return self._compute2DConvexHull()
|
||||
return None
|
||||
|
||||
## Get the buildplate polygon where will be printed
|
||||
# In case of printing all at once this is the same as convex hull (no individual adhesion)
|
||||
# For one at the time this includes the adhesion area
|
||||
def getPrintingArea(self) -> Optional[Polygon]:
|
||||
if self._isSingularOneAtATimeNode():
|
||||
# In one-at-a-time mode, every printed object gets it's own adhesion
|
||||
printing_area = self.getAdhesionArea()
|
||||
else:
|
||||
printing_area = self.getConvexHull()
|
||||
return printing_area
|
||||
|
||||
## The same as recomputeConvexHull, but using a timer if it was set.
|
||||
def recomputeConvexHullDelayed(self) -> None:
|
||||
if self._recompute_convex_hull_timer is not None:
|
||||
|
@ -153,10 +188,9 @@ class ConvexHullDecorator(SceneNodeDecorator):
|
|||
self._convex_hull_node = None
|
||||
return
|
||||
|
||||
convex_hull = self.getConvexHull()
|
||||
if self._convex_hull_node:
|
||||
self._convex_hull_node.setParent(None)
|
||||
hull_node = ConvexHullNode.ConvexHullNode(self._node, convex_hull, self._raft_thickness, root)
|
||||
hull_node = ConvexHullNode.ConvexHullNode(self._node, self.getPrintingArea(), self._raft_thickness, root)
|
||||
self._convex_hull_node = hull_node
|
||||
|
||||
def _onSettingValueChanged(self, key: str, property_name: str) -> None:
|
||||
|
@ -259,10 +293,14 @@ class ConvexHullDecorator(SceneNodeDecorator):
|
|||
return offset_hull
|
||||
|
||||
def _getHeadAndFans(self) -> Polygon:
|
||||
if self._global_stack:
|
||||
return Polygon(numpy.array(self._global_stack.getHeadAndFansCoordinates(), numpy.float32))
|
||||
if not self._global_stack:
|
||||
return Polygon()
|
||||
|
||||
polygon = Polygon(numpy.array(self._global_stack.getHeadAndFansCoordinates(), numpy.float32))
|
||||
offset_x = self._getSettingProperty("machine_nozzle_offset_x", "value")
|
||||
offset_y = self._getSettingProperty("machine_nozzle_offset_y", "value")
|
||||
return polygon.translate(-offset_x, -offset_y)
|
||||
|
||||
def _compute2DConvexHeadFull(self) -> Optional[Polygon]:
|
||||
convex_hull = self._compute2DConvexHull()
|
||||
if convex_hull:
|
||||
|
@ -393,6 +431,14 @@ class ConvexHullDecorator(SceneNodeDecorator):
|
|||
return True
|
||||
return self.__isDescendant(root, node.getParent())
|
||||
|
||||
## True if print_sequence is one_at_a_time and _node is not part of a group
|
||||
def _isSingularOneAtATimeNode(self) -> bool:
|
||||
if self._node is None:
|
||||
return False
|
||||
return self._global_stack is not None \
|
||||
and self._global_stack.getProperty("print_sequence", "value") == "one_at_a_time" \
|
||||
and not self.hasGroupAsParent(self._node)
|
||||
|
||||
_affected_settings = [
|
||||
"adhesion_type", "raft_margin", "print_sequence",
|
||||
"skirt_gap", "skirt_line_count", "skirt_brim_line_width", "skirt_distance", "brim_line_count"]
|
||||
|
@ -400,4 +446,4 @@ class ConvexHullDecorator(SceneNodeDecorator):
|
|||
## Settings that change the convex hull.
|
||||
#
|
||||
# If these settings change, the convex hull should be recalculated.
|
||||
_influencing_settings = {"xy_offset", "xy_offset_layer_0", "mold_enabled", "mold_width"}
|
||||
_influencing_settings = {"xy_offset", "xy_offset_layer_0", "mold_enabled", "mold_width", "anti_overhang_mesh", "infill_mesh", "cutting_mesh"}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
# Copyright (c) 2015 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
from typing import Optional
|
||||
from typing import Optional, TYPE_CHECKING
|
||||
|
||||
from UM.Application import Application
|
||||
from UM.Math.Polygon import Polygon
|
||||
|
@ -11,6 +11,9 @@ from UM.Math.Color import Color
|
|||
from UM.Mesh.MeshBuilder import MeshBuilder # To create a mesh to display the convex hull with.
|
||||
from UM.View.GL.OpenGL import OpenGL
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from UM.Mesh.MeshData import MeshData
|
||||
|
||||
|
||||
class ConvexHullNode(SceneNode):
|
||||
shader = None # To prevent the shader from being re-built over and over again, only load it once.
|
||||
|
@ -43,7 +46,8 @@ class ConvexHullNode(SceneNode):
|
|||
|
||||
# The node this mesh is "watching"
|
||||
self._node = node
|
||||
self._convex_hull_head_mesh = None
|
||||
# Area of the head + fans for display as a shadow on the buildplate
|
||||
self._convex_hull_head_mesh = None # type: Optional[MeshData]
|
||||
|
||||
self._node.decoratorsChanged.connect(self._onNodeDecoratorsChanged)
|
||||
self._onNodeDecoratorsChanged(self._node)
|
||||
|
@ -76,14 +80,17 @@ class ConvexHullNode(SceneNode):
|
|||
|
||||
if self.getParent():
|
||||
if self.getMeshData() and isinstance(self._node, SceneNode) and self._node.callDecoration("getBuildPlateNumber") == Application.getInstance().getMultiBuildPlateModel().activeBuildPlate:
|
||||
# The object itself (+ adhesion in one-at-a-time mode)
|
||||
renderer.queueNode(self, transparent = True, shader = ConvexHullNode.shader, backface_cull = True, sort = -8)
|
||||
if self._convex_hull_head_mesh:
|
||||
# The full head. Rendered as a hint to the user: If this area overlaps another object A; this object
|
||||
# cannot be printed after A, because the head would hit A while printing the current object
|
||||
renderer.queueNode(self, shader = ConvexHullNode.shader, transparent = True, mesh = self._convex_hull_head_mesh, backface_cull = True, sort = -8)
|
||||
|
||||
return True
|
||||
|
||||
def _onNodeDecoratorsChanged(self, node: SceneNode) -> None:
|
||||
convex_hull_head = self._node.callDecoration("getConvexHullHead")
|
||||
convex_hull_head = self._node.callDecoration("getConvexHullHeadFull")
|
||||
if convex_hull_head:
|
||||
convex_hull_head_builder = MeshBuilder()
|
||||
convex_hull_head_builder.addConvexPolygon(convex_hull_head.getPoints(), self._mesh_height - self._thickness)
|
||||
|
|
|
@ -3,7 +3,8 @@ from UM.Logger import Logger
|
|||
from PyQt5.QtCore import Qt, pyqtSlot, QObject
|
||||
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 UM.Application import Application
|
||||
|
@ -33,7 +34,7 @@ class CuraSceneController(QObject):
|
|||
source = args[0]
|
||||
else:
|
||||
source = None
|
||||
if not isinstance(source, SceneNode):
|
||||
if not isinstance(source, SceneNode) or isinstance(source, Camera):
|
||||
return
|
||||
max_build_plate = self._calcMaxBuildPlate()
|
||||
changed = False
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
# Copyright (c) 2019 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
from copy import deepcopy
|
||||
|
@ -6,13 +6,14 @@ from typing import cast, Dict, List, Optional
|
|||
|
||||
from UM.Application import Application
|
||||
from UM.Math.AxisAlignedBox import AxisAlignedBox
|
||||
from UM.Math.Polygon import Polygon #For typing.
|
||||
from UM.Math.Polygon import Polygon # For typing.
|
||||
from UM.Scene.SceneNode import SceneNode
|
||||
from UM.Scene.SceneNodeDecorator import SceneNodeDecorator #To cast the deepcopy of every decorator back to SceneNodeDecorator.
|
||||
from UM.Scene.SceneNodeDecorator import SceneNodeDecorator # To cast the deepcopy of every decorator back to SceneNodeDecorator.
|
||||
|
||||
import cura.CuraApplication # To get the build plate.
|
||||
from cura.Settings.ExtruderStack import ExtruderStack # For typing.
|
||||
from cura.Settings.SettingOverrideDecorator import SettingOverrideDecorator # For per-object settings.
|
||||
|
||||
import cura.CuraApplication #To get the build plate.
|
||||
from cura.Settings.ExtruderStack import ExtruderStack #For typing.
|
||||
from cura.Settings.SettingOverrideDecorator import SettingOverrideDecorator #For per-object settings.
|
||||
|
||||
## Scene nodes that are models are only seen when selecting the corresponding build plate
|
||||
# Note that many other nodes can just be UM SceneNode objects.
|
||||
|
@ -20,7 +21,7 @@ class CuraSceneNode(SceneNode):
|
|||
def __init__(self, parent: Optional["SceneNode"] = None, visible: bool = True, name: str = "", no_setting_override: bool = False) -> None:
|
||||
super().__init__(parent = parent, visible = visible, name = name)
|
||||
if not no_setting_override:
|
||||
self.addDecorator(SettingOverrideDecorator()) # now we always have a getActiveExtruderPosition, unless explicitly disabled
|
||||
self.addDecorator(SettingOverrideDecorator()) # Now we always have a getActiveExtruderPosition, unless explicitly disabled
|
||||
self._outside_buildarea = False
|
||||
|
||||
def setOutsideBuildArea(self, new_value: bool) -> None:
|
||||
|
@ -85,24 +86,14 @@ class CuraSceneNode(SceneNode):
|
|||
1.0
|
||||
]
|
||||
|
||||
## Return if the provided bbox collides with the bbox of this scene node
|
||||
def collidesWithBbox(self, check_bbox: AxisAlignedBox) -> bool:
|
||||
bbox = self.getBoundingBox()
|
||||
if bbox is not None:
|
||||
# Mark the node as outside the build volume if the bounding box test fails.
|
||||
if check_bbox.intersectsBox(bbox) != AxisAlignedBox.IntersectionResult.FullIntersection:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
## Return if any area collides with the convex hull of this scene node
|
||||
def collidesWithArea(self, areas: List[Polygon]) -> bool:
|
||||
convex_hull = self.callDecoration("getConvexHull")
|
||||
def collidesWithAreas(self, areas: List[Polygon]) -> bool:
|
||||
convex_hull = self.callDecoration("getPrintingArea")
|
||||
if convex_hull:
|
||||
if not convex_hull.isValid():
|
||||
return False
|
||||
|
||||
# Check for collisions between disallowed areas and the object
|
||||
# Check for collisions between provided areas and the object
|
||||
for area in areas:
|
||||
overlap = convex_hull.intersectsPolygon(area)
|
||||
if overlap is None:
|
||||
|
@ -112,21 +103,24 @@ class CuraSceneNode(SceneNode):
|
|||
|
||||
## Override of SceneNode._calculateAABB to exclude non-printing-meshes from bounding box
|
||||
def _calculateAABB(self) -> None:
|
||||
self._aabb = None
|
||||
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)
|
||||
self._aabb = AxisAlignedBox(minimum=position, maximum=position)
|
||||
|
||||
for child in self._children:
|
||||
for child in self.getAllChildren():
|
||||
if child.callDecoration("isNonPrintingMesh"):
|
||||
# Non-printing-meshes inside a group should not affect push apart or drop to build plate
|
||||
continue
|
||||
if aabb is None:
|
||||
aabb = child.getBoundingBox()
|
||||
if not child.getMeshData():
|
||||
# Nodes without mesh data should not affect bounding boxes of their parents.
|
||||
continue
|
||||
if self._aabb is None:
|
||||
self._aabb = child.getBoundingBox()
|
||||
else:
|
||||
aabb = aabb + child.getBoundingBox()
|
||||
self._aabb = aabb
|
||||
self._aabb = self._aabb + child.getBoundingBox()
|
||||
|
||||
## Taken from SceneNode, but replaced SceneNode with CuraSceneNode
|
||||
def __deepcopy__(self, memo: Dict[int, object]) -> "CuraSceneNode":
|
||||
|
|
|
@ -1,11 +1,18 @@
|
|||
from UM.Scene.SceneNodeDecorator import SceneNodeDecorator
|
||||
from typing import List
|
||||
from typing import List, Optional
|
||||
|
||||
|
||||
class GCodeListDecorator(SceneNodeDecorator):
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self._gcode_list = [] # type: List[str]
|
||||
self._filename = None # type: Optional[str]
|
||||
|
||||
def getGcodeFileName(self) -> Optional[str]:
|
||||
return self._filename
|
||||
|
||||
def setGcodeFileName(self, filename: str) -> None:
|
||||
self._filename = filename
|
||||
|
||||
def getGCodeList(self) -> List[str]:
|
||||
return self._gcode_list
|
||||
|
|
|
@ -1,15 +1,14 @@
|
|||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
# Copyright (c) 2019 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
import os
|
||||
import urllib.parse
|
||||
import uuid
|
||||
from typing import Dict, Union, Any, TYPE_CHECKING, List
|
||||
from typing import Any, cast, Dict, List, TYPE_CHECKING, Union
|
||||
|
||||
from PyQt5.QtCore import QObject, QUrl
|
||||
from PyQt5.QtWidgets import QMessageBox
|
||||
|
||||
|
||||
from UM.i18n import i18nCatalog
|
||||
from UM.FlameProfiler import pyqtSlot
|
||||
from UM.Logger import Logger
|
||||
|
@ -17,21 +16,19 @@ from UM.MimeTypeDatabase import MimeTypeDatabase, MimeTypeNotFoundError
|
|||
from UM.Platform import Platform
|
||||
from UM.SaveFile import SaveFile
|
||||
from UM.Settings.ContainerFormatError import ContainerFormatError
|
||||
from UM.Settings.ContainerRegistry import ContainerRegistry
|
||||
from UM.Settings.ContainerStack import ContainerStack
|
||||
from UM.Settings.DefinitionContainer import DefinitionContainer
|
||||
from UM.Settings.InstanceContainer import InstanceContainer
|
||||
|
||||
import cura.CuraApplication
|
||||
from cura.Machines.ContainerTree import ContainerTree
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from cura.CuraApplication import CuraApplication
|
||||
from cura.Machines.ContainerNode import ContainerNode
|
||||
from cura.Machines.MaterialNode import MaterialNode
|
||||
from cura.Machines.QualityChangesGroup import QualityChangesGroup
|
||||
from UM.PluginRegistry import PluginRegistry
|
||||
from cura.Settings.MachineManager import MachineManager
|
||||
from cura.Machines.MaterialManager import MaterialManager
|
||||
from cura.Machines.QualityManager import QualityManager
|
||||
from cura.Settings.CuraContainerRegistry import CuraContainerRegistry
|
||||
|
||||
catalog = i18nCatalog("cura")
|
||||
|
||||
|
@ -47,20 +44,16 @@ class ContainerManager(QObject):
|
|||
if ContainerManager.__instance is not None:
|
||||
raise RuntimeError("Try to create singleton '%s' more than once" % self.__class__.__name__)
|
||||
ContainerManager.__instance = self
|
||||
|
||||
try:
|
||||
super().__init__(parent = application)
|
||||
except TypeError:
|
||||
super().__init__()
|
||||
|
||||
self._application = application # type: CuraApplication
|
||||
self._plugin_registry = self._application.getPluginRegistry() # type: PluginRegistry
|
||||
self._container_registry = self._application.getContainerRegistry() # type: CuraContainerRegistry
|
||||
self._machine_manager = self._application.getMachineManager() # type: MachineManager
|
||||
self._material_manager = self._application.getMaterialManager() # type: MaterialManager
|
||||
self._quality_manager = self._application.getQualityManager() # type: QualityManager
|
||||
self._container_name_filters = {} # type: Dict[str, Dict[str, Any]]
|
||||
|
||||
@pyqtSlot(str, str, result=str)
|
||||
def getContainerMetaDataEntry(self, container_id: str, entry_names: str) -> str:
|
||||
metadatas = self._container_registry.findContainersMetadata(id = container_id)
|
||||
metadatas = cura.CuraApplication.CuraApplication.getInstance().getContainerRegistry().findContainersMetadata(id = container_id)
|
||||
if not metadatas:
|
||||
Logger.log("w", "Could not get metadata of container %s because it was not found.", container_id)
|
||||
return ""
|
||||
|
@ -89,15 +82,19 @@ class ContainerManager(QObject):
|
|||
# Update: In order for QML to use objects and sub objects, those (sub) objects must all be QObject. Is that what we want?
|
||||
@pyqtSlot("QVariant", str, str)
|
||||
def setContainerMetaDataEntry(self, container_node: "ContainerNode", entry_name: str, entry_value: str) -> bool:
|
||||
if container_node.container is None:
|
||||
Logger.log("w", "Container node {0} doesn't have a container.".format(container_node.container_id))
|
||||
return False
|
||||
root_material_id = container_node.getMetaDataEntry("base_file", "")
|
||||
if self._container_registry.isReadOnly(root_material_id):
|
||||
container_registry = cura.CuraApplication.CuraApplication.getInstance().getContainerRegistry()
|
||||
if container_registry.isReadOnly(root_material_id):
|
||||
Logger.log("w", "Cannot set metadata of read-only container %s.", root_material_id)
|
||||
return False
|
||||
|
||||
material_group = self._material_manager.getMaterialGroup(root_material_id)
|
||||
if material_group is None:
|
||||
Logger.log("w", "Unable to find material group for: %s.", root_material_id)
|
||||
root_material_query = container_registry.findContainers(id = root_material_id)
|
||||
if not root_material_query:
|
||||
Logger.log("w", "Unable to find root material: {root_material}.".format(root_material = root_material_id))
|
||||
return False
|
||||
root_material = root_material_query[0]
|
||||
|
||||
entries = entry_name.split("/")
|
||||
entry_name = entries.pop()
|
||||
|
@ -105,7 +102,7 @@ class ContainerManager(QObject):
|
|||
sub_item_changed = False
|
||||
if entries:
|
||||
root_name = entries.pop(0)
|
||||
root = material_group.root_material_node.getMetaDataEntry(root_name)
|
||||
root = root_material.getMetaDataEntry(root_name)
|
||||
|
||||
item = root
|
||||
for _ in range(len(entries)):
|
||||
|
@ -118,16 +115,14 @@ class ContainerManager(QObject):
|
|||
entry_name = root_name
|
||||
entry_value = root
|
||||
|
||||
container = material_group.root_material_node.getContainer()
|
||||
if container is not None:
|
||||
container.setMetaDataEntry(entry_name, entry_value)
|
||||
root_material.setMetaDataEntry(entry_name, entry_value)
|
||||
if sub_item_changed: #If it was only a sub-item that has changed then the setMetaDataEntry won't correctly notice that something changed, and we must manually signal that the metadata changed.
|
||||
container.metaDataChanged.emit(container)
|
||||
root_material.metaDataChanged.emit(root_material)
|
||||
return True
|
||||
|
||||
@pyqtSlot(str, result = str)
|
||||
def makeUniqueName(self, original_name: str) -> str:
|
||||
return self._container_registry.uniqueName(original_name)
|
||||
return cura.CuraApplication.CuraApplication.getInstance().getContainerRegistry().uniqueName(original_name)
|
||||
|
||||
## Get a list of string that can be used as name filters for a Qt File Dialog
|
||||
#
|
||||
|
@ -182,7 +177,7 @@ class ContainerManager(QObject):
|
|||
else:
|
||||
mime_type = self._container_name_filters[file_type]["mime"]
|
||||
|
||||
containers = self._container_registry.findContainers(id = container_id)
|
||||
containers = cura.CuraApplication.CuraApplication.getInstance().getContainerRegistry().findContainers(id = container_id)
|
||||
if not containers:
|
||||
return {"status": "error", "message": "Container not found"}
|
||||
container = containers[0]
|
||||
|
@ -240,18 +235,19 @@ class ContainerManager(QObject):
|
|||
except MimeTypeNotFoundError:
|
||||
return {"status": "error", "message": "Could not determine mime type of file"}
|
||||
|
||||
container_type = self._container_registry.getContainerForMimeType(mime_type)
|
||||
container_registry = cura.CuraApplication.CuraApplication.getInstance().getContainerRegistry()
|
||||
container_type = container_registry.getContainerForMimeType(mime_type)
|
||||
if not container_type:
|
||||
return {"status": "error", "message": "Could not find a container to handle the specified file."}
|
||||
|
||||
container_id = urllib.parse.unquote_plus(mime_type.stripExtension(os.path.basename(file_url)))
|
||||
container_id = self._container_registry.uniqueName(container_id)
|
||||
container_id = container_registry.uniqueName(container_id)
|
||||
|
||||
container = container_type(container_id)
|
||||
|
||||
try:
|
||||
with open(file_url, "rt", encoding = "utf-8") as f:
|
||||
container.deserialize(f.read())
|
||||
container.deserialize(f.read(), file_url)
|
||||
except PermissionError:
|
||||
return {"status": "error", "message": "Permission denied when trying to read the file."}
|
||||
except ContainerFormatError:
|
||||
|
@ -261,7 +257,7 @@ class ContainerManager(QObject):
|
|||
|
||||
container.setDirty(True)
|
||||
|
||||
self._container_registry.addContainer(container)
|
||||
container_registry.addContainer(container)
|
||||
|
||||
return {"status": "success", "message": "Successfully imported container {0}".format(container.getName())}
|
||||
|
||||
|
@ -273,44 +269,55 @@ class ContainerManager(QObject):
|
|||
# \return \type{bool} True if successful, False if not.
|
||||
@pyqtSlot(result = bool)
|
||||
def updateQualityChanges(self) -> bool:
|
||||
global_stack = self._machine_manager.activeMachine
|
||||
application = cura.CuraApplication.CuraApplication.getInstance()
|
||||
global_stack = application.getMachineManager().activeMachine
|
||||
if not global_stack:
|
||||
return False
|
||||
|
||||
self._machine_manager.blurSettings.emit()
|
||||
application.getMachineManager().blurSettings.emit()
|
||||
|
||||
current_quality_changes_name = global_stack.qualityChanges.getName()
|
||||
current_quality_type = global_stack.quality.getMetaDataEntry("quality_type")
|
||||
extruder_stacks = list(global_stack.extruders.values())
|
||||
container_registry = cura.CuraApplication.CuraApplication.getInstance().getContainerRegistry()
|
||||
machine_definition_id = ContainerTree.getInstance().machines[global_stack.definition.getId()].quality_definition
|
||||
for stack in [global_stack] + extruder_stacks:
|
||||
# Find the quality_changes container for this stack and merge the contents of the top container into it.
|
||||
quality_changes = stack.qualityChanges
|
||||
|
||||
if quality_changes.getId() == "empty_quality_changes":
|
||||
quality_changes = self._quality_manager._createQualityChanges(current_quality_type, current_quality_changes_name,
|
||||
global_stack, stack)
|
||||
self._container_registry.addContainer(quality_changes)
|
||||
quality_changes = InstanceContainer(container_registry.uniqueName((stack.getId() + "_" + current_quality_changes_name).lower().replace(" ", "_")))
|
||||
quality_changes.setName(current_quality_changes_name)
|
||||
quality_changes.setMetaDataEntry("type", "quality_changes")
|
||||
quality_changes.setMetaDataEntry("quality_type", current_quality_type)
|
||||
if stack.getMetaDataEntry("position") is not None: # Extruder stacks.
|
||||
quality_changes.setMetaDataEntry("position", stack.getMetaDataEntry("position"))
|
||||
quality_changes.setMetaDataEntry("intent_category", stack.quality.getMetaDataEntry("intent_category", "default"))
|
||||
quality_changes.setMetaDataEntry("setting_version", application.SettingVersion)
|
||||
quality_changes.setDefinition(machine_definition_id)
|
||||
container_registry.addContainer(quality_changes)
|
||||
stack.qualityChanges = quality_changes
|
||||
|
||||
if not quality_changes or self._container_registry.isReadOnly(quality_changes.getId()):
|
||||
if not quality_changes or container_registry.isReadOnly(quality_changes.getId()):
|
||||
Logger.log("e", "Could not update quality of a nonexistant or read only quality profile in stack %s", stack.getId())
|
||||
continue
|
||||
|
||||
self._performMerge(quality_changes, stack.getTop())
|
||||
|
||||
self._machine_manager.activeQualityChangesGroupChanged.emit()
|
||||
cura.CuraApplication.CuraApplication.getInstance().getMachineManager().activeQualityChangesGroupChanged.emit()
|
||||
|
||||
return True
|
||||
|
||||
## Clear the top-most (user) containers of the active stacks.
|
||||
@pyqtSlot()
|
||||
def clearUserContainers(self) -> None:
|
||||
self._machine_manager.blurSettings.emit()
|
||||
machine_manager = cura.CuraApplication.CuraApplication.getInstance().getMachineManager()
|
||||
machine_manager.blurSettings.emit()
|
||||
|
||||
send_emits_containers = []
|
||||
|
||||
# Go through global and extruder stacks and clear their topmost container (the user settings).
|
||||
global_stack = self._machine_manager.activeMachine
|
||||
global_stack = machine_manager.activeMachine
|
||||
extruder_stacks = list(global_stack.extruders.values())
|
||||
for stack in [global_stack] + extruder_stacks:
|
||||
container = stack.userChanges
|
||||
|
@ -318,40 +325,38 @@ class ContainerManager(QObject):
|
|||
send_emits_containers.append(container)
|
||||
|
||||
# user changes are possibly added to make the current setup match the current enabled extruders
|
||||
self._machine_manager.correctExtruderSettings()
|
||||
machine_manager.correctExtruderSettings()
|
||||
|
||||
for container in send_emits_containers:
|
||||
container.sendPostponedEmits()
|
||||
|
||||
## Get a list of materials that have the same GUID as the reference material
|
||||
#
|
||||
# \param material_id \type{str} the id of the material for which to get the linked materials.
|
||||
# \return \type{list} a list of names of materials with the same GUID
|
||||
# \param material_node The node representing the material for which to get
|
||||
# the same GUID.
|
||||
# \param exclude_self Whether to include the name of the material you
|
||||
# provided.
|
||||
# \return A list of names of materials with the same GUID.
|
||||
@pyqtSlot("QVariant", bool, result = "QStringList")
|
||||
def getLinkedMaterials(self, material_node: "MaterialNode", exclude_self: bool = False):
|
||||
guid = material_node.getMetaDataEntry("GUID", "")
|
||||
|
||||
self_root_material_id = material_node.getMetaDataEntry("base_file")
|
||||
material_group_list = self._material_manager.getMaterialGroupListByGUID(guid)
|
||||
|
||||
linked_material_names = []
|
||||
if material_group_list:
|
||||
for material_group in material_group_list:
|
||||
if exclude_self and material_group.name == self_root_material_id:
|
||||
continue
|
||||
linked_material_names.append(material_group.root_material_node.getMetaDataEntry("name", ""))
|
||||
return linked_material_names
|
||||
def getLinkedMaterials(self, material_node: "MaterialNode", exclude_self: bool = False) -> List[str]:
|
||||
same_guid = ContainerRegistry.getInstance().findInstanceContainersMetadata(GUID = material_node.guid)
|
||||
if exclude_self:
|
||||
return list({meta["name"] for meta in same_guid if meta["base_file"] != material_node.base_file})
|
||||
else:
|
||||
return list({meta["name"] for meta in same_guid})
|
||||
|
||||
## Unlink a material from all other materials by creating a new GUID
|
||||
# \param material_id \type{str} the id of the material to create a new GUID for.
|
||||
@pyqtSlot("QVariant")
|
||||
def unlinkMaterial(self, material_node: "MaterialNode") -> None:
|
||||
# Get the material group
|
||||
material_group = self._material_manager.getMaterialGroup(material_node.getMetaDataEntry("base_file", ""))
|
||||
|
||||
if material_group is None:
|
||||
if material_node.container is None: # Failed to lazy-load this container.
|
||||
return
|
||||
root_material_query = cura.CuraApplication.CuraApplication.getInstance().getContainerRegistry().findInstanceContainers(id = material_node.getMetaDataEntry("base_file", ""))
|
||||
if not root_material_query:
|
||||
Logger.log("w", "Unable to find material group for %s", material_node)
|
||||
return
|
||||
root_material = root_material_query[0]
|
||||
|
||||
# Generate a new GUID
|
||||
new_guid = str(uuid.uuid4())
|
||||
|
@ -359,9 +364,7 @@ class ContainerManager(QObject):
|
|||
# Update the GUID
|
||||
# NOTE: We only need to set the root material container because XmlMaterialProfile.setMetaDataEntry() will
|
||||
# take care of the derived containers too
|
||||
container = material_group.root_material_node.getContainer()
|
||||
if container is not None:
|
||||
container.setMetaDataEntry("GUID", new_guid)
|
||||
root_material.setMetaDataEntry("GUID", new_guid)
|
||||
|
||||
def _performMerge(self, merge_into: InstanceContainer, merge: InstanceContainer, clear_settings: bool = True) -> None:
|
||||
if merge == merge_into:
|
||||
|
@ -375,14 +378,16 @@ class ContainerManager(QObject):
|
|||
|
||||
def _updateContainerNameFilters(self) -> None:
|
||||
self._container_name_filters = {}
|
||||
for plugin_id, container_type in self._container_registry.getContainerTypes():
|
||||
plugin_registry = cura.CuraApplication.CuraApplication.getInstance().getPluginRegistry()
|
||||
container_registry = cura.CuraApplication.CuraApplication.getInstance().getContainerRegistry()
|
||||
for plugin_id, container_type in container_registry.getContainerTypes():
|
||||
# Ignore default container types since those are not plugins
|
||||
if container_type in (InstanceContainer, ContainerStack, DefinitionContainer):
|
||||
continue
|
||||
|
||||
serialize_type = ""
|
||||
try:
|
||||
plugin_metadata = self._plugin_registry.getMetaData(plugin_id)
|
||||
plugin_metadata = plugin_registry.getMetaData(plugin_id)
|
||||
if plugin_metadata:
|
||||
serialize_type = plugin_metadata["settings_container"]["type"]
|
||||
else:
|
||||
|
@ -390,7 +395,7 @@ class ContainerManager(QObject):
|
|||
except KeyError as e:
|
||||
continue
|
||||
|
||||
mime_type = self._container_registry.getMimeTypeForContainer(container_type)
|
||||
mime_type = container_registry.getMimeTypeForContainer(container_type)
|
||||
if mime_type is None:
|
||||
continue
|
||||
entry = {
|
||||
|
@ -426,7 +431,7 @@ class ContainerManager(QObject):
|
|||
path = file_url.toLocalFile()
|
||||
if not path:
|
||||
return {"status": "error", "message": catalog.i18nc("@info:status", "Invalid file URL:") + " " + str(file_url)}
|
||||
return self._container_registry.importProfile(path)
|
||||
return cura.CuraApplication.CuraApplication.getInstance().getContainerRegistry().importProfile(path)
|
||||
|
||||
@pyqtSlot(QObject, QUrl, str)
|
||||
def exportQualityChangesGroup(self, quality_changes_group: "QualityChangesGroup", file_url: QUrl, file_type: str) -> None:
|
||||
|
@ -436,8 +441,11 @@ class ContainerManager(QObject):
|
|||
if not path:
|
||||
return
|
||||
|
||||
container_list = [n.getContainer() for n in quality_changes_group.getAllNodes() if n.getContainer() is not None]
|
||||
self._container_registry.exportQualityProfile(container_list, path, file_type)
|
||||
container_registry = cura.CuraApplication.CuraApplication.getInstance().getContainerRegistry()
|
||||
container_list = [cast(InstanceContainer, container_registry.findContainers(id = quality_changes_group.metadata_for_global["id"])[0])] # type: List[InstanceContainer]
|
||||
for metadata in quality_changes_group.metadata_per_extruder.values():
|
||||
container_list.append(cast(InstanceContainer, container_registry.findContainers(id = metadata["id"])[0]))
|
||||
cura.CuraApplication.CuraApplication.getInstance().getContainerRegistry().exportQualityProfile(container_list, path, file_type)
|
||||
|
||||
__instance = None # type: ContainerManager
|
||||
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
# Copyright (c) 2019 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
import os
|
||||
import re
|
||||
import configparser
|
||||
|
||||
from typing import cast, Dict, Optional
|
||||
from typing import Any, cast, Dict, Optional, List, Union
|
||||
from PyQt5.QtWidgets import QMessageBox
|
||||
|
||||
from UM.Decorators import override
|
||||
|
@ -20,14 +20,16 @@ from UM.Logger import Logger
|
|||
from UM.Message import Message
|
||||
from UM.Platform import Platform
|
||||
from UM.PluginRegistry import PluginRegistry # For getting the possible profile writers to write with.
|
||||
from UM.Util import parseBool
|
||||
from UM.Resources import Resources
|
||||
from UM.Util import parseBool
|
||||
from cura.ReaderWriters.ProfileWriter import ProfileWriter
|
||||
|
||||
from . import ExtruderStack
|
||||
from . import GlobalStack
|
||||
|
||||
import cura.CuraApplication
|
||||
from cura.Machines.QualityManager import getMachineDefinitionIDForQualitySearch
|
||||
from cura.Settings.cura_empty_instance_containers import empty_quality_container
|
||||
from cura.Machines.ContainerTree import ContainerTree
|
||||
from cura.ReaderWriters.ProfileReader import NoProfileException, ProfileReader
|
||||
|
||||
from UM.i18n import i18nCatalog
|
||||
|
@ -50,10 +52,10 @@ class CuraContainerRegistry(ContainerRegistry):
|
|||
# This will also try to convert a ContainerStack to either Extruder or
|
||||
# Global stack based on metadata information.
|
||||
@override(ContainerRegistry)
|
||||
def addContainer(self, container):
|
||||
def addContainer(self, container: ContainerInterface) -> None:
|
||||
# Note: Intentional check with type() because we want to ignore subclasses
|
||||
if type(container) == ContainerStack:
|
||||
container = self._convertContainerStack(container)
|
||||
container = self._convertContainerStack(cast(ContainerStack, container))
|
||||
|
||||
if isinstance(container, InstanceContainer) and type(container) != type(self.getEmptyInstanceContainer()):
|
||||
# Check against setting version of the definition.
|
||||
|
@ -61,7 +63,7 @@ class CuraContainerRegistry(ContainerRegistry):
|
|||
actual_setting_version = int(container.getMetaDataEntry("setting_version", default = 0))
|
||||
if required_setting_version != actual_setting_version:
|
||||
Logger.log("w", "Instance container {container_id} is outdated. Its setting version is {actual_setting_version} but it should be {required_setting_version}.".format(container_id = container.getId(), actual_setting_version = actual_setting_version, required_setting_version = required_setting_version))
|
||||
return #Don't add.
|
||||
return # Don't add.
|
||||
|
||||
super().addContainer(container)
|
||||
|
||||
|
@ -71,9 +73,9 @@ class CuraContainerRegistry(ContainerRegistry):
|
|||
# \param new_name \type{string} Base name, which may not be unique
|
||||
# \param fallback_name \type{string} Name to use when (stripped) new_name is empty
|
||||
# \return \type{string} Name that is unique for the specified type and name/id
|
||||
def createUniqueName(self, container_type, current_name, new_name, fallback_name):
|
||||
def createUniqueName(self, container_type: str, current_name: str, new_name: str, fallback_name: str) -> str:
|
||||
new_name = new_name.strip()
|
||||
num_check = re.compile("(.*?)\s*#\d+$").match(new_name)
|
||||
num_check = re.compile(r"(.*?)\s*#\d+$").match(new_name)
|
||||
if num_check:
|
||||
new_name = num_check.group(1)
|
||||
if new_name == "":
|
||||
|
@ -92,7 +94,7 @@ class CuraContainerRegistry(ContainerRegistry):
|
|||
# Both the id and the name are checked, because they may not be the same and it is better if they are both unique
|
||||
# \param container_type \type{string} Type of the container (machine, quality, ...)
|
||||
# \param container_name \type{string} Name to check
|
||||
def _containerExists(self, container_type, container_name):
|
||||
def _containerExists(self, container_type: str, container_name: str):
|
||||
container_class = ContainerStack if container_type == "machine" else InstanceContainer
|
||||
|
||||
return self.findContainersMetadata(container_type = container_class, id = container_name, type = container_type, ignore_case = True) or \
|
||||
|
@ -100,16 +102,18 @@ class CuraContainerRegistry(ContainerRegistry):
|
|||
|
||||
## Exports an profile to a file
|
||||
#
|
||||
# \param instance_ids \type{list} the IDs of the profiles to export.
|
||||
# \param container_list \type{list} the containers to export. This is not
|
||||
# necessarily in any order!
|
||||
# \param file_name \type{str} the full path and filename to export to.
|
||||
# \param file_type \type{str} the file type with the format "<description> (*.<extension>)"
|
||||
def exportQualityProfile(self, container_list, file_name, file_type):
|
||||
# \return True if the export succeeded, false otherwise.
|
||||
def exportQualityProfile(self, container_list: List[InstanceContainer], file_name: str, file_type: str) -> bool:
|
||||
# Parse the fileType to deduce what plugin can save the file format.
|
||||
# fileType has the format "<description> (*.<extension>)"
|
||||
split = file_type.rfind(" (*.") # Find where the description ends and the extension starts.
|
||||
if split < 0: # Not found. Invalid format.
|
||||
Logger.log("e", "Invalid file format identifier %s", file_type)
|
||||
return
|
||||
return False
|
||||
description = file_type[:split]
|
||||
extension = file_type[split + 4:-1] # Leave out the " (*." and ")".
|
||||
if not file_name.endswith("." + extension): # Auto-fill the extension if the user did not provide any.
|
||||
|
@ -121,10 +125,12 @@ class CuraContainerRegistry(ContainerRegistry):
|
|||
result = QMessageBox.question(None, catalog.i18nc("@title:window", "File Already Exists"),
|
||||
catalog.i18nc("@label Don't translate the XML tag <filename>!", "The file <filename>{0}</filename> already exists. Are you sure you want to overwrite it?").format(file_name))
|
||||
if result == QMessageBox.No:
|
||||
return
|
||||
return False
|
||||
|
||||
profile_writer = self._findProfileWriter(extension, description)
|
||||
try:
|
||||
if profile_writer is None:
|
||||
raise Exception("Unable to find a profile writer")
|
||||
success = profile_writer.write(file_name, container_list)
|
||||
except Exception as e:
|
||||
Logger.log("e", "Failed to export profile to %s: %s", file_name, str(e))
|
||||
|
@ -132,23 +138,24 @@ class CuraContainerRegistry(ContainerRegistry):
|
|||
lifetime = 0,
|
||||
title = catalog.i18nc("@info:title", "Error"))
|
||||
m.show()
|
||||
return
|
||||
return False
|
||||
if not success:
|
||||
Logger.log("w", "Failed to export profile to %s: Writer plugin reported failure.", file_name)
|
||||
m = Message(catalog.i18nc("@info:status Don't translate the XML tag <filename>!", "Failed to export profile to <filename>{0}</filename>: Writer plugin reported failure.", file_name),
|
||||
lifetime = 0,
|
||||
title = catalog.i18nc("@info:title", "Error"))
|
||||
m.show()
|
||||
return
|
||||
return False
|
||||
m = Message(catalog.i18nc("@info:status Don't translate the XML tag <filename>!", "Exported profile to <filename>{0}</filename>", file_name),
|
||||
title = catalog.i18nc("@info:title", "Export succeeded"))
|
||||
m.show()
|
||||
return True
|
||||
|
||||
## Gets the plugin object matching the criteria
|
||||
# \param extension
|
||||
# \param description
|
||||
# \return The plugin object matching the given extension and description.
|
||||
def _findProfileWriter(self, extension, description):
|
||||
def _findProfileWriter(self, extension: str, description: str) -> Optional[ProfileWriter]:
|
||||
plugin_registry = PluginRegistry.getInstance()
|
||||
for plugin_id, meta_data in self._getIOPlugins("profile_writer"):
|
||||
for supported_type in meta_data["profile_writer"]: # All file types this plugin can supposedly write.
|
||||
|
@ -156,7 +163,7 @@ class CuraContainerRegistry(ContainerRegistry):
|
|||
if supported_extension == extension: # This plugin supports a file type with the same extension.
|
||||
supported_description = supported_type.get("description", None)
|
||||
if supported_description == description: # The description is also identical. Assume it's the same file type.
|
||||
return plugin_registry.getPluginObject(plugin_id)
|
||||
return cast(ProfileWriter, plugin_registry.getPluginObject(plugin_id))
|
||||
return None
|
||||
|
||||
## Imports a profile from a file
|
||||
|
@ -169,17 +176,18 @@ class CuraContainerRegistry(ContainerRegistry):
|
|||
if not file_name:
|
||||
return { "status": "error", "message": catalog.i18nc("@info:status Don't translate the XML tags <filename>!", "Failed to import profile from <filename>{0}</filename>: {1}", file_name, "Invalid path")}
|
||||
|
||||
plugin_registry = PluginRegistry.getInstance()
|
||||
extension = file_name.split(".")[-1]
|
||||
|
||||
global_stack = Application.getInstance().getGlobalContainerStack()
|
||||
if not global_stack:
|
||||
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)}
|
||||
container_tree = ContainerTree.getInstance()
|
||||
|
||||
machine_extruders = []
|
||||
for position in sorted(global_stack.extruders):
|
||||
machine_extruders.append(global_stack.extruders[position])
|
||||
|
||||
plugin_registry = PluginRegistry.getInstance()
|
||||
extension = file_name.split(".")[-1]
|
||||
|
||||
for plugin_id, meta_data in self._getIOPlugins("profile_reader"):
|
||||
if meta_data["profile_reader"][0]["extension"] != extension:
|
||||
continue
|
||||
|
@ -221,7 +229,7 @@ class CuraContainerRegistry(ContainerRegistry):
|
|||
# Make sure we have a profile_definition in the file:
|
||||
if profile_definition is None:
|
||||
break
|
||||
machine_definitions = self.findDefinitionContainers(id = profile_definition)
|
||||
machine_definitions = self.findContainers(id = profile_definition)
|
||||
if not machine_definitions:
|
||||
Logger.log("e", "Incorrect profile [%s]. Unknown machine type [%s]", file_name, profile_definition)
|
||||
return {"status": "error",
|
||||
|
@ -231,17 +239,17 @@ class CuraContainerRegistry(ContainerRegistry):
|
|||
|
||||
# Get the expected machine definition.
|
||||
# i.e.: We expect gcode for a UM2 Extended to be defined as normal UM2 gcode...
|
||||
profile_definition = getMachineDefinitionIDForQualitySearch(machine_definition)
|
||||
expected_machine_definition = getMachineDefinitionIDForQualitySearch(global_stack.definition)
|
||||
has_machine_quality = parseBool(machine_definition.getMetaDataEntry("has_machine_quality", "false"))
|
||||
profile_definition = machine_definition.getMetaDataEntry("quality_definition", machine_definition.getId()) if has_machine_quality else "fdmprinter"
|
||||
expected_machine_definition = container_tree.machines[global_stack.definition.getId()].quality_definition
|
||||
|
||||
# And check if the profile_definition matches either one (showing error if not):
|
||||
if profile_definition != expected_machine_definition:
|
||||
Logger.log("e", "Profile [%s] is for machine [%s] but the current active machine is [%s]. Will not import the profile", file_name, profile_definition, expected_machine_definition)
|
||||
return { "status": "error",
|
||||
"message": catalog.i18nc("@info:status Don't translate the XML tags <filename>!", "The machine defined in profile <filename>{0}</filename> ({1}) doesn't match with your current machine ({2}), could not import it.", file_name, profile_definition, expected_machine_definition)}
|
||||
|
||||
# Fix the global quality profile's definition field in case it's not correct
|
||||
Logger.log("d", "Profile {file_name} is for machine {profile_definition}, but the current active machine is {expected_machine_definition}. Changing profile's definition.".format(file_name = file_name, profile_definition = profile_definition, expected_machine_definition = expected_machine_definition))
|
||||
global_profile.setMetaDataEntry("definition", expected_machine_definition)
|
||||
for extruder_profile in extruder_profiles:
|
||||
extruder_profile.setMetaDataEntry("definition", expected_machine_definition)
|
||||
|
||||
quality_name = global_profile.getName()
|
||||
quality_type = global_profile.getMetaDataEntry("quality_type")
|
||||
|
||||
|
@ -264,10 +272,9 @@ class CuraContainerRegistry(ContainerRegistry):
|
|||
profile.setMetaDataEntry("type", "quality_changes")
|
||||
profile.setMetaDataEntry("definition", expected_machine_definition)
|
||||
profile.setMetaDataEntry("quality_type", quality_type)
|
||||
profile.setMetaDataEntry("position", "0")
|
||||
profile.setDirty(True)
|
||||
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():
|
||||
settable_per_extruder = global_stack.getProperty(qc_setting_key, "settable_per_extruder")
|
||||
if settable_per_extruder:
|
||||
|
@ -281,13 +288,14 @@ class CuraContainerRegistry(ContainerRegistry):
|
|||
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)
|
||||
|
||||
for profile in extruder_profiles:
|
||||
profile_or_list.append(profile)
|
||||
|
||||
# Import all profiles
|
||||
profile_ids_added = [] # type: List[str]
|
||||
for profile_index, profile in enumerate(profile_or_list):
|
||||
if profile_index == 0:
|
||||
# This is assumed to be the global profile
|
||||
|
@ -303,16 +311,20 @@ class CuraContainerRegistry(ContainerRegistry):
|
|||
profile.setMetaDataEntry("position", extruder_position)
|
||||
profile_id = (extruder_id + "_" + name_seed).lower().replace(" ", "_")
|
||||
|
||||
else: #More extruders in the imported file than in the machine.
|
||||
continue #Delete the additional profiles.
|
||||
else: # More extruders in the imported file than in the machine.
|
||||
continue # Delete the additional profiles.
|
||||
|
||||
result = self._configureProfile(profile, profile_id, new_name, expected_machine_definition)
|
||||
if result is not None:
|
||||
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>:",
|
||||
file_name) + " <message>" + result + "</message>"}
|
||||
# Remove any profiles that did got added.
|
||||
for profile_id in profile_ids_added:
|
||||
self.removeContainer(profile_id)
|
||||
|
||||
return {"status": "error", "message": catalog.i18nc(
|
||||
"@info:status Don't translate the XML tag <filename>!",
|
||||
"Failed to import profile from <filename>{0}</filename>:",
|
||||
file_name) + " " + result}
|
||||
profile_ids_added.append(profile.getId())
|
||||
return {"status": "ok", "message": catalog.i18nc("@info:status", "Successfully imported profile {0}", profile_or_list[0].getName())}
|
||||
|
||||
# This message is throw when the profile reader doesn't find any profile in the file
|
||||
|
@ -322,11 +334,28 @@ class CuraContainerRegistry(ContainerRegistry):
|
|||
return {"status": "error", "message": catalog.i18nc("@info:status", "Profile {0} has an unknown file type or is corrupted.", file_name)}
|
||||
|
||||
@override(ContainerRegistry)
|
||||
def load(self):
|
||||
def load(self) -> None:
|
||||
super().load()
|
||||
self._registerSingleExtrusionMachinesExtruderStacks()
|
||||
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.
|
||||
#
|
||||
# \param profile The profile to configure.
|
||||
|
@ -358,21 +387,40 @@ class CuraContainerRegistry(ContainerRegistry):
|
|||
global_stack = Application.getInstance().getGlobalContainerStack()
|
||||
if global_stack is None:
|
||||
return None
|
||||
definition_id = getMachineDefinitionIDForQualitySearch(global_stack.definition)
|
||||
definition_id = ContainerTree.getInstance().machines[global_stack.definition.getId()].quality_definition
|
||||
profile.setDefinition(definition_id)
|
||||
|
||||
# Check to make sure the imported profile actually makes sense in context of the current configuration.
|
||||
# This prevents issues where importing a "draft" profile for a machine without "draft" qualities would report as
|
||||
# successfully imported but then fail to show up.
|
||||
quality_manager = cura.CuraApplication.CuraApplication.getInstance()._quality_manager
|
||||
quality_group_dict = quality_manager.getQualityGroupsForMachineDefinition(global_stack)
|
||||
if quality_type not in quality_group_dict:
|
||||
quality_group_dict = ContainerTree.getInstance().getCurrentQualityGroups()
|
||||
# "not_supported" profiles can be imported.
|
||||
if quality_type != empty_quality_container.getMetaDataEntry("quality_type") and quality_type not in quality_group_dict:
|
||||
return catalog.i18nc("@info:status", "Could not find a quality type {0} for the current configuration.", quality_type)
|
||||
|
||||
ContainerRegistry.getInstance().addContainer(profile)
|
||||
|
||||
return None
|
||||
|
||||
@override(ContainerRegistry)
|
||||
def saveDirtyContainers(self) -> None:
|
||||
# Lock file for "more" atomically loading and saving to/from config dir.
|
||||
with self.lockFile():
|
||||
# Save base files first
|
||||
for instance in self.findDirtyContainers(container_type=InstanceContainer):
|
||||
if instance.getMetaDataEntry("removed"):
|
||||
continue
|
||||
if instance.getId() == instance.getMetaData().get("base_file"):
|
||||
self.saveContainer(instance)
|
||||
|
||||
for instance in self.findDirtyContainers(container_type=InstanceContainer):
|
||||
if instance.getMetaDataEntry("removed"):
|
||||
continue
|
||||
self.saveContainer(instance)
|
||||
|
||||
for stack in self.findContainerStacks():
|
||||
self.saveContainer(stack)
|
||||
|
||||
## Gets a list of profile writer plugins
|
||||
# \return List of tuples of (plugin_id, meta_data).
|
||||
def _getIOPlugins(self, io_type):
|
||||
|
@ -386,32 +434,8 @@ class CuraContainerRegistry(ContainerRegistry):
|
|||
result.append( (plugin_id, meta_data) )
|
||||
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.
|
||||
def _convertContainerStack(self, container):
|
||||
def _convertContainerStack(self, container: ContainerStack) -> Union[ExtruderStack.ExtruderStack, GlobalStack.GlobalStack]:
|
||||
assert type(container) == ContainerStack
|
||||
|
||||
container_type = container.getMetaDataEntry("type")
|
||||
|
@ -435,14 +459,14 @@ class CuraContainerRegistry(ContainerRegistry):
|
|||
|
||||
return new_stack
|
||||
|
||||
def _registerSingleExtrusionMachinesExtruderStacks(self):
|
||||
def _registerSingleExtrusionMachinesExtruderStacks(self) -> None:
|
||||
machines = self.findContainerStacks(type = "machine", machine_extruder_trains = {"0": "fdmextruder"})
|
||||
for machine in machines:
|
||||
extruder_stacks = self.findContainerStacks(type = "extruder_train", machine = machine.getId())
|
||||
if not extruder_stacks:
|
||||
self.addExtruderStackForSingleExtrusionMachine(machine, "fdmextruder")
|
||||
|
||||
def _onContainerAdded(self, container):
|
||||
def _onContainerAdded(self, container: ContainerInterface) -> None:
|
||||
# We don't have all the machines loaded in the beginning, so in order to add the missing extruder stack
|
||||
# for single extrusion machines, we subscribe to the containerAdded signal, and whenever a global stack
|
||||
# is added, we check to see if an extruder stack needs to be added.
|
||||
|
@ -521,7 +545,7 @@ class CuraContainerRegistry(ContainerRegistry):
|
|||
user_container.setMetaDataEntry("position", extruder_stack.getMetaDataEntry("position"))
|
||||
|
||||
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.
|
||||
for user_setting_key in machine.userChanges.getAllKeys():
|
||||
settable_per_extruder = machine.getProperty(user_setting_key, "settable_per_extruder")
|
||||
|
@ -583,7 +607,7 @@ class CuraContainerRegistry(ContainerRegistry):
|
|||
extruder_quality_changes_container.setMetaDataEntry("position", extruder_definition.getMetaDataEntry("position"))
|
||||
extruder_stack.qualityChanges = self.findInstanceContainers(id = quality_changes_id)[0]
|
||||
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_id = self.uniqueName(extruder_stack.getId() + "_qc_" + container_name)
|
||||
extruder_quality_changes_container = InstanceContainer(container_id, parent = application)
|
||||
|
@ -592,6 +616,7 @@ class CuraContainerRegistry(ContainerRegistry):
|
|||
extruder_quality_changes_container.setMetaDataEntry("setting_version", application.SettingVersion)
|
||||
extruder_quality_changes_container.setMetaDataEntry("position", extruder_definition.getMetaDataEntry("position"))
|
||||
extruder_quality_changes_container.setMetaDataEntry("quality_type", machine_quality_changes.getMetaDataEntry("quality_type"))
|
||||
extruder_quality_changes_container.setMetaDataEntry("intent_category", "default") # Intent categories weren't a thing back then.
|
||||
extruder_quality_changes_container.setDefinition(machine_quality_changes.getDefinition().getId())
|
||||
|
||||
self.addContainer(extruder_quality_changes_container)
|
||||
|
@ -601,7 +626,7 @@ class CuraContainerRegistry(ContainerRegistry):
|
|||
Logger.log("w", "Could not find quality_changes named [%s] for extruder [%s]",
|
||||
machine_quality_changes.getName(), extruder_stack.getId())
|
||||
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():
|
||||
settable_per_extruder = machine.getProperty(qc_setting_key, "settable_per_extruder")
|
||||
if settable_per_extruder:
|
||||
|
@ -642,7 +667,7 @@ class CuraContainerRegistry(ContainerRegistry):
|
|||
if qc_name not in qc_groups:
|
||||
qc_groups[qc_name] = []
|
||||
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())
|
||||
if quality_changes_container:
|
||||
qc_groups[qc_name].append(quality_changes_container)
|
||||
|
@ -656,7 +681,7 @@ class CuraContainerRegistry(ContainerRegistry):
|
|||
else:
|
||||
qc_dict["global"] = qc
|
||||
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():
|
||||
settable_per_extruder = machine.getProperty(qc_setting_key, "settable_per_extruder")
|
||||
if settable_per_extruder:
|
||||
|
@ -676,7 +701,7 @@ class CuraContainerRegistry(ContainerRegistry):
|
|||
|
||||
return extruder_stack
|
||||
|
||||
def _findQualityChangesContainerInCuraFolder(self, name):
|
||||
def _findQualityChangesContainerInCuraFolder(self, name: str) -> Optional[InstanceContainer]:
|
||||
quality_changes_dir = Resources.getPath(cura.CuraApplication.CuraApplication.ResourceTypes.QualityChangesInstanceContainer)
|
||||
|
||||
instance_container = None
|
||||
|
@ -689,18 +714,18 @@ class CuraContainerRegistry(ContainerRegistry):
|
|||
parser = configparser.ConfigParser(interpolation = None)
|
||||
try:
|
||||
parser.read([file_path])
|
||||
except:
|
||||
# skip, it is not a valid stack file
|
||||
except Exception:
|
||||
# Skip, it is not a valid stack file
|
||||
continue
|
||||
|
||||
if not parser.has_option("general", "name"):
|
||||
continue
|
||||
|
||||
if parser["general"]["name"] == name:
|
||||
# load the container
|
||||
# Load the container
|
||||
container_id = os.path.basename(file_path).replace(".inst.cfg", "")
|
||||
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
|
||||
|
||||
instance_container = InstanceContainer(container_id)
|
||||
|
@ -721,7 +746,7 @@ class CuraContainerRegistry(ContainerRegistry):
|
|||
# due to problems with loading order, some stacks may not have the proper next stack
|
||||
# set after upgrading, because the proper global stack was not yet loaded. This method
|
||||
# makes sure those extruders also get the right stack set.
|
||||
def _connectUpgradedExtruderStacksToMachines(self):
|
||||
def _connectUpgradedExtruderStacksToMachines(self) -> None:
|
||||
extruder_stacks = self.findContainers(container_type = ExtruderStack.ExtruderStack)
|
||||
for extruder_stack in extruder_stacks:
|
||||
if extruder_stack.getNextStack():
|
||||
|
@ -734,7 +759,7 @@ class CuraContainerRegistry(ContainerRegistry):
|
|||
else:
|
||||
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
|
||||
@override(ContainerRegistry)
|
||||
def getInstance(cls, *args, **kwargs) -> "CuraContainerRegistry":
|
||||
|
|
|
@ -87,6 +87,19 @@ class CuraContainerStack(ContainerStack):
|
|||
def qualityChanges(self) -> InstanceContainer:
|
||||
return cast(InstanceContainer, self._containers[_ContainerIndexes.QualityChanges])
|
||||
|
||||
## Set the intent container.
|
||||
#
|
||||
# \param new_intent The new intent container. It is expected to have a "type" metadata entry with the value "intent".
|
||||
def setIntent(self, new_intent: InstanceContainer, postpone_emit: bool = False) -> None:
|
||||
self.replaceContainer(_ContainerIndexes.Intent, new_intent, postpone_emit = postpone_emit)
|
||||
|
||||
## Get the quality container.
|
||||
#
|
||||
# \return The intent container. Should always be a valid container, but can be equal to the empty InstanceContainer.
|
||||
@pyqtProperty(InstanceContainer, fset = setIntent, notify = pyqtContainersChanged)
|
||||
def intent(self) -> InstanceContainer:
|
||||
return cast(InstanceContainer, self._containers[_ContainerIndexes.Intent])
|
||||
|
||||
## Set the quality container.
|
||||
#
|
||||
# \param new_quality The new quality container. It is expected to have a "type" metadata entry with the value "quality".
|
||||
|
@ -330,16 +343,18 @@ class CuraContainerStack(ContainerStack):
|
|||
class _ContainerIndexes:
|
||||
UserChanges = 0
|
||||
QualityChanges = 1
|
||||
Quality = 2
|
||||
Material = 3
|
||||
Variant = 4
|
||||
DefinitionChanges = 5
|
||||
Definition = 6
|
||||
Intent = 2
|
||||
Quality = 3
|
||||
Material = 4
|
||||
Variant = 5
|
||||
DefinitionChanges = 6
|
||||
Definition = 7
|
||||
|
||||
# Simple hash map to map from index to "type" metadata entry
|
||||
IndexTypeMap = {
|
||||
UserChanges: "user",
|
||||
QualityChanges: "quality_changes",
|
||||
Intent: "intent",
|
||||
Quality: "quality",
|
||||
Material: "material",
|
||||
Variant: "variant",
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue