Merge branch 'master' into master

This commit is contained in:
Koub 2019-12-17 14:57:22 +01:00 committed by GitHub
commit 2fcc5d8fd2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2109 changed files with 75029 additions and 55511 deletions

View File

@ -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. 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. 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.
Thank you for using Cura! Thank you for using Cura!
--> -->
**Application Version** **Application version**
<!-- The version of the application this issue occurs with --> (The version of the application this issue occurs with.)
**Platform** **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** **Printer**
<!-- Which printer was selected in Cura. Please attach project file as .curaproject.3mf.zip --> (Which printer was selected in Cura?)
**Steps to Reproduce** **Reproduction steps**
<!-- Add the steps needed that lead up to the issue --> 1. (Something you did.)
2. (Something you did next.)
**Actual Results** **Screenshot(s)**
<!-- What happens after the above steps have been followed --> (Image showing the problem, perhaps before/after images.)
**Actual results**
(What happens after the above steps have been followed.)
**Expected results** **Expected results**
<!-- What should happen after the above steps have been followed --> (What should happen after the above steps have been followed.)
**Additional Information** **Project file**
<!-- Extra information relevant to the issue, like screenshots --> (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.)

View File

@ -8,33 +8,42 @@ 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. 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. Also, please note the application version in the title of the issue. For example: "[3.2.1] Cannot connect to 3rd-party printer". Please do NOT write things like "Request:" or "[BUG]" in the title; this is what labels are for.
It is also helpful to attach a project (.3mf or .curaproject) file and Cura log file so we can debug issues quicker. Information about how to find the log file can be found at https://github.com/Ultimaker/Cura#logging-issues
To upload a project, try changing the extension to e.g. .curaproject.3mf.zip so that GitHub accepts uploading the file. Otherwise, we recommend http://wetransfer.com, but other file hosts like Google Drive or Dropbox work well too.
Thank you for using Cura! Thank you for using Cura!
--> -->
**Application Version** **Application version**
<!-- The version of the application this issue occurs with --> (The version of the application this issue occurs with.)
**Platform** **Platform**
<!-- Information about the operating system the issue occurs on. Include at least the operating system. In the case of visual glitches/issues, also include information about your graphics drivers and GPU. --> (Information about the operating system the issue occurs on. Include at least the operating system and maybe GPU.)
**Printer** **Printer**
<!-- Which printer was selected in Cura? If possible, please attach project file as .curaproject.3mf.zip --> (Which printer was selected in Cura?)
**Actual Results** **Reproduction steps**
<!-- What happens after the above steps have been followed --> 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** **Expected results**
<!-- What should happen after the above steps have been followed --> (What should happen after the above steps have been followed.)
**Additional Information** **Project file**
<!-- Extra information relevant to the issue, like screenshots --> (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.)

View File

@ -8,15 +8,16 @@ assignees: ''
--- ---
**Is your feature request related to a problem? Please describe.** **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 [...] --> (A clear and concise description of what the problem is. Ex. I'm always frustrated when [...])
**Describe the solution you'd like** **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.--> (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** **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. --> (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** **Affected users and/or printers**
<!-- Who do you think will benefit from this? Is everyone going to benefit from these changes? Only a few people? --> (Who do you think will benefit from this? Is everyone going to benefit from these changes? Or specific kinds of users?)
**Additional context** **Additional context**
<!-- Add any other context or screenshots about the feature request here. --> (Add any other context or screenshots about the feature request here.)

13
.github/workflows/cicd.yml vendored Normal file
View 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 Cura
uses: actions/checkout@v2
- name: Build and test
run: docker/build.sh

6
.gitignore vendored
View File

@ -35,7 +35,7 @@ cura.desktop
.pydevproject .pydevproject
.settings .settings
#Externally located plug-ins. #Externally located plug-ins commonly installed by our devs.
plugins/cura-big-flame-graph plugins/cura-big-flame-graph
plugins/cura-god-mode-plugin plugins/cura-god-mode-plugin
plugins/cura-siemensnx-plugin plugins/cura-siemensnx-plugin
@ -52,6 +52,7 @@ plugins/FlatProfileExporter
plugins/GodMode plugins/GodMode
plugins/OctoPrintPlugin plugins/OctoPrintPlugin
plugins/ProfileFlattener plugins/ProfileFlattener
plugins/SettingsGuide
plugins/X3GWriter plugins/X3GWriter
#Build stuff #Build stuff
@ -72,3 +73,6 @@ run.sh
CuraEngine CuraEngine
/.coverage /.coverage
#Prevents import failures when plugin running tests
plugins/__init__.py

View File

@ -1,12 +0,0 @@
image: registry.gitlab.com/ultimaker/cura/cura-build-environment:centos7
stages:
- build
build-and-test:
stage: build
script:
- docker/build.sh
artifacts:
paths:
- build

View File

@ -20,11 +20,12 @@ 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_APP_DISPLAY_NAME "Ultimaker Cura" CACHE STRING "Display name of Cura")
set(CURA_VERSION "master" CACHE STRING "Version name of Cura") set(CURA_VERSION "master" CACHE STRING "Version name of Cura")
set(CURA_BUILDTYPE "" CACHE STRING "Build type of Cura, eg. 'PPA'") set(CURA_BUILDTYPE "" CACHE STRING "Build type of Cura, eg. 'PPA'")
set(CURA_SDK_VERSION "" CACHE STRING "SDK version of Cura")
set(CURA_CLOUD_API_ROOT "" CACHE STRING "Alternative Cura cloud API root") set(CURA_CLOUD_API_ROOT "" CACHE STRING "Alternative Cura cloud API root")
set(CURA_CLOUD_API_VERSION "" CACHE STRING "Alternative Cura cloud API version") set(CURA_CLOUD_API_VERSION "" CACHE STRING "Alternative Cura cloud API version")
set(CURA_CLOUD_ACCOUNT_API_ROOT "" CACHE STRING "Alternative Cura cloud account API version")
configure_file(${CMAKE_SOURCE_DIR}/cura.desktop.in ${CMAKE_BINARY_DIR}/cura.desktop @ONLY) configure_file(${CMAKE_SOURCE_DIR}/cura.desktop.in ${CMAKE_BINARY_DIR}/cura.desktop @ONLY)
configure_file(cura/CuraVersion.py.in CuraVersion.py @ONLY) configure_file(cura/CuraVersion.py.in CuraVersion.py @ONLY)
@ -48,7 +49,7 @@ endif()
if(NOT ${URANIUM_DIR} STREQUAL "") if(NOT ${URANIUM_DIR} STREQUAL "")
set(CMAKE_MODULE_PATH "${URANIUM_DIR}/cmake") set(CMAKE_MODULE_PATH "${CMAKE_MODULE_PATH};${URANIUM_DIR}/cmake")
endif() endif()
if(NOT ${URANIUM_SCRIPTS_DIR} STREQUAL "") if(NOT ${URANIUM_SCRIPTS_DIR} STREQUAL "")
list(APPEND CMAKE_MODULE_PATH ${URANIUM_DIR}/cmake) list(APPEND CMAKE_MODULE_PATH ${URANIUM_DIR}/cmake)
@ -62,8 +63,8 @@ endif()
install(DIRECTORY resources install(DIRECTORY resources
DESTINATION ${CMAKE_INSTALL_DATADIR}/cura) DESTINATION ${CMAKE_INSTALL_DATADIR}/cura)
install(DIRECTORY plugins
DESTINATION lib${LIB_SUFFIX}/cura) include(CuraPluginInstall)
if(NOT APPLE AND NOT WIN32) if(NOT APPLE AND NOT WIN32)
install(FILES cura_app.py install(FILES cura_app.py

View 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()

View File

@ -47,7 +47,7 @@ function(cura_add_test)
if (NOT ${test_exists}) if (NOT ${test_exists})
add_test( add_test(
NAME ${_NAME} NAME ${_NAME}
COMMAND ${Python3_EXECUTABLE} -m pytest --verbose --full-trace --capture=no --no-print-log --junitxml=${CMAKE_BINARY_DIR}/junit-${_NAME}.xml ${_DIRECTORY} COMMAND ${Python3_EXECUTABLE} -m pytest --junitxml=${CMAKE_BINARY_DIR}/junit-${_NAME}.xml ${_DIRECTORY}
) )
set_tests_properties(${_NAME} PROPERTIES ENVIRONMENT LANG=C) set_tests_properties(${_NAME} PROPERTIES ENVIRONMENT LANG=C)
set_tests_properties(${_NAME} PROPERTIES ENVIRONMENT "PYTHONPATH=${_PYTHONPATH}") set_tests_properties(${_NAME} PROPERTIES ENVIRONMENT "PYTHONPATH=${_PYTHONPATH}")

View 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()

View File

@ -13,6 +13,7 @@ TryExec=@CMAKE_INSTALL_FULL_BINDIR@/cura
Icon=cura-icon Icon=cura-icon
Terminal=false Terminal=false
Type=Application Type=Application
MimeType=model/stl;application/vnd.ms-3mfdocument;application/prs.wavefront-obj;image/bmp;image/gif;image/jpeg;image/png;model/x3d+xml;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; Categories=Graphics;
Keywords=3D;Printing;Slicer; Keywords=3D;Printing;Slicer;
StartupWMClass=cura.real

View File

@ -9,7 +9,11 @@ DEFAULT_CURA_DISPLAY_NAME = "Ultimaker Cura"
DEFAULT_CURA_VERSION = "master" DEFAULT_CURA_VERSION = "master"
DEFAULT_CURA_BUILD_TYPE = "" DEFAULT_CURA_BUILD_TYPE = ""
DEFAULT_CURA_DEBUG_MODE = False DEFAULT_CURA_DEBUG_MODE = False
DEFAULT_CURA_SDK_VERSION = "6.1.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: try:
from cura.CuraVersion import CuraAppName # type: ignore from cura.CuraVersion import CuraAppName # type: ignore
@ -18,13 +22,6 @@ try:
except ImportError: except ImportError:
CuraAppName = DEFAULT_CURA_APP_NAME CuraAppName = DEFAULT_CURA_APP_NAME
try:
from cura.CuraVersion import CuraAppDisplayName # type: ignore
if CuraAppDisplayName == "":
CuraAppDisplayName = DEFAULT_CURA_DISPLAY_NAME
except ImportError:
CuraAppDisplayName = DEFAULT_CURA_DISPLAY_NAME
try: try:
from cura.CuraVersion import CuraVersion # type: ignore from cura.CuraVersion import CuraVersion # type: ignore
if CuraVersion == "": if CuraVersion == "":
@ -32,6 +29,9 @@ try:
except ImportError: except ImportError:
CuraVersion = DEFAULT_CURA_VERSION # [CodeStyle: Reflecting imported value] 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: try:
from cura.CuraVersion import CuraBuildType # type: ignore from cura.CuraVersion import CuraBuildType # type: ignore
except ImportError: except ImportError:
@ -42,9 +42,17 @@ try:
except ImportError: except ImportError:
CuraDebugMode = DEFAULT_CURA_DEBUG_MODE CuraDebugMode = DEFAULT_CURA_DEBUG_MODE
# CURA-6569
# Various convenience flags indicating what kind of Cura build it is.
__ENTERPRISE_VERSION_TYPE = "enterprise"
IsEnterpriseVersion = CuraBuildType.lower() == __ENTERPRISE_VERSION_TYPE
try: try:
from cura.CuraVersion import CuraSDKVersion # type: ignore from cura.CuraVersion import CuraAppDisplayName # type: ignore
if CuraSDKVersion == "": if CuraAppDisplayName == "":
CuraSDKVersion = DEFAULT_CURA_SDK_VERSION CuraAppDisplayName = DEFAULT_CURA_DISPLAY_NAME
if IsEnterpriseVersion:
CuraAppDisplayName = CuraAppDisplayName + " Enterprise"
except ImportError: except ImportError:
CuraSDKVersion = DEFAULT_CURA_SDK_VERSION CuraAppDisplayName = DEFAULT_CURA_DISPLAY_NAME

View File

@ -1,12 +1,19 @@
# Copyright (c) 2019 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
import numpy import numpy
import copy import copy
from typing import Optional, Tuple, TYPE_CHECKING
from UM.Math.Polygon import Polygon from UM.Math.Polygon import Polygon
if TYPE_CHECKING:
from UM.Scene.SceneNode import SceneNode
## Polygon representation as an array for use with Arrange ## Polygon representation as an array for use with Arrange
class ShapeArray: class ShapeArray:
def __init__(self, arr, offset_x, offset_y, scale = 1): def __init__(self, arr: numpy.array, offset_x: float, offset_y: float, scale: float = 1) -> None:
self.arr = arr self.arr = arr
self.offset_x = offset_x self.offset_x = offset_x
self.offset_y = offset_y self.offset_y = offset_y
@ -16,7 +23,7 @@ class ShapeArray:
# \param vertices # \param vertices
# \param scale scale the coordinates # \param scale scale the coordinates
@classmethod @classmethod
def fromPolygon(cls, vertices, scale = 1): def fromPolygon(cls, vertices: numpy.array, scale: float = 1) -> "ShapeArray":
# scale # scale
vertices = vertices * scale vertices = vertices * scale
# flip y, x -> x, y # flip y, x -> x, y
@ -42,7 +49,7 @@ class ShapeArray:
# \param min_offset offset for the offset ShapeArray # \param min_offset offset for the offset ShapeArray
# \param scale scale the coordinates # \param scale scale the coordinates
@classmethod @classmethod
def fromNode(cls, node, min_offset, scale = 0.5, 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 = node._transformation
transform_x = transform._data[0][3] transform_x = transform._data[0][3]
transform_y = transform._data[2][3] transform_y = transform._data[2][3]
@ -88,14 +95,16 @@ class ShapeArray:
# \param shape numpy format shape, [x-size, y-size] # \param shape numpy format shape, [x-size, y-size]
# \param vertices # \param vertices
@classmethod @classmethod
def arrayFromPolygon(cls, shape, vertices): def arrayFromPolygon(cls, shape: Tuple[int, int], vertices: numpy.array) -> numpy.array:
base_array = numpy.zeros(shape, dtype = numpy.int32) # Initialize your array of zeros base_array = numpy.zeros(shape, dtype = numpy.int32) # Initialize your array of zeros
fill = numpy.ones(base_array.shape) * True # Initialize boolean array defining shape fill fill = numpy.ones(base_array.shape) * True # Initialize boolean array defining shape fill
# Create check array for each edge segment, combine into fill array # Create check array for each edge segment, combine into fill array
for k in range(vertices.shape[0]): 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 # Set all values inside polygon to one
base_array[fill] = 1 base_array[fill] = 1
@ -111,9 +120,9 @@ class ShapeArray:
# \param p2 2-tuple with x, y for point 2 # \param p2 2-tuple with x, y for point 2
# \param base_array boolean array to project the line on # \param base_array boolean array to project the line on
@classmethod @classmethod
def _check(cls, p1, p2, base_array): def _check(cls, p1: numpy.array, p2: numpy.array, base_array: numpy.array) -> Optional[numpy.array]:
if p1[0] == p2[0] and p1[1] == p2[1]: if p1[0] == p2[0] and p1[1] == p2[1]:
return return None
idxs = numpy.indices(base_array.shape) # Create 3D array of indices idxs = numpy.indices(base_array.shape) # Create 3D array of indices
p1 = p1.astype(float) p1 = p1.astype(float)
@ -131,5 +140,4 @@ class ShapeArray:
max_col_idx = (idxs[0] - p1[0]) / (p2[0] - p1[0]) * (p2[1] - p1[1]) + p1[1] max_col_idx = (idxs[0] - p1[0]) / (p2[0] - p1[0]) * (p2[1] - p1[1]) + p1[1]
sign = numpy.sign(p2[0] - p1[0]) sign = numpy.sign(p2[0] - p1[0])
return idxs[1] * sign <= max_col_idx * sign return idxs[1] * sign <= max_col_idx * sign

View File

@ -1,6 +1,6 @@
# Copyright (c) 2019 Ultimaker B.V. # Copyright (c) 2019 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
from UM.Mesh.MeshData import MeshData
from cura.Scene.CuraSceneNode import CuraSceneNode from cura.Scene.CuraSceneNode import CuraSceneNode
from cura.Settings.ExtruderManager import ExtruderManager from cura.Settings.ExtruderManager import ExtruderManager
from UM.Application import Application #To modify the maximum zoom level. from UM.Application import Application #To modify the maximum zoom level.
@ -20,13 +20,19 @@ from UM.Signal import Signal
from PyQt5.QtCore import QTimer from PyQt5.QtCore import QTimer
from UM.View.RenderBatch import RenderBatch from UM.View.RenderBatch import RenderBatch
from UM.View.GL.OpenGL import OpenGL from UM.View.GL.OpenGL import OpenGL
from cura.Settings.GlobalStack import GlobalStack
catalog = i18nCatalog("cura") catalog = i18nCatalog("cura")
import numpy import numpy
import math import math
import copy
from typing import List, Optional from typing import List, Optional, TYPE_CHECKING, Any, Set, cast, Iterable, Dict
if TYPE_CHECKING:
from cura.CuraApplication import CuraApplication
from cura.Settings.ExtruderStack import ExtruderStack
from UM.Settings.ContainerStack import ContainerStack
# Radius of disallowed area in mm around prime. I.e. how much distance to keep from prime position. # Radius of disallowed area in mm around prime. I.e. how much distance to keep from prime position.
PRIME_CLEARANCE = 6.5 PRIME_CLEARANCE = 6.5
@ -36,45 +42,47 @@ PRIME_CLEARANCE = 6.5
class BuildVolume(SceneNode): class BuildVolume(SceneNode):
raftThicknessChanged = Signal() raftThicknessChanged = Signal()
def __init__(self, application, parent = None): def __init__(self, application: "CuraApplication", parent: Optional[SceneNode] = None) -> None:
super().__init__(parent) super().__init__(parent)
self._application = application self._application = application
self._machine_manager = self._application.getMachineManager() self._machine_manager = self._application.getMachineManager()
self._volume_outline_color = None self._volume_outline_color = None # type: Optional[Color]
self._x_axis_color = None self._x_axis_color = None # type: Optional[Color]
self._y_axis_color = None self._y_axis_color = None # type: Optional[Color]
self._z_axis_color = None self._z_axis_color = None # type: Optional[Color]
self._disallowed_area_color = None self._disallowed_area_color = None # type: Optional[Color]
self._error_area_color = None self._error_area_color = None # type: Optional[Color]
self._width = 0 #type: float self._width = 0 # type: float
self._height = 0 #type: float self._height = 0 # type: float
self._depth = 0 #type: float self._depth = 0 # type: float
self._shape = "" #type: str self._shape = "" # type: str
self._shader = None self._shader = None
self._origin_mesh = None self._origin_mesh = None # type: Optional[MeshData]
self._origin_line_length = 20 self._origin_line_length = 20
self._origin_line_width = 0.5 self._origin_line_width = 1.5
self._enabled = False
self._grid_mesh = None self._grid_mesh = None # type: Optional[MeshData]
self._grid_shader = None self._grid_shader = None
self._disallowed_areas = [] self._disallowed_areas = [] # type: List[Polygon]
self._disallowed_areas_no_brim = [] self._disallowed_areas_no_brim = [] # type: List[Polygon]
self._disallowed_area_mesh = None self._disallowed_area_mesh = None # type: Optional[MeshData]
self._disallowed_area_size = 0.
self._error_areas = [] self._error_areas = [] # type: List[Polygon]
self._error_mesh = None self._error_mesh = None # type: Optional[MeshData]
self.setCalculateBoundingBox(False) self.setCalculateBoundingBox(False)
self._volume_aabb = None self._volume_aabb = None # type: Optional[AxisAlignedBox]
self._raft_thickness = 0.0 self._raft_thickness = 0.0
self._extra_z_clearance = 0.0 self._extra_z_clearance = 0.0
self._adhesion_type = None self._adhesion_type = None # type: Any
self._platform = Platform(self) self._platform = Platform(self)
self._build_volume_message = Message(catalog.i18nc("@info:status", self._build_volume_message = Message(catalog.i18nc("@info:status",
@ -82,7 +90,7 @@ class BuildVolume(SceneNode):
" \"Print Sequence\" setting to prevent the gantry from colliding" " \"Print Sequence\" setting to prevent the gantry from colliding"
" with printed models."), title = catalog.i18nc("@info:title", "Build Volume")) " with printed models."), title = catalog.i18nc("@info:title", "Build Volume"))
self._global_container_stack = None self._global_container_stack = None # type: Optional[GlobalStack]
self._stack_change_timer = QTimer() self._stack_change_timer = QTimer()
self._stack_change_timer.setInterval(100) self._stack_change_timer.setInterval(100)
@ -100,7 +108,7 @@ class BuildVolume(SceneNode):
self._application.getController().getScene().sceneChanged.connect(self._onSceneChanged) self._application.getController().getScene().sceneChanged.connect(self._onSceneChanged)
#Objects loaded at the moment. We are connected to the property changed events of these objects. #Objects loaded at the moment. We are connected to the property changed events of these objects.
self._scene_objects = set() self._scene_objects = set() # type: Set[SceneNode]
self._scene_change_timer = QTimer() self._scene_change_timer = QTimer()
self._scene_change_timer.setInterval(100) self._scene_change_timer.setInterval(100)
@ -117,15 +125,11 @@ class BuildVolume(SceneNode):
# Therefore this works. # Therefore this works.
self._machine_manager.activeQualityChanged.connect(self._onStackChanged) self._machine_manager.activeQualityChanged.connect(self._onStackChanged)
# This should also ways work, and it is semantically more correct,
# but it does not update the disallowed areas after material change
self._machine_manager.activeStackChanged.connect(self._onStackChanged)
# Enable and disable extruder # Enable and disable extruder
self._machine_manager.extruderChanged.connect(self.updateNodeBoundaryCheck) self._machine_manager.extruderChanged.connect(self.updateNodeBoundaryCheck)
# list of settings which were updated # List of settings which were updated
self._changed_settings_since_last_rebuild = [] self._changed_settings_since_last_rebuild = [] # type: List[str]
def _onSceneChanged(self, source): def _onSceneChanged(self, source):
if self._global_container_stack: if self._global_container_stack:
@ -193,7 +197,7 @@ class BuildVolume(SceneNode):
self._disallowed_areas = areas self._disallowed_areas = areas
def render(self, renderer): def render(self, renderer):
if not self.getMeshData(): if not self.getMeshData() or not self.isVisible():
return True return True
if not self._shader: if not self._shader:
@ -219,9 +223,12 @@ class BuildVolume(SceneNode):
## For every sliceable node, update node._outside_buildarea ## For every sliceable node, update node._outside_buildarea
# #
def updateNodeBoundaryCheck(self): def updateNodeBoundaryCheck(self):
if not self._global_container_stack:
return
root = self._application.getController().getScene().getRoot() root = self._application.getController().getScene().getRoot()
nodes = list(BreadthFirstIterator(root)) nodes = cast(List[SceneNode], list(cast(Iterable, BreadthFirstIterator(root))))
group_nodes = [] group_nodes = [] # type: List[SceneNode]
build_volume_bounding_box = self.getBoundingBox() build_volume_bounding_box = self.getBoundingBox()
if build_volume_bounding_box: if build_volume_bounding_box:
@ -240,11 +247,14 @@ class BuildVolume(SceneNode):
group_nodes.append(node) # Keep list of affected group_nodes group_nodes.append(node) # Keep list of affected group_nodes
if node.callDecoration("isSliceable") or node.callDecoration("isGroup"): if node.callDecoration("isSliceable") or node.callDecoration("isGroup"):
if not isinstance(node, CuraSceneNode):
continue
if node.collidesWithBbox(build_volume_bounding_box): if node.collidesWithBbox(build_volume_bounding_box):
node.setOutsideBuildArea(True) node.setOutsideBuildArea(True)
continue continue
if node.collidesWithArea(self.getDisallowedAreas()): if node.collidesWithAreas(self.getDisallowedAreas()):
node.setOutsideBuildArea(True) node.setOutsideBuildArea(True)
continue continue
# If the entire node is below the build plate, still mark it as outside. # If the entire node is below the build plate, still mark it as outside.
@ -254,10 +264,11 @@ class BuildVolume(SceneNode):
continue continue
# Mark the node as outside build volume if the set extruder is disabled # Mark the node as outside build volume if the set extruder is disabled
extruder_position = node.callDecoration("getActiveExtruderPosition") extruder_position = node.callDecoration("getActiveExtruderPosition")
if extruder_position not in self._global_container_stack.extruders: try:
continue if not self._global_container_stack.extruderList[int(extruder_position)].isEnabled:
if not self._global_container_stack.extruders[extruder_position].isEnabled: node.setOutsideBuildArea(True)
node.setOutsideBuildArea(True) continue
except IndexError:
continue continue
node.setOutsideBuildArea(False) node.setOutsideBuildArea(False)
@ -277,8 +288,8 @@ class BuildVolume(SceneNode):
child_node.setOutsideBuildArea(group_node.isOutsideBuildArea()) child_node.setOutsideBuildArea(group_node.isOutsideBuildArea())
## Update the outsideBuildArea of a single node, given bounds or current build volume ## Update the outsideBuildArea of a single node, given bounds or current build volume
def checkBoundsAndUpdate(self, node: CuraSceneNode, bounds: Optional[AxisAlignedBox] = None): def checkBoundsAndUpdate(self, node: CuraSceneNode, bounds: Optional[AxisAlignedBox] = None) -> None:
if not isinstance(node, CuraSceneNode): if not isinstance(node, CuraSceneNode) or self._global_container_stack is None:
return return
if bounds is None: if bounds is None:
@ -298,19 +309,19 @@ class BuildVolume(SceneNode):
node.setOutsideBuildArea(True) node.setOutsideBuildArea(True)
return return
if node.collidesWithArea(self.getDisallowedAreas()): if node.collidesWithAreas(self.getDisallowedAreas()):
node.setOutsideBuildArea(True) node.setOutsideBuildArea(True)
return return
# Mark the node as outside build volume if the set extruder is disabled # Mark the node as outside build volume if the set extruder is disabled
extruder_position = node.callDecoration("getActiveExtruderPosition") extruder_position = node.callDecoration("getActiveExtruderPosition")
if not self._global_container_stack.extruders[extruder_position].isEnabled: if not self._global_container_stack.extruderList[int(extruder_position)].isEnabled:
node.setOutsideBuildArea(True) node.setOutsideBuildArea(True)
return return
node.setOutsideBuildArea(False) node.setOutsideBuildArea(False)
def _buildGridMesh(self, min_w, max_w, min_h, max_h, min_d, max_d, z_fight_distance): def _buildGridMesh(self, min_w: float, max_w: float, min_h: float, max_h: float, min_d: float, max_d:float, z_fight_distance: float) -> MeshData:
mb = MeshBuilder() mb = MeshBuilder()
if self._shape != "elliptic": if self._shape != "elliptic":
# Build plate grid mesh # Build plate grid mesh
@ -346,7 +357,7 @@ class BuildVolume(SceneNode):
mb.setVertexUVCoordinates(n, v[0], v[2] * aspect) mb.setVertexUVCoordinates(n, v[0], v[2] * aspect)
return mb.build().getTransformed(scale_matrix) return mb.build().getTransformed(scale_matrix)
def _buildMesh(self, min_w, max_w, min_h, max_h, min_d, max_d, z_fight_distance): def _buildMesh(self, min_w: float, max_w: float, min_h: float, max_h: float, min_d: float, max_d:float, z_fight_distance: float) -> MeshData:
if self._shape != "elliptic": if self._shape != "elliptic":
# Outline 'cube' of the build volume # Outline 'cube' of the build volume
mb = MeshBuilder() mb = MeshBuilder()
@ -379,7 +390,7 @@ class BuildVolume(SceneNode):
mb.addArc(max_w, Vector.Unit_Y, center = (0, max_h, 0), color = self._volume_outline_color) mb.addArc(max_w, Vector.Unit_Y, center = (0, max_h, 0), color = self._volume_outline_color)
return mb.build().getTransformed(scale_matrix) return mb.build().getTransformed(scale_matrix)
def _buildOriginMesh(self, origin): def _buildOriginMesh(self, origin: Vector) -> MeshData:
mb = MeshBuilder() mb = MeshBuilder()
mb.addCube( mb.addCube(
width=self._origin_line_length, width=self._origin_line_length,
@ -404,22 +415,80 @@ class BuildVolume(SceneNode):
) )
return mb.build() return mb.build()
def _updateColors(self):
theme = self._application.getTheme()
if theme is None:
return
self._volume_outline_color = Color(*theme.getColor("volume_outline").getRgb())
self._x_axis_color = Color(*theme.getColor("x_axis").getRgb())
self._y_axis_color = Color(*theme.getColor("y_axis").getRgb())
self._z_axis_color = Color(*theme.getColor("z_axis").getRgb())
self._disallowed_area_color = Color(*theme.getColor("disallowed_area").getRgb())
self._error_area_color = Color(*theme.getColor("error_area").getRgb())
def _buildErrorMesh(self, min_w: float, max_w: float, min_h: float, max_h: float, min_d: float, max_d: float, disallowed_area_height: float) -> Optional[MeshData]:
if not self._error_areas:
return None
mb = MeshBuilder()
for error_area in self._error_areas:
color = self._error_area_color
points = error_area.getPoints()
first = Vector(self._clamp(points[0][0], min_w, max_w), disallowed_area_height,
self._clamp(points[0][1], min_d, max_d))
previous_point = Vector(self._clamp(points[0][0], min_w, max_w), disallowed_area_height,
self._clamp(points[0][1], min_d, max_d))
for point in points:
new_point = Vector(self._clamp(point[0], min_w, max_w), disallowed_area_height,
self._clamp(point[1], min_d, max_d))
mb.addFace(first, previous_point, new_point, color=color)
previous_point = new_point
return mb.build()
def _buildDisallowedAreaMesh(self, min_w: float, max_w: float, min_h: float, max_h: float, min_d: float, max_d: float, disallowed_area_height: float) -> Optional[MeshData]:
if not self._disallowed_areas:
return None
mb = MeshBuilder()
color = self._disallowed_area_color
for polygon in self._disallowed_areas:
points = polygon.getPoints()
if len(points) == 0:
continue
first = Vector(self._clamp(points[0][0], min_w, max_w), disallowed_area_height,
self._clamp(points[0][1], min_d, max_d))
previous_point = Vector(self._clamp(points[0][0], min_w, max_w), disallowed_area_height,
self._clamp(points[0][1], min_d, max_d))
for point in points:
new_point = Vector(self._clamp(point[0], min_w, max_w), disallowed_area_height,
self._clamp(point[1], min_d, max_d))
mb.addFace(first, previous_point, new_point, color=color)
previous_point = new_point
# Find the largest disallowed area to exclude it from the maximum scale bounds.
# This is a very nasty hack. This pretty much only works for UM machines.
# This disallowed area_size needs a -lot- of rework at some point in the future: TODO
if numpy.min(points[:,
1]) >= 0: # This filters out all areas that have points to the left of the centre. This is done to filter the skirt area.
size = abs(numpy.max(points[:, 1]) - numpy.min(points[:, 1]))
else:
size = 0
self._disallowed_area_size = max(size, self._disallowed_area_size)
return mb.build()
## Recalculates the build volume & disallowed areas. ## Recalculates the build volume & disallowed areas.
def rebuild(self): def rebuild(self) -> None:
if not self._width or not self._height or not self._depth: if not self._width or not self._height or not self._depth:
return return
if not self._engine_ready: if not self._engine_ready:
return return
if not self._global_container_stack:
return
if not self._volume_outline_color: if not self._volume_outline_color:
theme = self._application.getTheme() self._updateColors()
self._volume_outline_color = Color(*theme.getColor("volume_outline").getRgb())
self._x_axis_color = Color(*theme.getColor("x_axis").getRgb())
self._y_axis_color = Color(*theme.getColor("y_axis").getRgb())
self._z_axis_color = Color(*theme.getColor("z_axis").getRgb())
self._disallowed_area_color = Color(*theme.getColor("disallowed_area").getRgb())
self._error_area_color = Color(*theme.getColor("error_area").getRgb())
min_w = -self._width / 2 min_w = -self._width / 2
max_w = self._width / 2 max_w = self._width / 2
@ -442,52 +511,10 @@ class BuildVolume(SceneNode):
self._origin_mesh = self._buildOriginMesh(origin) self._origin_mesh = self._buildOriginMesh(origin)
disallowed_area_height = 0.1 disallowed_area_height = 0.1
disallowed_area_size = 0 self._disallowed_area_size = 0.
if self._disallowed_areas: self._disallowed_area_mesh = self._buildDisallowedAreaMesh(min_w, max_w, min_h, max_h, min_d, max_d, disallowed_area_height)
mb = MeshBuilder()
color = self._disallowed_area_color
for polygon in self._disallowed_areas:
points = polygon.getPoints()
if len(points) == 0:
continue
first = Vector(self._clamp(points[0][0], min_w, max_w), disallowed_area_height, self._clamp(points[0][1], min_d, max_d)) self._error_mesh = self._buildErrorMesh(min_w, max_w, min_h, max_h, min_d, max_d, disallowed_area_height)
previous_point = Vector(self._clamp(points[0][0], min_w, max_w), disallowed_area_height, self._clamp(points[0][1], min_d, max_d))
for point in points:
new_point = Vector(self._clamp(point[0], min_w, max_w), disallowed_area_height, self._clamp(point[1], min_d, max_d))
mb.addFace(first, previous_point, new_point, color = color)
previous_point = new_point
# Find the largest disallowed area to exclude it from the maximum scale bounds.
# This is a very nasty hack. This pretty much only works for UM machines.
# This disallowed area_size needs a -lot- of rework at some point in the future: TODO
if numpy.min(points[:, 1]) >= 0: # This filters out all areas that have points to the left of the centre. This is done to filter the skirt area.
size = abs(numpy.max(points[:, 1]) - numpy.min(points[:, 1]))
else:
size = 0
disallowed_area_size = max(size, disallowed_area_size)
self._disallowed_area_mesh = mb.build()
else:
self._disallowed_area_mesh = None
if self._error_areas:
mb = MeshBuilder()
for error_area in self._error_areas:
color = self._error_area_color
points = error_area.getPoints()
first = Vector(self._clamp(points[0][0], min_w, max_w), disallowed_area_height,
self._clamp(points[0][1], min_d, max_d))
previous_point = Vector(self._clamp(points[0][0], min_w, max_w), disallowed_area_height,
self._clamp(points[0][1], min_d, max_d))
for point in points:
new_point = Vector(self._clamp(point[0], min_w, max_w), disallowed_area_height,
self._clamp(point[1], min_d, max_d))
mb.addFace(first, previous_point, new_point, color=color)
previous_point = new_point
self._error_mesh = mb.build()
else:
self._error_mesh = None
self._volume_aabb = AxisAlignedBox( self._volume_aabb = AxisAlignedBox(
minimum = Vector(min_w, min_h - 1.0, min_d), minimum = Vector(min_w, min_h - 1.0, min_d),
@ -499,23 +526,26 @@ class BuildVolume(SceneNode):
# This is probably wrong in all other cases. TODO! # This is probably wrong in all other cases. TODO!
# The +1 and -1 is added as there is always a bit of extra room required to work properly. # The +1 and -1 is added as there is always a bit of extra room required to work properly.
scale_to_max_bounds = AxisAlignedBox( scale_to_max_bounds = AxisAlignedBox(
minimum = Vector(min_w + bed_adhesion_size + 1, min_h, min_d + disallowed_area_size - bed_adhesion_size + 1), minimum = Vector(min_w + bed_adhesion_size + 1, min_h, min_d + self._disallowed_area_size - bed_adhesion_size + 1),
maximum = Vector(max_w - bed_adhesion_size - 1, max_h - self._raft_thickness - self._extra_z_clearance, max_d - disallowed_area_size + bed_adhesion_size - 1) maximum = Vector(max_w - bed_adhesion_size - 1, max_h - self._raft_thickness - self._extra_z_clearance, max_d - self._disallowed_area_size + bed_adhesion_size - 1)
) )
self._application.getController().getScene()._maximum_bounds = scale_to_max_bounds self._application.getController().getScene()._maximum_bounds = scale_to_max_bounds # type: ignore
self.updateNodeBoundaryCheck() self.updateNodeBoundaryCheck()
def getBoundingBox(self) -> AxisAlignedBox: def getBoundingBox(self):
return self._volume_aabb return self._volume_aabb
def getRaftThickness(self) -> float: def getRaftThickness(self) -> float:
return self._raft_thickness return self._raft_thickness
def _updateRaftThickness(self): def _updateRaftThickness(self) -> None:
if not self._global_container_stack:
return
old_raft_thickness = self._raft_thickness old_raft_thickness = self._raft_thickness
if self._global_container_stack.extruders: if self._global_container_stack.extruderList:
# This might be called before the extruder stacks have initialised, in which case getting the adhesion_type fails # This might be called before the extruder stacks have initialised, in which case getting the adhesion_type fails
self._adhesion_type = self._global_container_stack.getProperty("adhesion_type", "value") self._adhesion_type = self._global_container_stack.getProperty("adhesion_type", "value")
self._raft_thickness = 0.0 self._raft_thickness = 0.0
@ -524,7 +554,7 @@ class BuildVolume(SceneNode):
self._global_container_stack.getProperty("raft_base_thickness", "value") + self._global_container_stack.getProperty("raft_base_thickness", "value") +
self._global_container_stack.getProperty("raft_interface_thickness", "value") + self._global_container_stack.getProperty("raft_interface_thickness", "value") +
self._global_container_stack.getProperty("raft_surface_layers", "value") * self._global_container_stack.getProperty("raft_surface_layers", "value") *
self._global_container_stack.getProperty("raft_surface_thickness", "value") + self._global_container_stack.getProperty("raft_surface_thickness", "value") +
self._global_container_stack.getProperty("raft_airgap", "value") - self._global_container_stack.getProperty("raft_airgap", "value") -
self._global_container_stack.getProperty("layer_0_z_overlap", "value")) self._global_container_stack.getProperty("layer_0_z_overlap", "value"))
@ -533,28 +563,23 @@ class BuildVolume(SceneNode):
self.setPosition(Vector(0, -self._raft_thickness, 0), SceneNode.TransformSpace.World) self.setPosition(Vector(0, -self._raft_thickness, 0), SceneNode.TransformSpace.World)
self.raftThicknessChanged.emit() self.raftThicknessChanged.emit()
def _updateExtraZClearance(self) -> None: def _calculateExtraZClearance(self, extruders: List["ContainerStack"]) -> float:
if not self._global_container_stack:
return 0
extra_z = 0.0 extra_z = 0.0
extruders = ExtruderManager.getInstance().getUsedExtruderStacks()
use_extruders = False
for extruder in extruders: for extruder in extruders:
if extruder.getProperty("retraction_hop_enabled", "value"): if extruder.getProperty("retraction_hop_enabled", "value"):
retraction_hop = extruder.getProperty("retraction_hop", "value") retraction_hop = extruder.getProperty("retraction_hop", "value")
if extra_z is None or retraction_hop > extra_z: if extra_z is None or retraction_hop > extra_z:
extra_z = retraction_hop extra_z = retraction_hop
use_extruders = True return extra_z
if not use_extruders:
# If no extruders, take global value.
if self._global_container_stack.getProperty("retraction_hop_enabled", "value"):
extra_z = self._global_container_stack.getProperty("retraction_hop", "value")
if extra_z != self._extra_z_clearance:
self._extra_z_clearance = extra_z
def _onStackChanged(self): def _onStackChanged(self):
self._stack_change_timer.start() self._stack_change_timer.start()
## Update the build volume visualization ## Update the build volume visualization
def _onStackChangeTimerFinished(self): def _onStackChangeTimerFinished(self) -> None:
if self._global_container_stack: if self._global_container_stack:
self._global_container_stack.propertyChanged.disconnect(self._onSettingPropertyChanged) self._global_container_stack.propertyChanged.disconnect(self._onSettingPropertyChanged)
extruders = ExtruderManager.getInstance().getActiveExtruderStacks() extruders = ExtruderManager.getInstance().getActiveExtruderStacks()
@ -585,7 +610,7 @@ class BuildVolume(SceneNode):
self._updateDisallowedAreas() self._updateDisallowedAreas()
self._updateRaftThickness() self._updateRaftThickness()
self._updateExtraZClearance() self._extra_z_clearance = self._calculateExtraZClearance(ExtruderManager.getInstance().getUsedExtruderStacks())
if self._engine_ready: if self._engine_ready:
self.rebuild() self.rebuild()
@ -594,20 +619,23 @@ class BuildVolume(SceneNode):
if camera: if camera:
diagonal = self.getDiagonalSize() diagonal = self.getDiagonalSize()
if diagonal > 1: if diagonal > 1:
camera.setZoomRange(min = 0.1, max = diagonal * 5) #You can zoom out up to 5 times the diagonal. This gives some space around the volume. # You can zoom out up to 5 times the diagonal. This gives some space around the volume.
camera.setZoomRange(min = 0.1, max = diagonal * 5) # type: ignore
def _onEngineCreated(self): def _onEngineCreated(self) -> None:
self._engine_ready = True self._engine_ready = True
self.rebuild() self.rebuild()
def _onSettingChangeTimerFinished(self): def _onSettingChangeTimerFinished(self) -> None:
if not self._global_container_stack:
return
rebuild_me = False rebuild_me = False
update_disallowed_areas = False update_disallowed_areas = False
update_raft_thickness = False update_raft_thickness = False
update_extra_z_clearance = True update_extra_z_clearance = True
for setting_key in self._changed_settings_since_last_rebuild: for setting_key in self._changed_settings_since_last_rebuild:
if setting_key == "print_sequence": if setting_key == "print_sequence":
machine_height = self._global_container_stack.getProperty("machine_height", "value") machine_height = self._global_container_stack.getProperty("machine_height", "value")
if self._application.getGlobalContainerStack().getProperty("print_sequence", "value") == "one_at_a_time" and len(self._scene_objects) > 1: if self._application.getGlobalContainerStack().getProperty("print_sequence", "value") == "one_at_a_time" and len(self._scene_objects) > 1:
@ -620,33 +648,26 @@ class BuildVolume(SceneNode):
self._height = self._global_container_stack.getProperty("machine_height", "value") self._height = self._global_container_stack.getProperty("machine_height", "value")
self._build_volume_message.hide() self._build_volume_message.hide()
update_disallowed_areas = True update_disallowed_areas = True
rebuild_me = True
# sometimes the machine size or shape settings are adjusted on the active machine, we should reflect this # sometimes the machine size or shape settings are adjusted on the active machine, we should reflect this
if setting_key in self._machine_settings: if setting_key in self._machine_settings:
self._height = self._global_container_stack.getProperty("machine_height", "value") self._updateMachineSizeProperties()
self._width = self._global_container_stack.getProperty("machine_width", "value")
self._depth = self._global_container_stack.getProperty("machine_depth", "value")
self._shape = self._global_container_stack.getProperty("machine_shape", "value")
update_extra_z_clearance = True update_extra_z_clearance = True
update_disallowed_areas = True update_disallowed_areas = True
rebuild_me = True
if setting_key in self._skirt_settings + self._prime_settings + self._tower_settings + self._ooze_shield_settings + self._distance_settings + self._extruder_settings: if setting_key in self._disallowed_area_settings:
update_disallowed_areas = True update_disallowed_areas = True
rebuild_me = True
if setting_key in self._raft_settings: if setting_key in self._raft_settings:
update_raft_thickness = True update_raft_thickness = True
rebuild_me = True
if setting_key in self._extra_z_settings: if setting_key in self._extra_z_settings:
update_extra_z_clearance = True update_extra_z_clearance = True
rebuild_me = True
if setting_key in self._limit_to_extruder_settings: if setting_key in self._limit_to_extruder_settings:
update_disallowed_areas = True update_disallowed_areas = True
rebuild_me = True
rebuild_me = update_extra_z_clearance or update_disallowed_areas or update_raft_thickness
# We only want to update all of them once. # We only want to update all of them once.
if update_disallowed_areas: if update_disallowed_areas:
@ -656,7 +677,7 @@ class BuildVolume(SceneNode):
self._updateRaftThickness() self._updateRaftThickness()
if update_extra_z_clearance: if update_extra_z_clearance:
self._updateExtraZClearance() self._extra_z_clearance = self._calculateExtraZClearance(ExtruderManager.getInstance().getUsedExtruderStacks())
if rebuild_me: if rebuild_me:
self.rebuild() self.rebuild()
@ -664,7 +685,7 @@ class BuildVolume(SceneNode):
# We just did a rebuild, reset the list. # We just did a rebuild, reset the list.
self._changed_settings_since_last_rebuild = [] self._changed_settings_since_last_rebuild = []
def _onSettingPropertyChanged(self, setting_key: str, property_name: str): def _onSettingPropertyChanged(self, setting_key: str, property_name: str) -> None:
if property_name != "value": if property_name != "value":
return return
@ -675,6 +696,14 @@ class BuildVolume(SceneNode):
def hasErrors(self) -> bool: def hasErrors(self) -> bool:
return self._has_errors return self._has_errors
def _updateMachineSizeProperties(self) -> None:
if not self._global_container_stack:
return
self._height = self._global_container_stack.getProperty("machine_height", "value")
self._width = self._global_container_stack.getProperty("machine_width", "value")
self._depth = self._global_container_stack.getProperty("machine_depth", "value")
self._shape = self._global_container_stack.getProperty("machine_shape", "value")
## Calls _updateDisallowedAreas and makes sure the changes appear in the ## Calls _updateDisallowedAreas and makes sure the changes appear in the
# scene. # scene.
# #
@ -686,57 +715,26 @@ class BuildVolume(SceneNode):
def _updateDisallowedAreasAndRebuild(self): def _updateDisallowedAreasAndRebuild(self):
self._updateDisallowedAreas() self._updateDisallowedAreas()
self._updateRaftThickness() self._updateRaftThickness()
self._updateExtraZClearance() self._extra_z_clearance = self._calculateExtraZClearance(ExtruderManager.getInstance().getUsedExtruderStacks())
self.rebuild() self.rebuild()
def _updateDisallowedAreas(self): def _updateDisallowedAreas(self) -> None:
if not self._global_container_stack: if not self._global_container_stack:
return return
self._error_areas = [] self._error_areas = []
extruder_manager = ExtruderManager.getInstance() used_extruders = ExtruderManager.getInstance().getUsedExtruderStacks()
used_extruders = extruder_manager.getUsedExtruderStacks()
disallowed_border_size = self.getEdgeDisallowedSize() disallowed_border_size = self.getEdgeDisallowedSize()
if not used_extruders: result_areas = self._computeDisallowedAreasStatic(disallowed_border_size, used_extruders) # Normal machine disallowed areas can always be added.
# If no extruder is used, assume that the active extruder is used (else nothing is drawn)
if extruder_manager.getActiveExtruderStack():
used_extruders = [extruder_manager.getActiveExtruderStack()]
else:
used_extruders = [self._global_container_stack]
result_areas = self._computeDisallowedAreasStatic(disallowed_border_size, used_extruders) #Normal machine disallowed areas can always be added.
prime_areas = self._computeDisallowedAreasPrimeBlob(disallowed_border_size, used_extruders) prime_areas = self._computeDisallowedAreasPrimeBlob(disallowed_border_size, used_extruders)
result_areas_no_brim = self._computeDisallowedAreasStatic(0, used_extruders) #Where the priming is not allowed to happen. This is not added to the result, just for collision checking. result_areas_no_brim = self._computeDisallowedAreasStatic(0, used_extruders) # Where the priming is not allowed to happen. This is not added to the result, just for collision checking.
prime_disallowed_areas = copy.deepcopy(result_areas_no_brim)
#Check if prime positions intersect with disallowed areas. # Check if prime positions intersect with disallowed areas.
for extruder in used_extruders: for extruder in used_extruders:
extruder_id = extruder.getId() extruder_id = extruder.getId()
collision = False
for prime_polygon in prime_areas[extruder_id]:
for disallowed_polygon in prime_disallowed_areas[extruder_id]:
if prime_polygon.intersectsPolygon(disallowed_polygon) is not None:
collision = True
break
if collision:
break
#Also check other prime positions (without additional offset).
for other_extruder_id in prime_areas:
if extruder_id == other_extruder_id: #It is allowed to collide with itself.
continue
for other_prime_polygon in prime_areas[other_extruder_id]:
if prime_polygon.intersectsPolygon(other_prime_polygon):
collision = True
break
if collision:
break
if collision:
break
result_areas[extruder_id].extend(prime_areas[extruder_id]) result_areas[extruder_id].extend(prime_areas[extruder_id])
result_areas_no_brim[extruder_id].extend(prime_areas[extruder_id]) result_areas_no_brim[extruder_id].extend(prime_areas[extruder_id])
@ -744,33 +742,28 @@ class BuildVolume(SceneNode):
for area in nozzle_disallowed_areas: for area in nozzle_disallowed_areas:
polygon = Polygon(numpy.array(area, numpy.float32)) polygon = Polygon(numpy.array(area, numpy.float32))
polygon_disallowed_border = polygon.getMinkowskiHull(Polygon.approximatedCircle(disallowed_border_size)) polygon_disallowed_border = polygon.getMinkowskiHull(Polygon.approximatedCircle(disallowed_border_size))
result_areas[extruder_id].append(polygon_disallowed_border) #Don't perform the offset on these. result_areas[extruder_id].append(polygon_disallowed_border) # Don't perform the offset on these.
#polygon_minimal_border = polygon.getMinkowskiHull(5) result_areas_no_brim[extruder_id].append(polygon) # No brim
result_areas_no_brim[extruder_id].append(polygon) # no brim
# Add prime tower location as disallowed area. # Add prime tower location as disallowed area.
if len(used_extruders) > 1: #No prime tower in single-extrusion. if len([x for x in used_extruders if x.isEnabled]) > 1: # No prime tower if only one extruder is enabled
prime_tower_collision = False
if len([x for x in used_extruders if x.isEnabled]) > 1: #No prime tower if only one extruder is enabled prime_tower_areas = self._computeDisallowedAreasPrinted(used_extruders)
prime_tower_collision = False for extruder_id in prime_tower_areas:
prime_tower_areas = self._computeDisallowedAreasPrinted(used_extruders) for area_index, prime_tower_area in enumerate(prime_tower_areas[extruder_id]):
for extruder_id in prime_tower_areas: for area in result_areas[extruder_id]:
for i_area, prime_tower_area in enumerate(prime_tower_areas[extruder_id]): if prime_tower_area.intersectsPolygon(area) is not None:
for area in result_areas[extruder_id]: prime_tower_collision = True
if prime_tower_area.intersectsPolygon(area) is not None:
prime_tower_collision = True
break
if prime_tower_collision: #Already found a collision.
break break
if (ExtruderManager.getInstance().getResolveOrValue("prime_tower_brim_enable") and if prime_tower_collision: # Already found a collision.
ExtruderManager.getInstance().getResolveOrValue("adhesion_type") != "raft"): break
prime_tower_areas[extruder_id][i_area] = prime_tower_area.getMinkowskiHull( if self._global_container_stack.getProperty("prime_tower_brim_enable", "value") and self._global_container_stack.getProperty("adhesion_type", "value") != "raft":
Polygon.approximatedCircle(disallowed_border_size)) prime_tower_areas[extruder_id][area_index] = prime_tower_area.getMinkowskiHull(Polygon.approximatedCircle(disallowed_border_size))
if not prime_tower_collision: if not prime_tower_collision:
result_areas[extruder_id].extend(prime_tower_areas[extruder_id]) result_areas[extruder_id].extend(prime_tower_areas[extruder_id])
result_areas_no_brim[extruder_id].extend(prime_tower_areas[extruder_id]) result_areas_no_brim[extruder_id].extend(prime_tower_areas[extruder_id])
else: else:
self._error_areas.extend(prime_tower_areas[extruder_id]) self._error_areas.extend(prime_tower_areas[extruder_id])
self._has_errors = len(self._error_areas) > 0 self._has_errors = len(self._error_areas) > 0
@ -791,11 +784,14 @@ class BuildVolume(SceneNode):
# where that extruder may not print. # where that extruder may not print.
def _computeDisallowedAreasPrinted(self, used_extruders): def _computeDisallowedAreasPrinted(self, used_extruders):
result = {} result = {}
adhesion_extruder = None #type: ExtruderStack
for extruder in used_extruders: for extruder in used_extruders:
if int(extruder.getProperty("extruder_nr", "value")) == int(self._global_container_stack.getProperty("adhesion_extruder_nr", "value")):
adhesion_extruder = extruder
result[extruder.getId()] = [] result[extruder.getId()] = []
#Currently, the only normally printed object is the prime tower. # Currently, the only normally printed object is the prime tower.
if ExtruderManager.getInstance().getResolveOrValue("prime_tower_enable"): if self._global_container_stack.getProperty("prime_tower_enable", "value"):
prime_tower_size = self._global_container_stack.getProperty("prime_tower_size", "value") prime_tower_size = self._global_container_stack.getProperty("prime_tower_size", "value")
machine_width = self._global_container_stack.getProperty("machine_width", "value") machine_width = self._global_container_stack.getProperty("machine_width", "value")
machine_depth = self._global_container_stack.getProperty("machine_depth", "value") machine_depth = self._global_container_stack.getProperty("machine_depth", "value")
@ -805,27 +801,19 @@ class BuildVolume(SceneNode):
prime_tower_x = prime_tower_x - machine_width / 2 #Offset by half machine_width and _depth to put the origin in the front-left. prime_tower_x = prime_tower_x - machine_width / 2 #Offset by half machine_width and _depth to put the origin in the front-left.
prime_tower_y = prime_tower_y + machine_depth / 2 prime_tower_y = prime_tower_y + machine_depth / 2
if (ExtruderManager.getInstance().getResolveOrValue("prime_tower_brim_enable") and if adhesion_extruder is not None and self._global_container_stack.getProperty("prime_tower_brim_enable", "value") and self._global_container_stack.getProperty("adhesion_type", "value") != "raft":
ExtruderManager.getInstance().getResolveOrValue("adhesion_type") != "raft"):
brim_size = ( brim_size = (
extruder.getProperty("brim_line_count", "value") * adhesion_extruder.getProperty("brim_line_count", "value") *
extruder.getProperty("skirt_brim_line_width", "value") / 100.0 * adhesion_extruder.getProperty("skirt_brim_line_width", "value") / 100.0 *
extruder.getProperty("initial_layer_line_width_factor", "value") adhesion_extruder.getProperty("initial_layer_line_width_factor", "value")
) )
prime_tower_x -= brim_size prime_tower_x -= brim_size
prime_tower_y += brim_size prime_tower_y += brim_size
if self._global_container_stack.getProperty("prime_tower_circular", "value"): radius = prime_tower_size / 2
radius = prime_tower_size / 2 prime_tower_area = Polygon.approximatedCircle(radius)
prime_tower_area = Polygon.approximatedCircle(radius) prime_tower_area = prime_tower_area.translate(prime_tower_x - radius, prime_tower_y - radius)
prime_tower_area = prime_tower_area.translate(prime_tower_x - radius, prime_tower_y - radius)
else:
prime_tower_area = Polygon([
[prime_tower_x - prime_tower_size, prime_tower_y - prime_tower_size],
[prime_tower_x, prime_tower_y - prime_tower_size],
[prime_tower_x, prime_tower_y],
[prime_tower_x - prime_tower_size, prime_tower_y],
])
prime_tower_area = prime_tower_area.getMinkowskiHull(Polygon.approximatedCircle(0)) prime_tower_area = prime_tower_area.getMinkowskiHull(Polygon.approximatedCircle(0))
for extruder in used_extruders: for extruder in used_extruders:
result[extruder.getId()].append(prime_tower_area) #The prime tower location is the same for each extruder, regardless of offset. result[extruder.getId()].append(prime_tower_area) #The prime tower location is the same for each extruder, regardless of offset.
@ -843,9 +831,10 @@ class BuildVolume(SceneNode):
# \param used_extruders The extruder stacks to generate disallowed areas # \param used_extruders The extruder stacks to generate disallowed areas
# for. # for.
# \return A dictionary with for each used extruder ID the prime areas. # \return A dictionary with for each used extruder ID the prime areas.
def _computeDisallowedAreasPrimeBlob(self, border_size, used_extruders): def _computeDisallowedAreasPrimeBlob(self, border_size: float, used_extruders: List["ExtruderStack"]) -> Dict[str, List[Polygon]]:
result = {} result = {} # type: Dict[str, List[Polygon]]
if not self._global_container_stack:
return result
machine_width = self._global_container_stack.getProperty("machine_width", "value") machine_width = self._global_container_stack.getProperty("machine_width", "value")
machine_depth = self._global_container_stack.getProperty("machine_depth", "value") machine_depth = self._global_container_stack.getProperty("machine_depth", "value")
for extruder in used_extruders: for extruder in used_extruders:
@ -853,13 +842,13 @@ class BuildVolume(SceneNode):
prime_x = extruder.getProperty("extruder_prime_pos_x", "value") prime_x = extruder.getProperty("extruder_prime_pos_x", "value")
prime_y = -extruder.getProperty("extruder_prime_pos_y", "value") prime_y = -extruder.getProperty("extruder_prime_pos_y", "value")
#Ignore extruder prime position if it is not set or if blob is disabled # Ignore extruder prime position if it is not set or if blob is disabled
if (prime_x == 0 and prime_y == 0) or not prime_blob_enabled: if (prime_x == 0 and prime_y == 0) or not prime_blob_enabled:
result[extruder.getId()] = [] result[extruder.getId()] = []
continue continue
if not self._global_container_stack.getProperty("machine_center_is_zero", "value"): if not self._global_container_stack.getProperty("machine_center_is_zero", "value"):
prime_x = prime_x - machine_width / 2 #Offset by half machine_width and _depth to put the origin in the front-left. prime_x = prime_x - machine_width / 2 # Offset by half machine_width and _depth to put the origin in the front-left.
prime_y = prime_y + machine_depth / 2 prime_y = prime_y + machine_depth / 2
prime_polygon = Polygon.approximatedCircle(PRIME_CLEARANCE) prime_polygon = Polygon.approximatedCircle(PRIME_CLEARANCE)
@ -882,9 +871,12 @@ class BuildVolume(SceneNode):
# for. # for.
# \return A dictionary with for each used extruder ID the disallowed areas # \return A dictionary with for each used extruder ID the disallowed areas
# where that extruder may not print. # where that extruder may not print.
def _computeDisallowedAreasStatic(self, border_size, used_extruders): def _computeDisallowedAreasStatic(self, border_size:float, used_extruders: List["ExtruderStack"]) -> Dict[str, List[Polygon]]:
#Convert disallowed areas to polygons and dilate them. # Convert disallowed areas to polygons and dilate them.
machine_disallowed_polygons = [] machine_disallowed_polygons = []
if self._global_container_stack is None:
return {}
for area in self._global_container_stack.getProperty("machine_disallowed_areas", "value"): for area in self._global_container_stack.getProperty("machine_disallowed_areas", "value"):
polygon = Polygon(numpy.array(area, numpy.float32)) polygon = Polygon(numpy.array(area, numpy.float32))
polygon = polygon.getMinkowskiHull(Polygon.approximatedCircle(border_size)) polygon = polygon.getMinkowskiHull(Polygon.approximatedCircle(border_size))
@ -895,7 +887,7 @@ class BuildVolume(SceneNode):
nozzle_offsetting_for_disallowed_areas = self._global_container_stack.getMetaDataEntry( nozzle_offsetting_for_disallowed_areas = self._global_container_stack.getMetaDataEntry(
"nozzle_offsetting_for_disallowed_areas", True) "nozzle_offsetting_for_disallowed_areas", True)
result = {} result = {} # type: Dict[str, List[Polygon]]
for extruder in used_extruders: for extruder in used_extruders:
extruder_id = extruder.getId() extruder_id = extruder.getId()
offset_x = extruder.getProperty("machine_nozzle_offset_x", "value") offset_x = extruder.getProperty("machine_nozzle_offset_x", "value")
@ -904,13 +896,13 @@ class BuildVolume(SceneNode):
offset_y = extruder.getProperty("machine_nozzle_offset_y", "value") offset_y = extruder.getProperty("machine_nozzle_offset_y", "value")
if offset_y is None: if offset_y is None:
offset_y = 0 offset_y = 0
offset_y = -offset_y #Y direction of g-code is the inverse of Y direction of Cura's scene space. offset_y = -offset_y # Y direction of g-code is the inverse of Y direction of Cura's scene space.
result[extruder_id] = [] result[extruder_id] = []
for polygon in machine_disallowed_polygons: for polygon in machine_disallowed_polygons:
result[extruder_id].append(polygon.translate(offset_x, offset_y)) #Compensate for the nozzle offset of this extruder. result[extruder_id].append(polygon.translate(offset_x, offset_y)) # Compensate for the nozzle offset of this extruder.
#Add the border around the edge of the build volume. # Add the border around the edge of the build volume.
left_unreachable_border = 0 left_unreachable_border = 0
right_unreachable_border = 0 right_unreachable_border = 0
top_unreachable_border = 0 top_unreachable_border = 0
@ -918,7 +910,8 @@ class BuildVolume(SceneNode):
# Only do nozzle offsetting if needed # Only do nozzle offsetting if needed
if nozzle_offsetting_for_disallowed_areas: if nozzle_offsetting_for_disallowed_areas:
#The build volume is defined as the union of the area that all extruders can reach, so we need to know the relative offset to all extruders. # The build volume is defined as the union of the area that all extruders can reach, so we need to know
# the relative offset to all extruders.
for other_extruder in ExtruderManager.getInstance().getActiveExtruderStacks(): for other_extruder in ExtruderManager.getInstance().getActiveExtruderStacks():
other_offset_x = other_extruder.getProperty("machine_nozzle_offset_x", "value") other_offset_x = other_extruder.getProperty("machine_nozzle_offset_x", "value")
if other_offset_x is None: if other_offset_x is None:
@ -1002,8 +995,8 @@ class BuildVolume(SceneNode):
[ half_machine_width - border_size, 0] [ half_machine_width - border_size, 0]
], numpy.float32))) ], numpy.float32)))
result[extruder_id].append(Polygon(numpy.array([ result[extruder_id].append(Polygon(numpy.array([
[ half_machine_width,-half_machine_depth], [ half_machine_width, -half_machine_depth],
[-half_machine_width,-half_machine_depth], [-half_machine_width, -half_machine_depth],
[ 0, -half_machine_depth + border_size] [ 0, -half_machine_depth + border_size]
], numpy.float32))) ], numpy.float32)))
@ -1015,36 +1008,24 @@ class BuildVolume(SceneNode):
# stack. # stack.
# #
# \return A sequence of setting values, one for each extruder. # \return A sequence of setting values, one for each extruder.
def _getSettingFromAllExtruders(self, setting_key): def _getSettingFromAllExtruders(self, setting_key: str) -> List[Any]:
all_values = ExtruderManager.getInstance().getAllExtruderSettings(setting_key, "value") all_values = ExtruderManager.getInstance().getAllExtruderSettings(setting_key, "value")
all_types = ExtruderManager.getInstance().getAllExtruderSettings(setting_key, "type") all_types = ExtruderManager.getInstance().getAllExtruderSettings(setting_key, "type")
for i in range(len(all_values)): for i, (setting_value, setting_type) in enumerate(zip(all_values, all_types)):
if not all_values[i] and (all_types[i] == "int" or all_types[i] == "float"): if not setting_value and (setting_type == "int" or setting_type == "float"):
all_values[i] = 0 all_values[i] = 0
return all_values return all_values
## Calculate the disallowed radius around the edge. def _calculateBedAdhesionSize(self, used_extruders):
# if self._global_container_stack is None:
# This disallowed radius is to allow for space around the models that is return
# not part of the collision radius, such as bed adhesion (skirt/brim/raft)
# and travel avoid distance.
def getEdgeDisallowedSize(self):
if not self._global_container_stack or not self._global_container_stack.extruders:
return 0
container_stack = self._global_container_stack container_stack = self._global_container_stack
used_extruders = ExtruderManager.getInstance().getUsedExtruderStacks()
# If we are printing one at a time, we need to add the bed adhesion size to the disallowed areas of the objects
if container_stack.getProperty("print_sequence", "value") == "one_at_a_time":
return 0.1 # Return a very small value, so we do draw disallowed area's near the edges.
adhesion_type = container_stack.getProperty("adhesion_type", "value") adhesion_type = container_stack.getProperty("adhesion_type", "value")
skirt_brim_line_width = self._global_container_stack.getProperty("skirt_brim_line_width", "value") skirt_brim_line_width = self._global_container_stack.getProperty("skirt_brim_line_width", "value")
initial_layer_line_width_factor = self._global_container_stack.getProperty("initial_layer_line_width_factor", "value") initial_layer_line_width_factor = self._global_container_stack.getProperty("initial_layer_line_width_factor", "value")
#Use brim width if brim is enabled OR the prime tower has a brim. # Use brim width if brim is enabled OR the prime tower has a brim.
if adhesion_type == "brim" or (self._global_container_stack.getProperty("prime_tower_brim_enable", "value") and if adhesion_type == "brim" or (self._global_container_stack.getProperty("prime_tower_brim_enable", "value") and adhesion_type != "raft"):
adhesion_type != "raft"):
brim_line_count = self._global_container_stack.getProperty("brim_line_count", "value") brim_line_count = self._global_container_stack.getProperty("brim_line_count", "value")
bed_adhesion_size = skirt_brim_line_width * brim_line_count * initial_layer_line_width_factor / 100.0 bed_adhesion_size = skirt_brim_line_width * brim_line_count * initial_layer_line_width_factor / 100.0
@ -1053,11 +1034,12 @@ class BuildVolume(SceneNode):
# We don't create an additional line for the extruder we're printing the brim with. # We don't create an additional line for the extruder we're printing the brim with.
bed_adhesion_size -= skirt_brim_line_width * initial_layer_line_width_factor / 100.0 bed_adhesion_size -= skirt_brim_line_width * initial_layer_line_width_factor / 100.0
elif adhesion_type == "skirt": #No brim? Also not on prime tower? Then use whatever the adhesion type is saying: Skirt, raft or none. elif adhesion_type == "skirt": # No brim? Also not on prime tower? Then use whatever the adhesion type is saying: Skirt, raft or none.
skirt_distance = self._global_container_stack.getProperty("skirt_gap", "value") skirt_distance = self._global_container_stack.getProperty("skirt_gap", "value")
skirt_line_count = self._global_container_stack.getProperty("skirt_line_count", "value") skirt_line_count = self._global_container_stack.getProperty("skirt_line_count", "value")
bed_adhesion_size = skirt_distance + (skirt_brim_line_width * skirt_line_count) * initial_layer_line_width_factor / 100.0 bed_adhesion_size = skirt_distance + (
skirt_brim_line_width * skirt_line_count) * initial_layer_line_width_factor / 100.0
for extruder_stack in used_extruders: for extruder_stack in used_extruders:
bed_adhesion_size += extruder_stack.getProperty("skirt_brim_line_width", "value") * extruder_stack.getProperty("initial_layer_line_width_factor", "value") / 100.0 bed_adhesion_size += extruder_stack.getProperty("skirt_brim_line_width", "value") * extruder_stack.getProperty("initial_layer_line_width_factor", "value") / 100.0
@ -1066,10 +1048,8 @@ class BuildVolume(SceneNode):
bed_adhesion_size -= skirt_brim_line_width * initial_layer_line_width_factor / 100.0 bed_adhesion_size -= skirt_brim_line_width * initial_layer_line_width_factor / 100.0
elif adhesion_type == "raft": elif adhesion_type == "raft":
bed_adhesion_size = self._global_container_stack.getProperty("raft_margin", "value") bed_adhesion_size = self._global_container_stack.getProperty("raft_margin", "value")
elif adhesion_type == "none": elif adhesion_type == "none":
bed_adhesion_size = 0 bed_adhesion_size = 0
else: else:
raise Exception("Unknown bed adhesion type. Did you forget to update the build volume calculations for your new bed adhesion type?") raise Exception("Unknown bed adhesion type. Did you forget to update the build volume calculations for your new bed adhesion type?")
@ -1078,26 +1058,56 @@ class BuildVolume(SceneNode):
self._global_container_stack.getProperty("machine_depth", "value") self._global_container_stack.getProperty("machine_depth", "value")
) )
bed_adhesion_size = min(bed_adhesion_size, max_length_available) bed_adhesion_size = min(bed_adhesion_size, max_length_available)
return bed_adhesion_size
def _calculateFarthestShieldDistance(self, container_stack):
farthest_shield_distance = 0
if container_stack.getProperty("draft_shield_enabled", "value"):
farthest_shield_distance = max(farthest_shield_distance, container_stack.getProperty("draft_shield_dist", "value"))
if container_stack.getProperty("ooze_shield_enabled", "value"):
farthest_shield_distance = max(farthest_shield_distance,container_stack.getProperty("ooze_shield_dist", "value"))
return farthest_shield_distance
def _calculateSupportExpansion(self, container_stack):
support_expansion = 0 support_expansion = 0
support_enabled = self._global_container_stack.getProperty("support_enable", "value") support_enabled = self._global_container_stack.getProperty("support_enable", "value")
support_offset = self._global_container_stack.getProperty("support_offset", "value") support_offset = self._global_container_stack.getProperty("support_offset", "value")
if support_enabled and support_offset: if support_enabled and support_offset:
support_expansion += support_offset support_expansion += support_offset
return support_expansion
farthest_shield_distance = 0 def _calculateMoveFromWallRadius(self, used_extruders):
if container_stack.getProperty("draft_shield_enabled", "value"):
farthest_shield_distance = max(farthest_shield_distance, container_stack.getProperty("draft_shield_dist", "value"))
if container_stack.getProperty("ooze_shield_enabled", "value"):
farthest_shield_distance = max(farthest_shield_distance, container_stack.getProperty("ooze_shield_dist", "value"))
move_from_wall_radius = 0 # Moves that start from outer wall. move_from_wall_radius = 0 # Moves that start from outer wall.
move_from_wall_radius = max(move_from_wall_radius, max(self._getSettingFromAllExtruders("infill_wipe_dist"))) all_values = [move_from_wall_radius]
avoid_enabled_per_extruder = [stack.getProperty("travel_avoid_other_parts","value") for stack in used_extruders] all_values.extend(self._getSettingFromAllExtruders("infill_wipe_dist"))
move_from_wall_radius = max(all_values)
avoid_enabled_per_extruder = [stack.getProperty("travel_avoid_other_parts", "value") for stack in used_extruders]
travel_avoid_distance_per_extruder = [stack.getProperty("travel_avoid_distance", "value") for stack in used_extruders] travel_avoid_distance_per_extruder = [stack.getProperty("travel_avoid_distance", "value") for stack in used_extruders]
for avoid_other_parts_enabled, avoid_distance in zip(avoid_enabled_per_extruder, travel_avoid_distance_per_extruder): #For each extruder (or just global). for avoid_other_parts_enabled, avoid_distance in zip(avoid_enabled_per_extruder, travel_avoid_distance_per_extruder): # For each extruder (or just global).
if avoid_other_parts_enabled: if avoid_other_parts_enabled:
move_from_wall_radius = max(move_from_wall_radius, avoid_distance) move_from_wall_radius = max(move_from_wall_radius, avoid_distance)
return move_from_wall_radius
## Calculate the disallowed radius around the edge.
#
# This disallowed radius is to allow for space around the models that is
# not part of the collision radius, such as bed adhesion (skirt/brim/raft)
# and travel avoid distance.
def getEdgeDisallowedSize(self):
if not self._global_container_stack or not self._global_container_stack.extruderList:
return 0
container_stack = self._global_container_stack
used_extruders = ExtruderManager.getInstance().getUsedExtruderStacks()
# If we are printing one at a time, we need to add the bed adhesion size to the disallowed areas of the objects
if container_stack.getProperty("print_sequence", "value") == "one_at_a_time":
return 0.1
bed_adhesion_size = self._calculateBedAdhesionSize(used_extruders)
support_expansion = self._calculateSupportExpansion(self._global_container_stack)
farthest_shield_distance = self._calculateFarthestShieldDistance(self._global_container_stack)
move_from_wall_radius = self._calculateMoveFromWallRadius(used_extruders)
# Now combine our different pieces of data to get the final border size. # Now combine our different pieces of data to get the final border size.
# Support expansion is added to the bed adhesion, since the bed adhesion goes around support. # Support expansion is added to the bed adhesion, since the bed adhesion goes around support.
@ -1113,8 +1123,9 @@ class BuildVolume(SceneNode):
_raft_settings = ["adhesion_type", "raft_base_thickness", "raft_interface_thickness", "raft_surface_layers", "raft_surface_thickness", "raft_airgap", "layer_0_z_overlap"] _raft_settings = ["adhesion_type", "raft_base_thickness", "raft_interface_thickness", "raft_surface_layers", "raft_surface_thickness", "raft_airgap", "layer_0_z_overlap"]
_extra_z_settings = ["retraction_hop_enabled", "retraction_hop"] _extra_z_settings = ["retraction_hop_enabled", "retraction_hop"]
_prime_settings = ["extruder_prime_pos_x", "extruder_prime_pos_y", "extruder_prime_pos_z", "prime_blob_enable"] _prime_settings = ["extruder_prime_pos_x", "extruder_prime_pos_y", "extruder_prime_pos_z", "prime_blob_enable"]
_tower_settings = ["prime_tower_enable", "prime_tower_circular", "prime_tower_size", "prime_tower_position_x", "prime_tower_position_y", "prime_tower_brim_enable"] _tower_settings = ["prime_tower_enable", "prime_tower_size", "prime_tower_position_x", "prime_tower_position_y", "prime_tower_brim_enable"]
_ooze_shield_settings = ["ooze_shield_enabled", "ooze_shield_dist"] _ooze_shield_settings = ["ooze_shield_enabled", "ooze_shield_dist"]
_distance_settings = ["infill_wipe_dist", "travel_avoid_distance", "support_offset", "support_enable", "travel_avoid_other_parts", "travel_avoid_supports"] _distance_settings = ["infill_wipe_dist", "travel_avoid_distance", "support_offset", "support_enable", "travel_avoid_other_parts", "travel_avoid_supports"]
_extruder_settings = ["support_enable", "support_bottom_enable", "support_roof_enable", "support_infill_extruder_nr", "support_extruder_nr_layer_0", "support_bottom_extruder_nr", "support_roof_extruder_nr", "brim_line_count", "adhesion_extruder_nr", "adhesion_type"] #Settings that can affect which extruders are used. _extruder_settings = ["support_enable", "support_bottom_enable", "support_roof_enable", "support_infill_extruder_nr", "support_extruder_nr_layer_0", "support_bottom_extruder_nr", "support_roof_extruder_nr", "brim_line_count", "adhesion_extruder_nr", "adhesion_type"] #Settings that can affect which extruders are used.
_limit_to_extruder_settings = ["wall_extruder_nr", "wall_0_extruder_nr", "wall_x_extruder_nr", "top_bottom_extruder_nr", "infill_extruder_nr", "support_infill_extruder_nr", "support_extruder_nr_layer_0", "support_bottom_extruder_nr", "support_roof_extruder_nr", "adhesion_extruder_nr"] _limit_to_extruder_settings = ["wall_extruder_nr", "wall_0_extruder_nr", "wall_x_extruder_nr", "top_bottom_extruder_nr", "infill_extruder_nr", "support_infill_extruder_nr", "support_extruder_nr_layer_0", "support_bottom_extruder_nr", "support_roof_extruder_nr", "adhesion_extruder_nr"]
_disallowed_area_settings = _skirt_settings + _prime_settings + _tower_settings + _ooze_shield_settings + _distance_settings + _extruder_settings

View File

@ -12,9 +12,10 @@ import json
import ssl import ssl
import urllib.request import urllib.request
import urllib.error 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.QtWidgets import QDialog, QDialogButtonBox, QVBoxLayout, QLabel, QTextEdit, QGroupBox, QCheckBox, QPushButton
from PyQt5.QtGui import QDesktopServices from PyQt5.QtGui import QDesktopServices
@ -22,9 +23,10 @@ from UM.Application import Application
from UM.Logger import Logger from UM.Logger import Logger
from UM.View.GL.OpenGL import OpenGL from UM.View.GL.OpenGL import OpenGL
from UM.i18n import i18nCatalog from UM.i18n import i18nCatalog
from UM.Platform import Platform
from UM.Resources import Resources from UM.Resources import Resources
from cura import ApplicationMetadata
catalog = i18nCatalog("cura") catalog = i18nCatalog("cura")
MYPY = False MYPY = False
@ -181,6 +183,7 @@ class CrashHandler:
self.cura_version = catalog.i18nc("@label unknown version of Cura", "Unknown") 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 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 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", "Qt version") + ":</b> " + str(QT_VERSION_STR) + "<br/>"
crash_info += "<b>" + catalog.i18nc("@label", "PyQt version") + ":</b> " + str(PYQT_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) group.setLayout(layout)
self.data["cura_version"] = self.cura_version 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["os"] = {"type": platform.system(), "version": platform.version()}
self.data["qt_version"] = QT_VERSION_STR self.data["qt_version"] = QT_VERSION_STR
self.data["pyqt_version"] = PYQT_VERSION_STR self.data["pyqt_version"] = PYQT_VERSION_STR
@ -352,11 +356,13 @@ class CrashHandler:
# Convert data to bytes # Convert data to bytes
binary_data = json.dumps(self.data).encode("utf-8") 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 # Submit data
kwoptions = {"data": binary_data, "timeout": 5} kwoptions = {"data": binary_data,
"timeout": 5,
if Platform.isOSX(): "context": context}
kwoptions["context"] = ssl._create_unverified_context()
Logger.log("i", "Sending crash report info to [%s]...", self.crash_url) Logger.log("i", "Sending crash report info to [%s]...", self.crash_url)
if not self.has_started: if not self.has_started:

View File

@ -3,15 +3,17 @@
from PyQt5.QtCore import QObject, QUrl from PyQt5.QtCore import QObject, QUrl
from PyQt5.QtGui import QDesktopServices from PyQt5.QtGui import QDesktopServices
from typing import List, TYPE_CHECKING, cast from typing import List, Optional, cast
from UM.Event import CallFunctionEvent from UM.Event import CallFunctionEvent
from UM.FlameProfiler import pyqtSlot from UM.FlameProfiler import pyqtSlot
from UM.Math.Quaternion import Quaternion
from UM.Math.Vector import Vector from UM.Math.Vector import Vector
from UM.Scene.Selection import Selection from UM.Scene.Selection import Selection
from UM.Scene.Iterator.BreadthFirstIterator import BreadthFirstIterator from UM.Scene.Iterator.BreadthFirstIterator import BreadthFirstIterator
from UM.Operations.GroupedOperation import GroupedOperation from UM.Operations.GroupedOperation import GroupedOperation
from UM.Operations.RemoveSceneNodeOperation import RemoveSceneNodeOperation from UM.Operations.RemoveSceneNodeOperation import RemoveSceneNodeOperation
from UM.Operations.RotateOperation import RotateOperation
from UM.Operations.TranslateOperation import TranslateOperation from UM.Operations.TranslateOperation import TranslateOperation
import cura.CuraApplication import cura.CuraApplication
@ -23,9 +25,8 @@ from cura.Settings.ExtruderManager import ExtruderManager
from cura.Operations.SetBuildPlateNumberOperation import SetBuildPlateNumberOperation from cura.Operations.SetBuildPlateNumberOperation import SetBuildPlateNumberOperation
from UM.Logger import Logger from UM.Logger import Logger
from UM.Scene.SceneNode import SceneNode
if TYPE_CHECKING:
from UM.Scene.SceneNode import SceneNode
class CuraActions(QObject): class CuraActions(QObject):
def __init__(self, parent: QObject = None) -> None: def __init__(self, parent: QObject = None) -> None:

View File

@ -15,7 +15,7 @@ from PyQt5.QtQml import qmlRegisterUncreatableType, qmlRegisterSingletonType, qm
from UM.i18n import i18nCatalog from UM.i18n import i18nCatalog
from UM.Application import Application from UM.Application import Application
from UM.Decorators import override from UM.Decorators import override, deprecated
from UM.FlameProfiler import pyqtSlot from UM.FlameProfiler import pyqtSlot
from UM.Logger import Logger from UM.Logger import Logger
from UM.Message import Message from UM.Message import Message
@ -23,7 +23,6 @@ from UM.Platform import Platform
from UM.PluginError import PluginNotFoundError from UM.PluginError import PluginNotFoundError
from UM.Resources import Resources from UM.Resources import Resources
from UM.Preferences import Preferences from UM.Preferences import Preferences
from UM.Qt.Bindings import MainWindow
from UM.Qt.QtApplication import QtApplication # The class we're inheriting from. from UM.Qt.QtApplication import QtApplication # The class we're inheriting from.
import UM.Util import UM.Util
from UM.View.SelectionPass import SelectionPass # For typing. from UM.View.SelectionPass import SelectionPass # For typing.
@ -47,7 +46,6 @@ from UM.Scene.Selection import Selection
from UM.Scene.ToolHandle import ToolHandle from UM.Scene.ToolHandle import ToolHandle
from UM.Settings.ContainerRegistry import ContainerRegistry from UM.Settings.ContainerRegistry import ContainerRegistry
from UM.Settings.ContainerStack import ContainerStack
from UM.Settings.InstanceContainer import InstanceContainer from UM.Settings.InstanceContainer import InstanceContainer
from UM.Settings.SettingDefinition import SettingDefinition, DefinitionPropertyType from UM.Settings.SettingDefinition import SettingDefinition, DefinitionPropertyType
from UM.Settings.SettingFunction import SettingFunction from UM.Settings.SettingFunction import SettingFunction
@ -69,11 +67,10 @@ from cura.Scene.BuildPlateDecorator import BuildPlateDecorator
from cura.Scene.ConvexHullDecorator import ConvexHullDecorator from cura.Scene.ConvexHullDecorator import ConvexHullDecorator
from cura.Scene.CuraSceneController import CuraSceneController from cura.Scene.CuraSceneController import CuraSceneController
from cura.Scene.CuraSceneNode import CuraSceneNode from cura.Scene.CuraSceneNode import CuraSceneNode
from cura.Scene.SliceableObjectDecorator import SliceableObjectDecorator from cura.Scene.SliceableObjectDecorator import SliceableObjectDecorator
from cura.Scene import ZOffsetDecorator from cura.Scene import ZOffsetDecorator
from cura.Machines.MachineErrorChecker import MachineErrorChecker from cura.Machines.MachineErrorChecker import MachineErrorChecker
from cura.Machines.VariantManager import VariantManager
from cura.Machines.Models.BuildPlateModel import BuildPlateModel from cura.Machines.Models.BuildPlateModel import BuildPlateModel
from cura.Machines.Models.CustomQualityProfilesDropDownMenuModel import CustomQualityProfilesDropDownMenuModel from cura.Machines.Models.CustomQualityProfilesDropDownMenuModel import CustomQualityProfilesDropDownMenuModel
@ -84,6 +81,7 @@ from cura.Machines.Models.FirstStartMachineActionsModel import FirstStartMachine
from cura.Machines.Models.GenericMaterialsModel import GenericMaterialsModel from cura.Machines.Models.GenericMaterialsModel import GenericMaterialsModel
from cura.Machines.Models.GlobalStacksModel import GlobalStacksModel from cura.Machines.Models.GlobalStacksModel import GlobalStacksModel
from cura.Machines.Models.MaterialBrandsModel import MaterialBrandsModel 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.MultiBuildPlateModel import MultiBuildPlateModel
from cura.Machines.Models.NozzleModel import NozzleModel from cura.Machines.Models.NozzleModel import NozzleModel
from cura.Machines.Models.QualityManagementModel import QualityManagementModel from cura.Machines.Models.QualityManagementModel import QualityManagementModel
@ -91,6 +89,8 @@ from cura.Machines.Models.QualityProfilesDropDownMenuModel import QualityProfile
from cura.Machines.Models.QualitySettingsModel import QualitySettingsModel from cura.Machines.Models.QualitySettingsModel import QualitySettingsModel
from cura.Machines.Models.SettingVisibilityPresetsModel import SettingVisibilityPresetsModel from cura.Machines.Models.SettingVisibilityPresetsModel import SettingVisibilityPresetsModel
from cura.Machines.Models.UserChangesModel import UserChangesModel 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.PrinterOutputDevice import PrinterOutputDevice
from cura.PrinterOutput.NetworkMJPGImage import NetworkMJPGImage from cura.PrinterOutput.NetworkMJPGImage import NetworkMJPGImage
@ -100,8 +100,10 @@ from cura.Settings.ContainerManager import ContainerManager
from cura.Settings.CuraContainerRegistry import CuraContainerRegistry from cura.Settings.CuraContainerRegistry import CuraContainerRegistry
from cura.Settings.CuraFormulaFunctions import CuraFormulaFunctions from cura.Settings.CuraFormulaFunctions import CuraFormulaFunctions
from cura.Settings.ExtruderManager import ExtruderManager from cura.Settings.ExtruderManager import ExtruderManager
from cura.Settings.ExtruderStack import ExtruderStack
from cura.Settings.MachineManager import MachineManager from cura.Settings.MachineManager import MachineManager
from cura.Settings.MachineNameValidator import MachineNameValidator from cura.Settings.MachineNameValidator import MachineNameValidator
from cura.Settings.IntentManager import IntentManager
from cura.Settings.MaterialSettingsVisibilityHandler import MaterialSettingsVisibilityHandler from cura.Settings.MaterialSettingsVisibilityHandler import MaterialSettingsVisibilityHandler
from cura.Settings.SettingInheritanceManager import SettingInheritanceManager from cura.Settings.SettingInheritanceManager import SettingInheritanceManager
from cura.Settings.SidebarCustomMenuItemsModel import SidebarCustomMenuItemsModel from cura.Settings.SidebarCustomMenuItemsModel import SidebarCustomMenuItemsModel
@ -129,13 +131,10 @@ from . import CuraActions
from . import PrintJobPreviewImageProvider from . import PrintJobPreviewImageProvider
from cura import ApplicationMetadata, UltimakerCloudAuthentication from cura import ApplicationMetadata, UltimakerCloudAuthentication
from cura.Settings.GlobalStack import GlobalStack
if TYPE_CHECKING: if TYPE_CHECKING:
from cura.Machines.MaterialManager import MaterialManager
from cura.Machines.QualityManager import QualityManager
from UM.Settings.EmptyInstanceContainer import EmptyInstanceContainer from UM.Settings.EmptyInstanceContainer import EmptyInstanceContainer
from cura.Settings.GlobalStack import GlobalStack
numpy.seterr(all = "ignore") numpy.seterr(all = "ignore")
@ -144,7 +143,7 @@ class CuraApplication(QtApplication):
# SettingVersion represents the set of settings available in the machine/extruder definitions. # SettingVersion represents the set of settings available in the machine/extruder definitions.
# You need to make sure that this version number needs to be increased if there is any non-backwards-compatible # You need to make sure that this version number needs to be increased if there is any non-backwards-compatible
# changes of the settings. # changes of the settings.
SettingVersion = 7 SettingVersion = 11
Created = False Created = False
@ -160,6 +159,7 @@ class CuraApplication(QtApplication):
ExtruderStack = Resources.UserType + 9 ExtruderStack = Resources.UserType + 9
DefinitionChangesContainer = Resources.UserType + 10 DefinitionChangesContainer = Resources.UserType + 10
SettingVisibilityPreset = Resources.UserType + 11 SettingVisibilityPreset = Resources.UserType + 11
IntentInstanceContainer = Resources.UserType + 12
Q_ENUMS(ResourceTypes) Q_ENUMS(ResourceTypes)
@ -168,7 +168,7 @@ class CuraApplication(QtApplication):
app_display_name = ApplicationMetadata.CuraAppDisplayName, app_display_name = ApplicationMetadata.CuraAppDisplayName,
version = ApplicationMetadata.CuraVersion, version = ApplicationMetadata.CuraVersion,
api_version = ApplicationMetadata.CuraSDKVersion, api_version = ApplicationMetadata.CuraSDKVersion,
buildtype = ApplicationMetadata.CuraBuildType, build_type = ApplicationMetadata.CuraBuildType,
is_debug_mode = ApplicationMetadata.CuraDebugMode, is_debug_mode = ApplicationMetadata.CuraDebugMode,
tray_icon_name = "cura-icon-32.png", tray_icon_name = "cura-icon-32.png",
**kwargs) **kwargs)
@ -196,13 +196,12 @@ class CuraApplication(QtApplication):
self.empty_container = None # type: EmptyInstanceContainer self.empty_container = None # type: EmptyInstanceContainer
self.empty_definition_changes_container = None # type: EmptyInstanceContainer self.empty_definition_changes_container = None # type: EmptyInstanceContainer
self.empty_variant_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_material_container = None # type: EmptyInstanceContainer
self.empty_quality_container = None # type: EmptyInstanceContainer self.empty_quality_container = None # type: EmptyInstanceContainer
self.empty_quality_changes_container = None # type: EmptyInstanceContainer self.empty_quality_changes_container = None # type: EmptyInstanceContainer
self._variant_manager = None
self._material_manager = None self._material_manager = None
self._quality_manager = None
self._machine_manager = None self._machine_manager = None
self._extruder_manager = None self._extruder_manager = None
self._container_manager = None self._container_manager = None
@ -219,9 +218,11 @@ class CuraApplication(QtApplication):
self._machine_error_checker = None self._machine_error_checker = None
self._machine_settings_manager = MachineSettingsManager(self, parent = self) 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._discovered_printer_model = DiscoveredPrintersModel(self, parent = self)
self._first_start_machine_actions_model = FirstStartMachineActionsModel(self, parent = self) self._first_start_machine_actions_model = None
self._welcome_pages_model = WelcomePagesModel(self, parent = self) self._welcome_pages_model = WelcomePagesModel(self, parent = self)
self._add_printer_pages_model = AddPrinterPagesModel(self, parent = self) self._add_printer_pages_model = AddPrinterPagesModel(self, parent = self)
self._whats_new_pages_model = WhatsNewPagesModel(self, parent = self) self._whats_new_pages_model = WhatsNewPagesModel(self, parent = self)
@ -345,7 +346,7 @@ class CuraApplication(QtApplication):
# Adds expected directory names and search paths for Resources. # Adds expected directory names and search paths for Resources.
def __addExpectedResourceDirsAndSearchPaths(self): def __addExpectedResourceDirsAndSearchPaths(self):
# this list of dir names will be used by UM to detect an old cura directory # 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.addExpectedDirNameInData(dir_name)
Resources.addSearchPath(os.path.join(self._app_install_dir, "share", "cura", "resources")) Resources.addSearchPath(os.path.join(self._app_install_dir, "share", "cura", "resources"))
@ -403,6 +404,7 @@ class CuraApplication(QtApplication):
Resources.addStorageType(self.ResourceTypes.MachineStack, "machine_instances") Resources.addStorageType(self.ResourceTypes.MachineStack, "machine_instances")
Resources.addStorageType(self.ResourceTypes.DefinitionChangesContainer, "definition_changes") Resources.addStorageType(self.ResourceTypes.DefinitionChangesContainer, "definition_changes")
Resources.addStorageType(self.ResourceTypes.SettingVisibilityPreset, "setting_visibility") 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.QualityInstanceContainer, "quality")
self._container_registry.addResourceType(self.ResourceTypes.QualityChangesInstanceContainer, "quality_changes") self._container_registry.addResourceType(self.ResourceTypes.QualityChangesInstanceContainer, "quality_changes")
@ -412,6 +414,7 @@ class CuraApplication(QtApplication):
self._container_registry.addResourceType(self.ResourceTypes.ExtruderStack, "extruder_train") self._container_registry.addResourceType(self.ResourceTypes.ExtruderStack, "extruder_train")
self._container_registry.addResourceType(self.ResourceTypes.MachineStack, "machine") self._container_registry.addResourceType(self.ResourceTypes.MachineStack, "machine")
self._container_registry.addResourceType(self.ResourceTypes.DefinitionChangesContainer, "definition_changes") 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.QmlFiles, "qml")
Resources.addType(self.ResourceTypes.Firmware, "firmware") Resources.addType(self.ResourceTypes.Firmware, "firmware")
@ -421,7 +424,7 @@ class CuraApplication(QtApplication):
# Add empty variant, material and quality containers. # Add empty variant, material and quality containers.
# Since they are empty, they should never be serialized and instead just programmatically created. # Since they are empty, they should never be serialized and instead just programmatically created.
# We need them to simplify the switching between materials. # 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( self._container_registry.addContainer(
cura.Settings.cura_empty_instance_containers.empty_definition_changes_container) cura.Settings.cura_empty_instance_containers.empty_definition_changes_container)
@ -430,6 +433,9 @@ class CuraApplication(QtApplication):
self._container_registry.addContainer(cura.Settings.cura_empty_instance_containers.empty_variant_container) 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.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._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 self.empty_material_container = cura.Settings.cura_empty_instance_containers.empty_material_container
@ -444,14 +450,15 @@ class CuraApplication(QtApplication):
def __setLatestResouceVersionsForVersionUpgrade(self): def __setLatestResouceVersionsForVersionUpgrade(self):
self._version_upgrade_manager.setCurrentVersions( self._version_upgrade_manager.setCurrentVersions(
{ {
("quality", InstanceContainer.Version * 1000000 + self.SettingVersion): (self.ResourceTypes.QualityInstanceContainer, "application/x-uranium-instancecontainer"), ("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"), ("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"), ("intent", InstanceContainer.Version * 1000000 + self.SettingVersion): (self.ResourceTypes.IntentInstanceContainer, "application/x-uranium-instancecontainer"),
("extruder_train", ContainerStack.Version * 1000000 + self.SettingVersion): (self.ResourceTypes.ExtruderStack, "application/x-cura-extruderstack"), ("machine_stack", GlobalStack.Version * 1000000 + self.SettingVersion): (self.ResourceTypes.MachineStack, "application/x-cura-globalstack"),
("preferences", Preferences.Version * 1000000 + self.SettingVersion): (Resources.Preferences, "application/x-uranium-preferences"), ("extruder_train", ExtruderStack.Version * 1000000 + self.SettingVersion): (self.ResourceTypes.ExtruderStack, "application/x-cura-extruderstack"),
("user", InstanceContainer.Version * 1000000 + self.SettingVersion): (self.ResourceTypes.UserInstanceContainer, "application/x-uranium-instancecontainer"), ("preferences", Preferences.Version * 1000000 + self.SettingVersion): (Resources.Preferences, "application/x-uranium-preferences"),
("definition_changes", InstanceContainer.Version * 1000000 + self.SettingVersion): (self.ResourceTypes.DefinitionChangesContainer, "application/x-uranium-instancecontainer"), ("user", InstanceContainer.Version * 1000000 + self.SettingVersion): (self.ResourceTypes.UserInstanceContainer, "application/x-uranium-instancecontainer"),
("variant", InstanceContainer.Version * 1000000 + self.SettingVersion): (self.ResourceTypes.VariantInstanceContainer, "application/x-uranium-instancecontainer"), ("definition_changes", InstanceContainer.Version * 1000000 + self.SettingVersion): (self.ResourceTypes.DefinitionChangesContainer, "application/x-uranium-instancecontainer"),
("variant", InstanceContainer.Version * 1000000 + self.SettingVersion): (self.ResourceTypes.VariantInstanceContainer, "application/x-uranium-instancecontainer"),
} }
) )
@ -503,10 +510,13 @@ class CuraApplication(QtApplication):
self.showSplashMessage(self._i18n_catalog.i18nc("@info:progress", "Loading machines...")) self.showSplashMessage(self._i18n_catalog.i18nc("@info:progress", "Loading machines..."))
self._container_registry.allMetadataLoaded.connect(ContainerRegistry.getInstance)
with self._container_registry.lockFile(): with self._container_registry.lockFile():
self._container_registry.loadAllMetadata() 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 = self.getPreferences()
preferences.addPreference("metadata/setting_version", 0) 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. 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.
@ -631,9 +641,17 @@ class CuraApplication(QtApplication):
## A reusable dialogbox ## 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 = callback
self._message_box_callback_arguments = callback_arguments self._message_box_callback_arguments = callback_arguments
self.showMessageBox.emit(title, text, informativeText, detailedText, buttons, icon) self.showMessageBox.emit(title, text, informativeText, detailedText, buttons, icon)
@ -659,14 +677,14 @@ class CuraApplication(QtApplication):
def discardOrKeepProfileChangesClosed(self, option: str) -> None: def discardOrKeepProfileChangesClosed(self, option: str) -> None:
global_stack = self.getGlobalContainerStack() global_stack = self.getGlobalContainerStack()
if option == "discard": if option == "discard":
for extruder in global_stack.extruders.values(): for extruder in global_stack.extruderList:
extruder.userChanges.clear() extruder.userChanges.clear()
global_stack.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 # 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 # before slicing. To ensure that slicer uses right settings values
elif option == "keep": elif option == "keep":
for extruder in global_stack.extruders.values(): for extruder in global_stack.extruderList:
extruder.userChanges.update() extruder.userChanges.update()
global_stack.userChanges.update() global_stack.userChanges.update()
@ -700,6 +718,8 @@ class CuraApplication(QtApplication):
## Handle loading of all plugin types (and the backend explicitly) ## Handle loading of all plugin types (and the backend explicitly)
# \sa PluginRegistry # \sa PluginRegistry
def _loadPlugins(self) -> None: def _loadPlugins(self) -> None:
self._plugin_registry.setCheckIfTrusted(ApplicationMetadata.IsEnterpriseVersion)
self._plugin_registry.addType("profile_reader", self._addProfileReader) self._plugin_registry.addType("profile_reader", self._addProfileReader)
self._plugin_registry.addType("profile_writer", self._addProfileWriter) self._plugin_registry.addType("profile_writer", self._addProfileWriter)
@ -723,21 +743,6 @@ class CuraApplication(QtApplication):
def run(self): def run(self):
super().run() 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") Logger.log("i", "Initializing machine manager")
self._machine_manager = MachineManager(self, parent = self) self._machine_manager = MachineManager(self, parent = self)
@ -839,7 +844,6 @@ class CuraApplication(QtApplication):
if diagonal < 1: #No printer added yet. Set a default camera distance for normal-sized printers. if diagonal < 1: #No printer added yet. Set a default camera distance for normal-sized printers.
diagonal = 375 diagonal = 375
camera.setPosition(Vector(-80, 250, 700) * diagonal / 375) camera.setPosition(Vector(-80, 250, 700) * diagonal / 375)
camera.setPerspective(True)
camera.lookAt(Vector(0, 0, 0)) camera.lookAt(Vector(0, 0, 0))
controller.getScene().setActiveCamera("3d") controller.getScene().setActiveCamera("3d")
@ -874,6 +878,10 @@ class CuraApplication(QtApplication):
@pyqtSlot(result = QObject) @pyqtSlot(result = QObject)
def getFirstStartMachineActionsModel(self, *args) -> "FirstStartMachineActionsModel": 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 return self._first_start_machine_actions_model
@pyqtSlot(result = QObject) @pyqtSlot(result = QObject)
@ -918,16 +926,8 @@ class CuraApplication(QtApplication):
self._extruder_manager = ExtruderManager() self._extruder_manager = ExtruderManager()
return self._extruder_manager return self._extruder_manager
def getVariantManager(self, *args) -> VariantManager: def getIntentManager(self, *args) -> IntentManager:
return self._variant_manager return IntentManager.getInstance()
@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 getObjectsModel(self, *args): def getObjectsModel(self, *args):
if self._object_manager is None: if self._object_manager is None:
@ -975,6 +975,18 @@ class CuraApplication(QtApplication):
def getMachineActionManager(self, *args): def getMachineActionManager(self, *args):
return self._machine_action_manager 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): def getSimpleModeSettingsManager(self, *args):
if self._simple_mode_settings_manager is None: if self._simple_mode_settings_manager is None:
self._simple_mode_settings_manager = SimpleModeSettingsManager() self._simple_mode_settings_manager = SimpleModeSettingsManager()
@ -1028,6 +1040,7 @@ class CuraApplication(QtApplication):
qmlRegisterSingletonType(CuraSceneController, "Cura", 1, 0, "SceneController", self.getCuraSceneController) qmlRegisterSingletonType(CuraSceneController, "Cura", 1, 0, "SceneController", self.getCuraSceneController)
qmlRegisterSingletonType(ExtruderManager, "Cura", 1, 0, "ExtruderManager", self.getExtruderManager) qmlRegisterSingletonType(ExtruderManager, "Cura", 1, 0, "ExtruderManager", self.getExtruderManager)
qmlRegisterSingletonType(MachineManager, "Cura", 1, 0, "MachineManager", self.getMachineManager) 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(SettingInheritanceManager, "Cura", 1, 0, "SettingInheritanceManager", self.getSettingInheritanceManager)
qmlRegisterSingletonType(SimpleModeSettingsManager, "Cura", 1, 0, "SimpleModeSettingsManager", self.getSimpleModeSettingsManager) qmlRegisterSingletonType(SimpleModeSettingsManager, "Cura", 1, 0, "SimpleModeSettingsManager", self.getSimpleModeSettingsManager)
qmlRegisterSingletonType(MachineActionManager.MachineActionManager, "Cura", 1, 0, "MachineActionManager", self.getMachineActionManager) qmlRegisterSingletonType(MachineActionManager.MachineActionManager, "Cura", 1, 0, "MachineActionManager", self.getMachineActionManager)
@ -1052,7 +1065,8 @@ class CuraApplication(QtApplication):
qmlRegisterType(FavoriteMaterialsModel, "Cura", 1, 0, "FavoriteMaterialsModel") qmlRegisterType(FavoriteMaterialsModel, "Cura", 1, 0, "FavoriteMaterialsModel")
qmlRegisterType(GenericMaterialsModel, "Cura", 1, 0, "GenericMaterialsModel") qmlRegisterType(GenericMaterialsModel, "Cura", 1, 0, "GenericMaterialsModel")
qmlRegisterType(MaterialBrandsModel, "Cura", 1, 0, "MaterialBrandsModel") qmlRegisterType(MaterialBrandsModel, "Cura", 1, 0, "MaterialBrandsModel")
qmlRegisterType(QualityManagementModel, "Cura", 1, 0, "QualityManagementModel") qmlRegisterSingletonType(QualityManagementModel, "Cura", 1, 0, "QualityManagementModel", self.getQualityManagementModel)
qmlRegisterSingletonType(MaterialManagementModel, "Cura", 1, 5, "MaterialManagementModel", self.getMaterialManagementModel)
qmlRegisterType(DiscoveredPrintersModel, "Cura", 1, 0, "DiscoveredPrintersModel") qmlRegisterType(DiscoveredPrintersModel, "Cura", 1, 0, "DiscoveredPrintersModel")
@ -1061,6 +1075,8 @@ class CuraApplication(QtApplication):
qmlRegisterSingletonType(CustomQualityProfilesDropDownMenuModel, "Cura", 1, 0, qmlRegisterSingletonType(CustomQualityProfilesDropDownMenuModel, "Cura", 1, 0,
"CustomQualityProfilesDropDownMenuModel", self.getCustomQualityProfilesDropDownMenuModel) "CustomQualityProfilesDropDownMenuModel", self.getCustomQualityProfilesDropDownMenuModel)
qmlRegisterType(NozzleModel, "Cura", 1, 0, "NozzleModel") 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(MaterialSettingsVisibilityHandler, "Cura", 1, 0, "MaterialSettingsVisibilityHandler")
qmlRegisterType(SettingVisibilityPresetsModel, "Cura", 1, 0, "SettingVisibilityPresetsModel") qmlRegisterType(SettingVisibilityPresetsModel, "Cura", 1, 0, "SettingVisibilityPresetsModel")
@ -1263,7 +1279,7 @@ class CuraApplication(QtApplication):
@pyqtSlot() @pyqtSlot()
def arrangeObjectsToAllBuildPlates(self) -> None: def arrangeObjectsToAllBuildPlates(self) -> None:
nodes_to_arrange = [] 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): if not isinstance(node, SceneNode):
continue continue
@ -1290,7 +1306,7 @@ class CuraApplication(QtApplication):
def arrangeAll(self) -> None: def arrangeAll(self) -> None:
nodes_to_arrange = [] nodes_to_arrange = []
active_build_plate = self.getMultiBuildPlateModel().activeBuildPlate 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): if not isinstance(node, SceneNode):
continue continue
@ -1328,7 +1344,13 @@ class CuraApplication(QtApplication):
Logger.log("i", "Reloading all loaded mesh data.") Logger.log("i", "Reloading all loaded mesh data.")
nodes = [] nodes = []
has_merged_nodes = False has_merged_nodes = False
for node in DepthFirstIterator(self.getController().getScene().getRoot()): # 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 not isinstance(node, CuraSceneNode) or not node.getMeshData():
if node.getName() == "MergedMesh": if node.getName() == "MergedMesh":
has_merged_nodes = True has_merged_nodes = True
@ -1336,21 +1358,29 @@ class CuraApplication(QtApplication):
nodes.append(node) 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: if not nodes:
return return
for node in nodes: for node in nodes:
file_name = node.getMeshData().getFileName() mesh_data = node.getMeshData()
if file_name:
job = ReadMeshJob(file_name)
job._node = node # type: ignore
job.finished.connect(self._reloadMeshFinished)
if has_merged_nodes:
job.finished.connect(self.updateOriginOfMergedMeshes)
job.start() if mesh_data:
else: file_name = mesh_data.getFileName()
Logger.log("w", "Unable to reload data because we don't have a filename.") if file_name:
job = ReadMeshJob(file_name)
job._node = node # type: ignore
job.finished.connect(self._reloadMeshFinished)
if has_merged_nodes:
job.finished.connect(self.updateOriginOfMergedMeshes)
job.start()
else:
Logger.log("w", "Unable to reload data because we don't have a filename.")
@pyqtSlot("QStringList") @pyqtSlot("QStringList")
def setExpandedCategories(self, categories: List[str]) -> None: def setExpandedCategories(self, categories: List[str]) -> None:
@ -1581,8 +1611,12 @@ class CuraApplication(QtApplication):
openProjectFile = pyqtSignal(QUrl, arguments = ["project_file"]) # Emitted when a project file is about to open. openProjectFile = pyqtSignal(QUrl, arguments = ["project_file"]) # Emitted when a project file is about to open.
@pyqtSlot(QUrl, bool) @pyqtSlot(QUrl, str)
def readLocalFile(self, file, skip_project_file_check = False): @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(): if not file.isValid():
return return
@ -1593,10 +1627,24 @@ class CuraApplication(QtApplication):
self.deleteAll() self.deleteAll()
break 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) self.callLater(self.openProjectFile.emit, file)
return 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"): if self.getPreferences().getValue("cura/select_models_on_load"):
Selection.clear() Selection.clear()
@ -1657,7 +1705,7 @@ class CuraApplication(QtApplication):
arranger = Arrange.create(x = machine_width, y = machine_depth, fixed_nodes = fixed_nodes) arranger = Arrange.create(x = machine_width, y = machine_depth, fixed_nodes = fixed_nodes)
min_offset = 8 min_offset = 8
default_extruder_position = self.getMachineManager().defaultExtruderPosition 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") select_models_on_load = self.getPreferences().getValue("cura/select_models_on_load")
@ -1751,7 +1799,7 @@ class CuraApplication(QtApplication):
try: try:
result = workspace_reader.preRead(file_path, show_dialog=False) result = workspace_reader.preRead(file_path, show_dialog=False)
return result == WorkspaceReader.PreReadResult.accepted return result == WorkspaceReader.PreReadResult.accepted
except Exception as e: except Exception:
Logger.logException("e", "Could not check file %s", file_url) Logger.logException("e", "Could not check file %s", file_url)
return False return False
@ -1810,3 +1858,44 @@ class CuraApplication(QtApplication):
return main_window.height() return main_window.height()
else: else:
return 0 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))

View File

@ -6,7 +6,6 @@ CuraAppDisplayName = "@CURA_APP_DISPLAY_NAME@"
CuraVersion = "@CURA_VERSION@" CuraVersion = "@CURA_VERSION@"
CuraBuildType = "@CURA_BUILDTYPE@" CuraBuildType = "@CURA_BUILDTYPE@"
CuraDebugMode = True if "@_cura_debugmode@" == "ON" else False CuraDebugMode = True if "@_cura_debugmode@" == "ON" else False
CuraSDKVersion = "@CURA_SDK_VERSION@"
CuraCloudAPIRoot = "@CURA_CLOUD_API_ROOT@" CuraCloudAPIRoot = "@CURA_CLOUD_API_ROOT@"
CuraCloudAPIVersion = "@CURA_CLOUD_API_VERSION@" CuraCloudAPIVersion = "@CURA_CLOUD_API_VERSION@"
CuraCloudAccountAPIRoot = "@CURA_CLOUD_ACCOUNT_API_ROOT@" CuraCloudAccountAPIRoot = "@CURA_CLOUD_ACCOUNT_API_ROOT@"

View File

@ -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 import numpy
from UM.Mesh.MeshBuilder import MeshBuilder
from UM.Mesh.MeshData import MeshData
from cura.LayerPolygon import LayerPolygon
class Layer: class Layer:
def __init__(self, layer_id): def __init__(self, layer_id: int) -> None:
self._id = layer_id self._id = layer_id
self._height = 0.0 self._height = 0.0
self._thickness = 0.0 self._thickness = 0.0
self._polygons = [] self._polygons = [] # type: List[LayerPolygon]
self._element_count = 0 self._element_count = 0
@property @property
@ -20,7 +26,7 @@ class Layer:
return self._thickness return self._thickness
@property @property
def polygons(self): def polygons(self) -> List[LayerPolygon]:
return self._polygons return self._polygons
@property @property
@ -33,14 +39,14 @@ class Layer:
def setThickness(self, thickness): def setThickness(self, thickness):
self._thickness = thickness self._thickness = thickness
def lineMeshVertexCount(self): def lineMeshVertexCount(self) -> int:
result = 0 result = 0
for polygon in self._polygons: for polygon in self._polygons:
result += polygon.lineMeshVertexCount() result += polygon.lineMeshVertexCount()
return result return result
def lineMeshElementCount(self): def lineMeshElementCount(self) -> int:
result = 0 result = 0
for polygon in self._polygons: for polygon in self._polygons:
result += polygon.lineMeshElementCount() result += polygon.lineMeshElementCount()
@ -57,18 +63,18 @@ class Layer:
result_index_offset += polygon.lineMeshElementCount() result_index_offset += polygon.lineMeshElementCount()
self._element_count += polygon.elementCount 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) return self.createMeshOrJumps(True)
def createJumps(self): def createJumps(self) -> MeshData:
return self.createMeshOrJumps(False) 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 # 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 ) __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() builder = MeshBuilder()
line_count = 0 line_count = 0
@ -79,14 +85,14 @@ class Layer:
for polygon in self._polygons: for polygon in self._polygons:
line_count += polygon.jumpCount 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) builder.reserveFaceAndVertexCount(2 * line_count, 4 * line_count)
for polygon in self._polygons: 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 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()] points = numpy.concatenate((polygon.data[:-1], polygon.data[1:]), 1)[index_mask.ravel()]
# Line types of the points we want to draw # Line types of the points we want to draw
line_types = polygon.types[index_mask] line_types = polygon.types[index_mask]

View File

@ -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. # 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 from typing import Any, Optional
import numpy import numpy
@ -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 # When type is used as index returns true if type == LayerPolygon.InfillType or type == LayerPolygon.SkinType or type == LayerPolygon.SupportInfillType
# Should be generated in better way, not hardcoded. # Should be generated in better way, not hardcoded.
self._isInfillOrSkinTypeMap = numpy.array([0, 0, 0, 1, 0, 0, 1, 1, 0, 0, 1], dtype=numpy.bool) self._isInfillOrSkinTypeMap = numpy.array([0, 0, 0, 1, 0, 0, 1, 1, 0, 0, 1, 0], dtype = numpy.bool)
self._build_cache_line_mesh_mask = None # type: Optional[numpy.ndarray] self._build_cache_line_mesh_mask = None # type: Optional[numpy.ndarray]
self._build_cache_needed_points = None # type: Optional[numpy.ndarray] self._build_cache_needed_points = None # type: Optional[numpy.ndarray]
def buildCache(self) -> None: def buildCache(self) -> None:
# For the line mesh we do not draw Infill or Jumps. Therefore those lines are filtered out. # For the line mesh we do not draw Infill or Jumps. Therefore those lines are filtered out.
self._build_cache_line_mesh_mask = numpy.ones(self._jump_mask.shape, dtype=bool) self._build_cache_line_mesh_mask = numpy.ones(self._jump_mask.shape, dtype = bool)
mesh_line_count = numpy.sum(self._build_cache_line_mesh_mask) mesh_line_count = numpy.sum(self._build_cache_line_mesh_mask)
self._index_begin = 0 self._index_begin = 0
self._index_end = mesh_line_count self._index_end = mesh_line_count
self._build_cache_needed_points = numpy.ones((len(self._types), 2), dtype=numpy.bool) self._build_cache_needed_points = numpy.ones((len(self._types), 2), dtype = numpy.bool)
# Only if the type of line segment changes do we need to add an extra vertex to change colors # Only if the type of line segment changes do we need to add an extra vertex to change colors
self._build_cache_needed_points[1:, 0][:, numpy.newaxis] = self._types[1:] != self._types[:-1] self._build_cache_needed_points[1:, 0][:, numpy.newaxis] = self._types[1:] != self._types[:-1]
# Mark points as unneeded if they are of types we don't want in the line mesh according to the calculated mask # Mark points as unneeded if they are of types we don't want in the line mesh according to the calculated mask
@ -136,9 +136,9 @@ class LayerPolygon:
self._index_begin += index_offset self._index_begin += index_offset
self._index_end += index_offset self._index_end += index_offset
indices[self._index_begin:self._index_end, :] = numpy.arange(self._index_end-self._index_begin, dtype=numpy.int32).reshape((-1, 1)) indices[self._index_begin:self._index_end, :] = numpy.arange(self._index_end-self._index_begin, dtype = numpy.int32).reshape((-1, 1))
# When the line type changes the index needs to be increased by 2. # When the line type changes the index needs to be increased by 2.
indices[self._index_begin:self._index_end, :] += numpy.cumsum(needed_points_list[line_mesh_mask.ravel(), 0], dtype=numpy.int32).reshape((-1, 1)) indices[self._index_begin:self._index_end, :] += numpy.cumsum(needed_points_list[line_mesh_mask.ravel(), 0], dtype = numpy.int32).reshape((-1, 1))
# Each line segment goes from it's starting point p to p+1, offset by the vertex index. # Each line segment goes from it's starting point p to p+1, offset by the vertex index.
# The -1 is to compensate for the neccecarily True value of needed_points_list[0,0] which causes an unwanted +1 in cumsum above. # The -1 is to compensate for the neccecarily True value of needed_points_list[0,0] which causes an unwanted +1 in cumsum above.
indices[self._index_begin:self._index_end, :] += numpy.array([self._vertex_begin - 1, self._vertex_begin]) indices[self._index_begin:self._index_end, :] += numpy.array([self._vertex_begin - 1, self._vertex_begin])
@ -232,7 +232,7 @@ class LayerPolygon:
@classmethod @classmethod
def getColorMap(cls): def getColorMap(cls):
if cls.__color_map is None: if cls.__color_map is None:
theme = Application.getInstance().getTheme() theme = QtApplication.getInstance().getTheme()
cls.__color_map = numpy.array([ cls.__color_map = numpy.array([
theme.getColor("layerview_none").getRgbF(), # NoneType theme.getColor("layerview_none").getRgbF(), # NoneType
theme.getColor("layerview_inset_0").getRgbF(), # Inset0Type theme.getColor("layerview_inset_0").getRgbF(), # Inset0Type

View File

@ -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. # Cura is released under the terms of the LGPLv3 or higher.
from typing import Optional, Any, Dict, Union, TYPE_CHECKING from typing import Any, Dict, Optional
from collections import OrderedDict
from UM.ConfigurationErrorMessage import ConfigurationErrorMessage from UM.ConfigurationErrorMessage import ConfigurationErrorMessage
from UM.Settings.ContainerRegistry import ContainerRegistry
from UM.Logger import Logger from UM.Logger import Logger
from UM.Settings.InstanceContainer import InstanceContainer from UM.Settings.InstanceContainer import InstanceContainer
## ## A node in the container tree. It represents one container.
# 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.
# #
# 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: class ContainerNode:
__slots__ = ("_metadata", "_container", "children_map") ## 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 = {} # type: Dict[str, ContainerNode] # Mapping from container ID to container node.
def __init__(self, metadata: Optional[Dict[str, Any]] = None) -> None: ## Gets the metadata of the container that this node represents.
self._metadata = metadata # Getting the metadata from the container directly is about 10x as fast.
self._container = None # type: Optional[InstanceContainer] # \return The metadata of the container in this node.
self.children_map = OrderedDict() # type: ignore # This is because it's children are supposed to override it. def getMetadata(self):
return ContainerRegistry.getInstance().findContainersMetadata(id = self.container_id)[0]
## Get an entry value from the metadata ## 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: 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 default
return self._metadata.get(entry, default) return container_metadata[0].get(entry, default)
def getMetadata(self) -> Dict[str, Any]: ## The container that this node's container ID refers to.
if self._metadata is None: #
return {} # This can be used to finally instantiate the container in order to put it
return self._metadata # in the container stack.
# \return A container.
def getChildNode(self, child_key: str) -> Optional["ContainerNode"]: @property
return self.children_map.get(child_key) def container(self) -> Optional[InstanceContainer]:
if not self._container:
def getContainer(self) -> Optional["InstanceContainer"]: container_list = ContainerRegistry.getInstance().findInstanceContainers(id = self.container_id)
if self._metadata is None: if len(container_list) == 0:
Logger.log("e", "Cannot get container for a ContainerNode without metadata.") Logger.log("e", "Failed to lazy-load container [{container_id}]. Cannot find it.".format(container_id = self.container_id))
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))
error_message = ConfigurationErrorMessage.getInstance() error_message = ConfigurationErrorMessage.getInstance()
error_message.addFaultyContainers(container_id) error_message.addFaultyContainers(self.container_id)
return None return None
self._container = container_list[0] self._container = container_list[0]
return self._container return self._container
def __str__(self) -> str: def __str__(self) -> str:
return "%s[%s]" % (self.__class__.__name__, self.getMetaDataEntry("id")) return "%s[%s]" % (self.__class__.__name__, self.container_id)

View 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]

View 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")

View File

@ -58,7 +58,6 @@ class MachineErrorChecker(QObject):
# Whenever the machine settings get changed, we schedule an error check. # Whenever the machine settings get changed, we schedule an error check.
self._machine_manager.globalContainerChanged.connect(self.startErrorCheck) self._machine_manager.globalContainerChanged.connect(self.startErrorCheck)
self._machine_manager.globalValueChanged.connect(self.startErrorCheck)
self._onMachineChanged() self._onMachineChanged()
@ -67,7 +66,7 @@ class MachineErrorChecker(QObject):
self._global_stack.propertyChanged.disconnect(self.startErrorCheckPropertyChanged) self._global_stack.propertyChanged.disconnect(self.startErrorCheckPropertyChanged)
self._global_stack.containersChanged.disconnect(self.startErrorCheck) self._global_stack.containersChanged.disconnect(self.startErrorCheck)
for extruder in self._global_stack.extruders.values(): for extruder in self._global_stack.extruderList:
extruder.propertyChanged.disconnect(self.startErrorCheckPropertyChanged) extruder.propertyChanged.disconnect(self.startErrorCheckPropertyChanged)
extruder.containersChanged.disconnect(self.startErrorCheck) extruder.containersChanged.disconnect(self.startErrorCheck)
@ -77,7 +76,7 @@ class MachineErrorChecker(QObject):
self._global_stack.propertyChanged.connect(self.startErrorCheckPropertyChanged) self._global_stack.propertyChanged.connect(self.startErrorCheckPropertyChanged)
self._global_stack.containersChanged.connect(self.startErrorCheck) self._global_stack.containersChanged.connect(self.startErrorCheck)
for extruder in self._global_stack.extruders.values(): for extruder in self._global_stack.extruderList:
extruder.propertyChanged.connect(self.startErrorCheckPropertyChanged) extruder.propertyChanged.connect(self.startErrorCheckPropertyChanged)
extruder.containersChanged.connect(self.startErrorCheck) extruder.containersChanged.connect(self.startErrorCheck)
@ -127,7 +126,7 @@ class MachineErrorChecker(QObject):
# Populate the (stack, key) tuples to check # Populate the (stack, key) tuples to check
self._stacks_and_keys_to_check = deque() self._stacks_and_keys_to_check = deque()
for stack in [global_stack] + list(global_stack.extruders.values()): for stack in global_stack.extruderList:
for key in stack.getAllKeys(): for key in stack.getAllKeys():
self._stacks_and_keys_to_check.append((stack, key)) self._stacks_and_keys_to_check.append((stack, key))

View File

@ -0,0 +1,184 @@
# 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)
self.variants["empty"].materialsChanged.connect(self.materialsChanged)
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)

View File

@ -1,723 +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 material_metadatas: #Not a registered material profile. Don't store this in the look-up tables.
continue
if root_material_id not in self._material_group_map:
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 = str(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
# Sort all nodes with respect to the container ID lengths in the ascending order so the base material container
# will be the first one to be removed. We need to do this to ensure that all containers get loaded & deleted.
nodes_to_remove = sorted(nodes_to_remove, key = lambda x: len(x.getMetaDataEntry("id", "")))
# Try to load all containers first. If there is any faulty ones, they will be put into the faulty container
# list, so removeContainer() can ignore those ones.
for node in nodes_to_remove:
container_id = node.getMetaDataEntry("id", "")
results = self._container_registry.findContainers(id = container_id)
if not results:
self._container_registry.addWrongContainerId(container_id)
for node in nodes_to_remove:
self._container_registry.removeContainer(node.getMetaDataEntry("id", ""))
#
# Methods for GUI
#
@pyqtSlot("QVariant", result=bool)
def canMaterialBeRemoved(self, material_node: "MaterialNode"):
# Check if the material is active in any extruder train. In that case, the material shouldn't be removed!
# In the future we might enable this again, but right now, it's causing a ton of issues if we do (since it
# corrupts the configuration)
root_material_id = material_node.getMetaDataEntry("base_file")
material_group = self.getMaterialGroup(root_material_id)
if not material_group:
return False
nodes_to_remove = [material_group.root_material_node] + material_group.derived_material_node_list
ids_to_remove = [node.getMetaDataEntry("id", "") for node in nodes_to_remove]
for extruder_stack in self._container_registry.findContainerStacks(type="extruder_train"):
if extruder_stack.material.getId() in ids_to_remove:
return False
return True
@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

View File

@ -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. # 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: # Its subcontainers are quality profiles.
# - 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.
#
#
class MaterialNode(ContainerNode): 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: container_registry = ContainerRegistry.getInstance()
super().__init__(metadata = metadata) my_metadata = container_registry.findContainersMetadata(id = container_id)[0]
self.material_map = {} # type: Dict[str, MaterialNode] # material_root_id -> material_node 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. ## Finds the preferred quality for this printer with this material and this
self.children_map = OrderedDict() # type: OrderedDict[str, "MaterialNode"] # 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"]: @UM.FlameProfiler.profile
return self.children_map.get(child_key) 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)

View File

@ -1,18 +1,22 @@
# Copyright (c) 2018 Ultimaker B.V. # Copyright (c) 2019 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
from typing import Optional, 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.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. ## 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. # Those 2 models are used by the material drop down menu to show generic materials and branded materials separately.
# The extruder position defined here is being used to bound a menu to the correct extruder. This is used in the top # The extruder position defined here is being used to bound a menu to the correct extruder. This is used in the top
# bar menu "Settings" -> "Extruder nr" -> "Material" -> this menu # bar menu "Settings" -> "Extruder nr" -> "Material" -> this menu
from cura.Machines.MaterialNode import MaterialNode
class BaseMaterialsModel(ListModel): class BaseMaterialsModel(ListModel):
extruderPositionChanged = pyqtSignal() extruderPositionChanged = pyqtSignal()
@ -24,19 +28,36 @@ class BaseMaterialsModel(ListModel):
self._application = CuraApplication.getInstance() 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 # Make these managers available to all material models
self._container_registry = self._application.getInstance().getContainerRegistry() self._container_registry = self._application.getInstance().getContainerRegistry()
self._machine_manager = self._application.getMachineManager() 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 # Update the stack and the model data when the machine changes
self._machine_manager.globalContainerChanged.connect(self._updateExtruderStack) self._machine_manager.globalContainerChanged.connect(self._updateExtruderStack)
self._updateExtruderStack()
# Update this model when switching machines # Update this model when switching machines or tabs, when adding materials or changing their metadata.
self._machine_manager.activeStackChanged.connect(self._update) self._machine_manager.activeStackChanged.connect(self._onChanged)
ContainerTree.getInstance().materialsChanged.connect(self._materialsListChanged)
# Update this model when list of materials changes self._application.getMaterialManagementModel().favoritesChanged.connect(self._onChanged)
self._material_manager.materialsUpdated.connect(self._update)
self.addRoleName(Qt.UserRole + 1, "root_material_id") self.addRoleName(Qt.UserRole + 1, "root_material_id")
self.addRoleName(Qt.UserRole + 2, "id") self.addRoleName(Qt.UserRole + 2, "id")
@ -55,12 +76,8 @@ class BaseMaterialsModel(ListModel):
self.addRoleName(Qt.UserRole + 15, "container_node") self.addRoleName(Qt.UserRole + 15, "container_node")
self.addRoleName(Qt.UserRole + 16, "is_favorite") self.addRoleName(Qt.UserRole + 16, "is_favorite")
self._extruder_position = 0 def _onChanged(self) -> None:
self._extruder_stack = None self._update_timer.start()
self._available_materials = None # type: Optional[Dict[str, MaterialNode]]
self._favorite_ids = set() # type: Set[str]
self._enabled = True
def _updateExtruderStack(self): def _updateExtruderStack(self):
global_stack = self._machine_manager.activeMachine global_stack = self._machine_manager.activeMachine
@ -68,14 +85,19 @@ class BaseMaterialsModel(ListModel):
return return
if self._extruder_stack is not None: if self._extruder_stack is not None:
self._extruder_stack.pyqtContainersChanged.disconnect(self._update) self._extruder_stack.pyqtContainersChanged.disconnect(self._onChanged)
self._extruder_stack.approximateMaterialDiameterChanged.disconnect(self._update) self._extruder_stack.approximateMaterialDiameterChanged.disconnect(self._onChanged)
self._extruder_stack = global_stack.extruders.get(str(self._extruder_position))
try:
self._extruder_stack = global_stack.extruderList[self._extruder_position]
except IndexError:
self._extruder_stack = None
if self._extruder_stack is not None: if self._extruder_stack is not None:
self._extruder_stack.pyqtContainersChanged.connect(self._update) self._extruder_stack.pyqtContainersChanged.connect(self._onChanged)
self._extruder_stack.approximateMaterialDiameterChanged.connect(self._update) self._extruder_stack.approximateMaterialDiameterChanged.connect(self._onChanged)
# Force update the model when the extruder stack changes # Force update the model when the extruder stack changes
self._update() self._onChanged()
def setExtruderPosition(self, position: int): def setExtruderPosition(self, position: int):
if self._extruder_stack is None or self._extruder_position != position: if self._extruder_stack is None or self._extruder_position != position:
@ -92,43 +114,76 @@ class BaseMaterialsModel(ListModel):
self._enabled = enabled self._enabled = enabled
if self._enabled: if self._enabled:
# ensure the data is there again. # ensure the data is there again.
self._update() self._onChanged()
self.enabledChanged.emit() self.enabledChanged.emit()
@pyqtProperty(bool, fset=setEnabled, notify=enabledChanged) @pyqtProperty(bool, fset = setEnabled, notify = enabledChanged)
def enabled(self): def enabled(self):
return self._enabled return self._enabled
## This is an abstract method that needs to be implemented by the specific ## Triggered when a list of materials changed somewhere in the container
# models themselves. # 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): 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 ## 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 # _update() method in order to prevent errors. It's the same in all models
# so it's placed here for easy access. # so it's placed here for easy access.
def _canUpdate(self): def _canUpdate(self):
global_stack = self._machine_manager.activeMachine global_stack = self._machine_manager.activeMachine
if global_stack is None or not self._enabled: if global_stack is None or not self._enabled:
return False return False
extruder_position = str(self._extruder_position) extruder_position = str(self._extruder_position)
if extruder_position not in global_stack.extruders: if extruder_position not in global_stack.extruders:
return False 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 return True
## This is another convenience function which is shared by all material ## This is another convenience function which is shared by all material
# models so it's put here to avoid having so much duplicated code. # models so it's put here to avoid having so much duplicated code.
def _createMaterialItem(self, root_material_id, container_node): 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 = { item = {
"root_material_id": root_material_id, "root_material_id": root_material_id,
"id": metadata["id"], "id": metadata["id"],
@ -149,4 +204,3 @@ class BaseMaterialsModel(ListModel):
"is_favorite": root_material_id in self._favorite_ids "is_favorite": root_material_id in self._favorite_ids
} }
return item return item

View File

@ -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. # Cura is released under the terms of the LGPLv3 or higher.
from PyQt5.QtCore import Qt from PyQt5.QtCore import Qt
from UM.Application import Application
from UM.Logger import Logger from UM.Logger import Logger
from UM.Qt.ListModel import ListModel from UM.Qt.ListModel import ListModel
from UM.Util import parseBool
from cura.Machines.VariantType import VariantType
class BuildPlateModel(ListModel): class BuildPlateModel(ListModel):
@ -21,31 +16,9 @@ class BuildPlateModel(ListModel):
self.addRoleName(self.NameRole, "name") self.addRoleName(self.NameRole, "name")
self.addRoleName(self.ContainerNodeRole, "container_node") 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() self._update()
def _update(self): def _update(self):
Logger.log("d", "Updating {model_class_name}.".format(model_class_name = self.__class__.__name__)) Logger.log("d", "Updating {model_class_name}.".format(model_class_name = self.__class__.__name__))
global_stack = self._machine_manager._global_container_stack self.setItems([])
if not global_stack: return
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)

View File

@ -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. # Cura is released under the terms of the LGPLv3 or higher.
from typing import Optional, TYPE_CHECKING
from UM.Logger import Logger 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 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): 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__)) 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: if active_global_stack is None:
self.setItems([]) self.setItems([])
Logger.log("d", "No active GlobalStack, set %s as empty.", self.__class__.__name__) Logger.log("d", "No active GlobalStack, set %s as empty.", self.__class__.__name__)
return return
quality_changes_group_dict = self._quality_manager.getQualityChangesGroups(active_global_stack) quality_changes_list = ContainerTree.getInstance().getCurrentQualityChangesGroups()
item_list = [] item_list = []
for key in sorted(quality_changes_group_dict, key = lambda name: name.upper()): for quality_changes_group in sorted(quality_changes_list, key = lambda qgc: qgc.name.lower()):
quality_changes_group = quality_changes_group_dict[key]
item = {"name": quality_changes_group.name, item = {"name": quality_changes_group.name,
"layer_height": "", "layer_height": "",
"layer_height_without_unit": "", "layer_height_without_unit": "",

View File

@ -11,7 +11,6 @@ from UM.Util import parseBool
from UM.OutputDevice.OutputDeviceManager import ManualDeviceAdditionAttempt from UM.OutputDevice.OutputDeviceManager import ManualDeviceAdditionAttempt
if TYPE_CHECKING: if TYPE_CHECKING:
from PyQt5.QtCore import QObject
from UM.OutputDevice.OutputDevicePlugin import OutputDevicePlugin from UM.OutputDevice.OutputDevicePlugin import OutputDevicePlugin
from cura.CuraApplication import CuraApplication from cura.CuraApplication import CuraApplication
from cura.PrinterOutput.NetworkedPrinterOutputDevice import NetworkedPrinterOutputDevice from cura.PrinterOutput.NetworkedPrinterOutputDevice import NetworkedPrinterOutputDevice
@ -62,32 +61,46 @@ class DiscoveredPrinter(QObject):
self._machine_type = machine_type self._machine_type = machine_type
self.machineTypeChanged.emit() 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 # Human readable machine type string
@pyqtProperty(str, notify = machineTypeChanged) @pyqtProperty(str, notify = machineTypeChanged)
def readableMachineType(self) -> str: def readableMachineType(self) -> str:
from cura.CuraApplication import CuraApplication from cura.CuraApplication import CuraApplication
machine_manager = CuraApplication.getInstance().getMachineManager() machine_manager = CuraApplication.getInstance().getMachineManager()
# In ClusterUM3OutputDevice, when it updates a printer information, it updates the machine type using the field # 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 # "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. # like "Ultimaker 3". The code below handles this case.
if machine_manager.hasHumanReadableMachineTypeName(self._machine_type): if self._hasHumanReadableMachineTypeName(self._machine_type):
readable_type = self._machine_type readable_type = self._machine_type
else: else:
readable_type = machine_manager.getMachineTypeNameFromId(self._machine_type) readable_type = self._getMachineTypeNameFromId(self._machine_type)
if not readable_type: if not readable_type:
readable_type = catalog.i18nc("@label", "Unknown") readable_type = catalog.i18nc("@label", "Unknown")
return readable_type return readable_type
@pyqtProperty(bool, notify = machineTypeChanged) @pyqtProperty(bool, notify = machineTypeChanged)
def isUnknownMachineType(self) -> bool: def isUnknownMachineType(self) -> bool:
from cura.CuraApplication import CuraApplication if self._hasHumanReadableMachineTypeName(self._machine_type):
machine_manager = CuraApplication.getInstance().getMachineManager()
if machine_manager.hasHumanReadableMachineTypeName(self._machine_type):
readable_type = self._machine_type readable_type = self._machine_type
else: else:
readable_type = machine_manager.getMachineTypeNameFromId(self._machine_type) readable_type = self._getMachineTypeNameFromId(self._machine_type)
return not readable_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) @pyqtProperty(QObject, constant = True)
def device(self) -> "NetworkedPrinterOutputDevice": def device(self) -> "NetworkedPrinterOutputDevice":
return self._device return self._device

View File

@ -1,28 +1,33 @@
# Copyright (c) 2018 Ultimaker B.V. # Copyright (c) 2019 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
from cura.Machines.Models.BaseMaterialsModel import BaseMaterialsModel 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. ## Model that shows the list of favorite materials.
class FavoriteMaterialsModel(BaseMaterialsModel): class FavoriteMaterialsModel(BaseMaterialsModel):
def __init__(self, parent = None): def __init__(self, parent = None):
super().__init__(parent) super().__init__(parent)
self._update() 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): def _update(self):
if not self._canUpdate(): if not self._canUpdate():
return return
super()._update()
# Get updated list of favorites
self._favorite_ids = self._material_manager.getFavorites()
item_list = [] item_list = []
for root_material_id, container_node in self._available_materials.items(): 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 # 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 continue
# Only add results for favorite materials # Only add results for favorite materials
@ -30,7 +35,8 @@ class FavoriteMaterialsModel(BaseMaterialsModel):
continue continue
item = self._createMaterialItem(root_material_id, container_node) item = self._createMaterialItem(root_material_id, container_node)
item_list.append(item) if item:
item_list.append(item)
# Sort the item list alphabetically by name # Sort the item list alphabetically by name
item_list = sorted(item_list, key = lambda d: d["brand"].upper()) item_list = sorted(item_list, key = lambda d: d["brand"].upper())

View File

@ -33,11 +33,11 @@ class FirstStartMachineActionsModel(ListModel):
self._current_action_index = 0 self._current_action_index = 0
self._application = application self._application = application
self._application.initializationFinished.connect(self._initialize) self._application.initializationFinished.connect(self.initialize)
self._previous_global_stack = None self._previous_global_stack = None
def _initialize(self) -> None: def initialize(self) -> None:
self._application.getMachineManager().globalContainerChanged.connect(self._update) self._application.getMachineManager().globalContainerChanged.connect(self._update)
self._update() self._update()

View File

@ -1,4 +1,4 @@
# Copyright (c) 2018 Ultimaker B.V. # Copyright (c) 2019 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
from cura.Machines.Models.BaseMaterialsModel import BaseMaterialsModel from cura.Machines.Models.BaseMaterialsModel import BaseMaterialsModel
@ -7,30 +7,27 @@ class GenericMaterialsModel(BaseMaterialsModel):
def __init__(self, parent = None): def __init__(self, parent = None):
super().__init__(parent) super().__init__(parent)
self._update() self._onChanged()
def _update(self): def _update(self):
if not self._canUpdate(): if not self._canUpdate():
return return
super()._update()
# Get updated list of favorites
self._favorite_ids = self._material_manager.getFavorites()
item_list = [] item_list = []
for root_material_id, container_node in self._available_materials.items(): 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 # 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 continue
# Only add results for generic materials # Only add results for generic materials
if metadata["brand"].lower() != "generic": if container_node.getMetaDataEntry("brand", "unknown").lower() != "generic":
continue continue
item = self._createMaterialItem(root_material_id, container_node) item = self._createMaterialItem(root_material_id, container_node)
item_list.append(item) if item:
item_list.append(item)
# Sort the item list alphabetically by name # Sort the item list alphabetically by name
item_list = sorted(item_list, key = lambda d: d["name"].upper()) item_list = sorted(item_list, key = lambda d: d["name"].upper())

View 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)

View 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)

View 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.")
}

View 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)

View File

@ -1,4 +1,4 @@
# Copyright (c) 2018 Ultimaker B.V. # Copyright (c) 2019 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
from PyQt5.QtCore import Qt, pyqtSignal from PyQt5.QtCore import Qt, pyqtSignal
@ -29,8 +29,7 @@ class MaterialBrandsModel(BaseMaterialsModel):
def _update(self): def _update(self):
if not self._canUpdate(): if not self._canUpdate():
return return
# Get updated list of favorites super()._update()
self._favorite_ids = self._material_manager.getFavorites()
brand_item_list = [] brand_item_list = []
brand_group_dict = {} brand_group_dict = {}
@ -55,7 +54,8 @@ class MaterialBrandsModel(BaseMaterialsModel):
# Now handle the individual materials # Now handle the individual materials
item = self._createMaterialItem(root_material_id, container_node) item = self._createMaterialItem(root_material_id, container_node)
brand_group_dict[brand][material_type].append(item) if item:
brand_group_dict[brand][material_type].append(item)
# Part 2: Organize the tree into models # Part 2: Organize the tree into models
# #

View 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))

View File

@ -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. # Cura is released under the terms of the LGPLv3 or higher.
from PyQt5.QtCore import Qt from PyQt5.QtCore import Qt
from UM.Application import Application
from UM.Logger import Logger from UM.Logger import Logger
from UM.Qt.ListModel import ListModel from UM.Qt.ListModel import ListModel
from UM.Util import parseBool import cura.CuraApplication # Imported like this to prevent circular dependencies.
from cura.Machines.ContainerTree import ContainerTree
from cura.Machines.VariantType import VariantType
class NozzleModel(ListModel): class NozzleModel(ListModel):
@ -23,33 +21,24 @@ class NozzleModel(ListModel):
self.addRoleName(self.HotendNameRole, "hotend_name") self.addRoleName(self.HotendNameRole, "hotend_name")
self.addRoleName(self.ContainerNodeRole, "container_node") self.addRoleName(self.ContainerNodeRole, "container_node")
self._application = Application.getInstance() cura.CuraApplication.CuraApplication.getInstance().getMachineManager().globalContainerChanged.connect(self._update)
self._machine_manager = self._application.getMachineManager()
self._variant_manager = self._application.getVariantManager()
self._machine_manager.globalContainerChanged.connect(self._update)
self._update() self._update()
def _update(self): def _update(self):
Logger.log("d", "Updating {model_class_name}.".format(model_class_name = self.__class__.__name__)) Logger.log("d", "Updating {model_class_name}.".format(model_class_name = self.__class__.__name__))
global_stack = self._machine_manager.activeMachine global_stack = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack()
if global_stack is None: if global_stack is None:
self.setItems([]) self.setItems([])
return return
machine_node = ContainerTree.getInstance().machines[global_stack.definition.getId()]
has_variants = parseBool(global_stack.getMetaDataEntry("has_variants", False)) if not machine_node.has_variants:
if not has_variants:
self.setItems([])
return
variant_node_dict = self._variant_manager.getVariantNodes(global_stack, VariantType.NOZZLE)
if not variant_node_dict:
self.setItems([]) self.setItems([])
return return
item_list = [] 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, item = {"id": hotend_name,
"hotend_name": hotend_name, "hotend_name": hotend_name,
"container_node": container_node "container_node": container_node
@ -57,4 +46,4 @@ class NozzleModel(ListModel):
item_list.append(item) item_list.append(item)
self.setItems(item_list) self.setItems(item_list)

View File

@ -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. # 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.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. # This the QML model for the quality management page.
@ -13,26 +33,257 @@ class QualityManagementModel(ListModel):
NameRole = Qt.UserRole + 1 NameRole = Qt.UserRole + 1
IsReadOnlyRole = Qt.UserRole + 2 IsReadOnlyRole = Qt.UserRole + 2
QualityGroupRole = Qt.UserRole + 3 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) super().__init__(parent)
self.addRoleName(self.NameRole, "name") self.addRoleName(self.NameRole, "name")
self.addRoleName(self.IsReadOnlyRole, "is_read_only") self.addRoleName(self.IsReadOnlyRole, "is_read_only")
self.addRoleName(self.QualityGroupRole, "quality_group") self.addRoleName(self.QualityGroupRole, "quality_group")
self.addRoleName(self.QualityTypeRole, "quality_type")
self.addRoleName(self.QualityChangesGroupRole, "quality_changes_group") self.addRoleName(self.QualityChangesGroupRole, "quality_changes_group")
self.addRoleName(self.IntentCategoryRole, "intent_category")
self.addRoleName(self.SectionNameRole, "section_name")
from cura.CuraApplication import CuraApplication application = cura.CuraApplication.CuraApplication.getInstance()
self._container_registry = CuraApplication.getInstance().getContainerRegistry() container_registry = application.getContainerRegistry()
self._machine_manager = CuraApplication.getInstance().getMachineManager() self._machine_manager = application.getMachineManager()
self._extruder_manager = CuraApplication.getInstance().getExtruderManager() self._machine_manager.activeQualityGroupChanged.connect(self._onChange)
self._quality_manager = CuraApplication.getInstance().getQualityManager() 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._extruder_manager = application.getExtruderManager()
self._quality_manager.qualitiesUpdated.connect(self._update) self._extruder_manager.extrudersChanged.connect(self._onChange)
self._update() 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): def _update(self):
Logger.log("d", "Updating {model_class_name}.".format(model_class_name = self.__class__.__name__)) Logger.log("d", "Updating {model_class_name}.".format(model_class_name = self.__class__.__name__))
@ -42,38 +293,71 @@ class QualityManagementModel(ListModel):
self.setItems([]) self.setItems([])
return return
quality_group_dict = self._quality_manager.getQualityGroups(global_stack) container_tree = ContainerTree.getInstance()
quality_changes_group_dict = self._quality_manager.getQualityChangesGroups(global_stack) 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() available_quality_types = set(quality_type for quality_type, quality_group in quality_group_dict.items()
if quality_group.is_available) 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 # Nothing to show
self.setItems([]) self.setItems([])
return return
item_list = [] item_list = []
# Create quality group items # Create quality group items (intent category = "default")
for quality_group in quality_group_dict.values(): for quality_group in quality_group_dict.values():
if not quality_group.is_available: if not quality_group.is_available:
continue continue
layer_height = fetchLayerHeight(quality_group)
item = {"name": quality_group.name, item = {"name": quality_group.name,
"is_read_only": True, "is_read_only": True,
"quality_group": quality_group, "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) item_list.append(item)
# Sort by quality names # Sort by layer_height for built-in qualities
item_list = sorted(item_list, key = lambda x: x["name"].upper()) 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 # Create quality_changes group items
quality_changes_item_list = [] 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_group = quality_group_dict.get(quality_changes_group.quality_type)
quality_type = quality_changes_group.quality_type
item = {"name": quality_changes_group.name, item = {"name": quality_changes_group.name,
"is_read_only": False, "is_read_only": False,
"quality_group": quality_group, "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) quality_changes_item_list.append(item)
# Sort quality_changes items by names and append to the item list # Sort quality_changes items by names and append to the item list

View File

@ -1,14 +1,13 @@
# Copyright (c) 2018 Ultimaker B.V. # Copyright (c) 2019 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
from PyQt5.QtCore import Qt, QTimer 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.Logger import Logger
from UM.Qt.ListModel import ListModel 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
# #
@ -36,14 +35,17 @@ class QualityProfilesDropDownMenuModel(ListModel):
self.addRoleName(self.QualityChangesGroupRole, "quality_changes_group") self.addRoleName(self.QualityChangesGroupRole, "quality_changes_group")
self.addRoleName(self.IsExperimentalRole, "is_experimental") self.addRoleName(self.IsExperimentalRole, "is_experimental")
self._application = Application.getInstance() application = cura.CuraApplication.CuraApplication.getInstance()
self._machine_manager = self._application.getMachineManager() machine_manager = application.getMachineManager()
self._quality_manager = Application.getInstance().getQualityManager()
self._application.globalContainerStackChanged.connect(self._onChange) application.globalContainerStackChanged.connect(self._onChange)
self._machine_manager.activeQualityGroupChanged.connect(self._onChange) machine_manager.activeQualityGroupChanged.connect(self._onChange)
self._machine_manager.extruderChanged.connect(self._onChange) machine_manager.activeMaterialChanged.connect(self._onChange)
self._quality_manager.qualitiesUpdated.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 self._layer_height_unit = "" # This is cached
@ -60,25 +62,36 @@ class QualityProfilesDropDownMenuModel(ListModel):
def _update(self): def _update(self):
Logger.log("d", "Updating {model_class_name}.".format(model_class_name = self.__class__.__name__)) Logger.log("d", "Updating {model_class_name}.".format(model_class_name = self.__class__.__name__))
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: if global_stack is None:
self.setItems([]) self.setItems([])
Logger.log("d", "No active GlobalStack, set quality profile model as empty.") Logger.log("d", "No active GlobalStack, set quality profile model as empty.")
return 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 # 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.") Logger.log("d", "No active material compatibility, set quality profile model as empty.")
self.setItems([]) self.setItems([])
return return
quality_group_dict = self._quality_manager.getQualityGroups(global_stack) quality_group_dict = ContainerTree.getInstance().getCurrentQualityGroups()
item_list = [] item_list = []
for key in sorted(quality_group_dict): for quality_group in quality_group_dict.values():
quality_group = quality_group_dict[key] layer_height = fetchLayerHeight(quality_group)
layer_height = self._fetchLayerHeight(quality_group)
item = {"name": quality_group.name, item = {"name": quality_group.name,
"quality_type": quality_group.quality_type, "quality_type": quality_group.quality_type,
@ -94,32 +107,3 @@ class QualityProfilesDropDownMenuModel(ListModel):
item_list = sorted(item_list, key = lambda x: x["layer_height"]) item_list = sorted(item_list, key = lambda x: x["layer_height"])
self.setItems(item_list) 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)

View File

@ -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. # Cura is released under the terms of the LGPLv3 or higher.
from PyQt5.QtCore import pyqtProperty, pyqtSignal, Qt from PyQt5.QtCore import pyqtProperty, pyqtSignal, Qt
from UM.Application import Application import cura.CuraApplication
from UM.Logger import Logger from UM.Logger import Logger
from UM.Qt.ListModel import ListModel from UM.Qt.ListModel import ListModel
from UM.Settings.ContainerRegistry import ContainerRegistry from UM.Settings.ContainerRegistry import ContainerRegistry
@ -35,15 +35,13 @@ class QualitySettingsModel(ListModel):
self.addRoleName(self.CategoryRole, "category") self.addRoleName(self.CategoryRole, "category")
self._container_registry = ContainerRegistry.getInstance() self._container_registry = ContainerRegistry.getInstance()
self._application = Application.getInstance() self._application = cura.CuraApplication.CuraApplication.getInstance()
self._quality_manager = self._application.getQualityManager() 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_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._selected_quality_item = None # The selected quality in the quality management page
self._i18n_catalog = None self._i18n_catalog = None
self._quality_manager.qualitiesUpdated.connect(self._update)
self._update() self._update()
selectedPositionChanged = pyqtSignal() selectedPositionChanged = pyqtSignal()
@ -93,21 +91,33 @@ class QualitySettingsModel(ListModel):
quality_node = quality_group.nodes_for_extruders.get(str(self._selected_position)) quality_node = quality_group.nodes_for_extruders.get(str(self._selected_position))
settings_keys = quality_group.getAllKeys() settings_keys = quality_group.getAllKeys()
quality_containers = [] quality_containers = []
if quality_node is not None and quality_node.getContainer() is not None: if quality_node is not None and quality_node.container is not None:
quality_containers.append(quality_node.getContainer()) 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 # 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. # the settings in that quality_changes_group.
if quality_changes_group is not None: if quality_changes_group is not None:
if self._selected_position == self.GLOBAL_STACK_POSITION: container_registry = ContainerRegistry.getInstance()
quality_changes_node = quality_changes_group.node_for_global 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: else:
quality_changes_node = quality_changes_group.nodes_for_extruders.get(str(self._selected_position)) quality_changes_metadata = extruders_container.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 if quality_changes_metadata is not None: # It can be None if number of extruders are changed during runtime.
quality_containers.insert(0, quality_changes_node.getContainer()) container = container_registry.findContainers(id = quality_changes_metadata["id"])
settings_keys.update(quality_changes_group.getAllKeys()) 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 # 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. # which setting belongs in which category.
current_category = "" current_category = ""

View File

@ -50,7 +50,7 @@ class UserChangesModel(ListModel):
return return
stacks = [global_stack] 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. # Check if the definition container has a translation file and ensure it's loaded.
definition = global_stack.getBottom() definition = global_stack.getBottom()

View File

@ -1,33 +1,37 @@
# Copyright (c) 2018 Ultimaker B.V. # Copyright (c) 2019 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
from typing import TYPE_CHECKING from typing import Any, Dict, Optional
from UM.Application import Application from PyQt5.QtCore import QObject, pyqtProperty, pyqtSignal
from UM.ConfigurationErrorMessage import ConfigurationErrorMessage
from .QualityGroup import QualityGroup
if TYPE_CHECKING:
from cura.Machines.QualityNode import QualityNode
class QualityChangesGroup(QualityGroup): ## Data struct to group several quality changes instance containers together.
def __init__(self, name: str, quality_type: str, parent = None) -> None: #
super().__init__(name, quality_type, parent) # Each group represents one "custom profile" as the user sees it, which
self._container_registry = Application.getInstance().getContainerRegistry() # contains an instance container for the global stack and one instance
# container per extruder.
class QualityChangesGroup(QObject):
def addNode(self, node: "QualityNode") -> None: def __init__(self, name: str, quality_type: str, intent_category: str, parent: Optional["QObject"] = None) -> None:
extruder_position = node.getMetaDataEntry("position") 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. nameChanged = pyqtSignal()
ConfigurationErrorMessage.getInstance().addFaultyContainers(node.getMetaDataEntry("id"))
return
if extruder_position is None: # Then we're a global quality changes profile. def setName(self, name: str) -> None:
self.node_for_global = node if self._name != name:
else: # This is an extruder's quality changes profile. self._name = name
self.nodes_for_extruders[extruder_position] = node self.nameChanged.emit()
@pyqtProperty(str, fset = setName, notify = nameChanged)
def name(self) -> str:
return self._name
def __str__(self) -> str: 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)

View File

@ -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. # Cura is released under the terms of the LGPLv3 or higher.
from typing import Dict, Optional, List, Set from typing import Dict, Optional, List, Set
from PyQt5.QtCore import QObject, pyqtSlot from PyQt5.QtCore import QObject, pyqtSlot
from UM.Logger import Logger
from UM.Util import parseBool from UM.Util import parseBool
from cura.Machines.ContainerNode import ContainerNode 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. # A concrete example: When there are two extruders and the user selects the
# Some concrete examples are Quality and QualityChanges: when we select quality type "normal", this quality type # quality type "normal", this quality type must be applied to all stacks in a
# must be applied to all stacks in a machine, although each stack can have different containers. Use an Ultimaker 3 # machine, although each stack can have different containers. So one global
# as an example, suppose we choose quality type "normal", the actual InstanceContainers on each stack may look # profile gets put on the global stack and one extruder profile gets put on
# as below: # each extruder stack. This quality group then contains the following
# GlobalStack ExtruderStack 1 ExtruderStack 2 # profiles (for instance):
# quality container: um3_global_normal um3_aa04_pla_normal um3_aa04_abs_normal # 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 # The purpose of these quality groups is to group the containers that can be
# a machine, so when a quality/custom quality is selected, the container can be directly applied to each stack instead # applied to a configuration, so that when a quality level is selected, the
# of looking them up again. # container can directly be applied to each stack instead of looking them up
# # again.
class QualityGroup(QObject): class QualityGroup:
## Constructs a new group.
def __init__(self, name: str, quality_type: str, parent = None) -> None: # \param name The user-visible name for the group.
super().__init__(parent) # \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.name = name
self.node_for_global = None # type: Optional[ContainerNode] self.node_for_global = None # type: Optional[ContainerNode]
self.nodes_for_extruders = {} # type: Dict[int, ContainerNode] self.nodes_for_extruders = {} # type: Dict[int, ContainerNode]
@ -34,7 +40,6 @@ class QualityGroup(QObject):
self.is_available = False self.is_available = False
self.is_experimental = False self.is_experimental = False
@pyqtSlot(result = str)
def getName(self) -> str: def getName(self) -> str:
return self.name return self.name
@ -43,7 +48,7 @@ class QualityGroup(QObject):
for node in [self.node_for_global] + list(self.nodes_for_extruders.values()): for node in [self.node_for_global] + list(self.nodes_for_extruders.values()):
if node is None: if node is None:
continue continue
container = node.getContainer() container = node.container
if container: if container:
result.update(container.getAllKeys()) result.update(container.getAllKeys())
return result return result
@ -60,6 +65,9 @@ class QualityGroup(QObject):
self.node_for_global = node self.node_for_global = node
# Update is_experimental flag # 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)) is_experimental = parseBool(node.getMetaDataEntry("is_experimental", False))
self.is_experimental |= is_experimental self.is_experimental |= is_experimental
@ -67,5 +75,8 @@ class QualityGroup(QObject):
self.nodes_for_extruders[position] = node self.nodes_for_extruders[position] = node
# Update is_experimental flag # 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)) is_experimental = parseBool(node.getMetaDataEntry("is_experimental", False))
self.is_experimental |= is_experimental self.is_experimental |= is_experimental

View File

@ -1,552 +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_name = self._container_registry.uniqueName(quality_changes_name)
new_quality_changes = self._createQualityChanges(quality_group.quality_type, new_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

View File

@ -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. # 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 UM.Settings.ContainerRegistry import ContainerRegistry
from .QualityChangesGroup import QualityChangesGroup 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): 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: my_metadata = ContainerRegistry.getInstance().findContainersMetadata(id = container_id)[0]
super().__init__(metadata = metadata) self.quality_type = my_metadata["quality_type"]
self.quality_type_map = {} # type: Dict[str, QualityNode] # quality_type -> QualityNode for InstanceContainer # 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"]: @UM.FlameProfiler.profile
return self.children_map.get(child_key) def _loadAll(self) -> None:
container_registry = ContainerRegistry.getInstance()
def addQualityMetadata(self, quality_type: str, metadata: Dict[str, Any]): # Find all intent profiles that fit the current configuration.
if quality_type not in self.quality_type_map: from cura.Machines.MachineNode import MachineNode
self.quality_type_map[quality_type] = QualityNode(metadata) 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"]: self.intents["empty_intent"] = IntentNode("empty_intent", quality = self)
return self.quality_type_map.get(quality_type) # Otherwise, there are no intents for global profiles.
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))

View File

@ -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

View File

@ -0,0 +1,188 @@
# 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 = None
for submaterial in materials_same_base_file:
if submaterial["definition"] == self.machine.container_id:
if submaterial.get("variant_name", "empty") == self.variant_name:
most_specific_submaterial = submaterial
break # most specific match possible
if submaterial.get("variant_name", "empty") == "empty":
most_specific_submaterial = submaterial
if most_specific_submaterial is None:
Logger.log("w", "Material %s removed, but no suitable replacement found", base_file)
else:
Logger.log("i", "Material %s (%s) overridden by %s", base_file, self.variant_name, most_specific_submaterial.get("id"))
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"])

View File

@ -2,10 +2,12 @@
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
import copy import copy
from typing import List
from UM.Job import Job from UM.Job import Job
from UM.Operations.GroupedOperation import GroupedOperation from UM.Operations.GroupedOperation import GroupedOperation
from UM.Message import Message from UM.Message import Message
from UM.Scene.SceneNode import SceneNode
from UM.i18n import i18nCatalog from UM.i18n import i18nCatalog
i18n_catalog = i18nCatalog("cura") i18n_catalog = i18nCatalog("cura")
@ -23,7 +25,7 @@ class MultiplyObjectsJob(Job):
self._count = count self._count = count
self._min_offset = min_offset 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, 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")) dismissable=False, progress=0, title = i18n_catalog.i18nc("@info:title", "Placing Objects"))
status_message.show() status_message.show()
@ -33,13 +35,15 @@ class MultiplyObjectsJob(Job):
current_progress = 0 current_progress = 0
global_container_stack = Application.getInstance().getGlobalContainerStack() 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_width = global_container_stack.getProperty("machine_width", "value")
machine_depth = global_container_stack.getProperty("machine_depth", "value") machine_depth = global_container_stack.getProperty("machine_depth", "value")
root = scene.getRoot() root = scene.getRoot()
scale = 0.5 scale = 0.5
arranger = Arrange.create(x = machine_width, y = machine_depth, scene_root = root, scale = scale, min_offset = self._min_offset) 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 = [] nodes = []
not_fit_count = 0 not_fit_count = 0
@ -67,7 +71,11 @@ class MultiplyObjectsJob(Job):
new_node = copy.deepcopy(node) new_node = copy.deepcopy(node)
solution_found = False solution_found = False
if not node_too_big: if not node_too_big:
solution_found = arranger.findNodePlacement(new_node, offset_shape_arr, hull_shape_arr) 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: if node_too_big or not solution_found:
found_solution_for_all = False found_solution_for_all = False

View File

@ -99,7 +99,7 @@ class AuthorizationHelpers:
}) })
except requests.exceptions.ConnectionError: except requests.exceptions.ConnectionError:
# Connection was suddenly dropped. Nothing we can do about that. # Connection was suddenly dropped. Nothing we can do about that.
Logger.log("w", "Something failed while attempting to parse the JWT token") Logger.logException("w", "Something failed while attempting to parse the JWT token")
return None return None
if token_request.status_code not in (200, 201): if token_request.status_code not in (200, 201):
Logger.log("w", "Could not retrieve token data from auth server: %s", token_request.text) Logger.log("w", "Could not retrieve token data from auth server: %s", token_request.text)

View File

@ -25,6 +25,10 @@ class AuthorizationRequestHandler(BaseHTTPRequestHandler):
self.authorization_callback = None # type: Optional[Callable[[AuthenticationResponse], None]] self.authorization_callback = None # type: Optional[Callable[[AuthenticationResponse], None]]
self.verification_code = None # type: Optional[str] self.verification_code = None # type: Optional[str]
# 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: def do_GET(self) -> None:
# Extract values from the query string. # Extract values from the query string.
parsed_url = urlparse(self.path) parsed_url = urlparse(self.path)

View File

@ -2,12 +2,14 @@
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
import json import json
import webbrowser
from datetime import datetime, timedelta from datetime import datetime, timedelta
from typing import Optional, TYPE_CHECKING from typing import Optional, TYPE_CHECKING
from urllib.parse import urlencode from urllib.parse import urlencode
import requests.exceptions import requests.exceptions
from PyQt5.QtCore import QUrl
from PyQt5.QtGui import QDesktopServices
from UM.Logger import Logger from UM.Logger import Logger
from UM.Message import Message from UM.Message import Message
@ -163,7 +165,7 @@ class AuthorizationService:
}) })
# Open the authorization page in a new browser window. # 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. # Start a local web server to receive the callback URL on.
self._server.start(verification_code) self._server.start(verification_code)
@ -195,8 +197,6 @@ class AuthorizationService:
self._unable_to_get_data_message.hide() 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 = Message(i18n_catalog.i18nc("@info", "Unable to reach the Ultimaker account server."), title = i18n_catalog.i18nc("@info:title", "Warning"))
self._unable_to_get_data_message.addAction("retry", i18n_catalog.i18nc("@action:button", "Retry"), "[no_icon]", "[no_description]")
self._unable_to_get_data_message.actionTriggered.connect(self._onMessageActionTriggered)
self._unable_to_get_data_message.show() self._unable_to_get_data_message.show()
except ValueError: except ValueError:
Logger.logException("w", "Could not load auth data from preferences") Logger.logException("w", "Could not load auth data from preferences")
@ -218,6 +218,3 @@ class AuthorizationService:
self.accessTokenChanged.emit() self.accessTokenChanged.emit()
def _onMessageActionTriggered(self, _, action):
if action == "retry":
self.loadAuthDataFromPreferences()

View File

@ -63,6 +63,10 @@ class LocalAuthorizationServer:
Logger.log("d", "Stopping local oauth2 web server...") Logger.log("d", "Stopping local oauth2 web server...")
if self._web_server: if self._web_server:
self._web_server.server_close() 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 = None
self._web_server_thread = None self._web_server_thread = None

View File

@ -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. # Cura is released under the terms of the LGPLv3 or higher.
import sys from typing import List
from shapely import affinity from UM.Scene.Iterator import Iterator
from shapely.geometry import Polygon
from UM.Scene.Iterator.Iterator import Iterator
from UM.Scene.SceneNode import SceneNode from UM.Scene.SceneNode import SceneNode
from functools import cmp_to_key
## 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.
# Iterator that determines the object print order when one-at a time mode is enabled. ## Fills the ``_node_stack`` with a list of scene nodes that need to be
# # printed in order.
# In one-at-a-time mode, only one extruder can be enabled to print. In order to maximize the number of objects we can def _fillStack(self) -> None:
# 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 = []
super().__init__(scene_node) # Call super to make multiple inheritance work.
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
node_list = [] node_list = []
for node in self._scene_node.getChildren(): for node in self._scene_node.getChildren():
if not issubclass(type(node), SceneNode): if not issubclass(type(node), SceneNode):
continue continue
convex_hull = node.callDecoration("getConvexHull") if node.callDecoration("getConvexHull"):
if convex_hull: node_list.append(node)
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)
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, if len(node_list) < 2:
"min_coord": [convex_hull_polygon.bounds[0], convex_hull_polygon.bounds[1]], 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

View File

@ -17,6 +17,7 @@ from cura.Scene import ZOffsetDecorator
import random # used for list shuffling import random # used for list shuffling
class PlatformPhysics: class PlatformPhysics:
def __init__(self, controller, volume): def __init__(self, controller, volume):
super().__init__() super().__init__()
@ -49,18 +50,20 @@ class PlatformPhysics:
return return
root = self._controller.getScene().getRoot() 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 # Keep a list of nodes that are moving. We use this so that we don't move two intersecting objects in the
# same direction. # same direction.
transformed_nodes = [] 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)) nodes = list(BreadthFirstIterator(root))
# Only check nodes inside build area. # Only check nodes inside build area.
nodes = [node for node in nodes if (hasattr(node, "_outside_buildarea") and not node._outside_buildarea)] 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) random.shuffle(nodes)
for node in nodes: for node in nodes:
if node is root or not isinstance(node, SceneNode) or node.getBoundingBox() is None: if node is root or not isinstance(node, SceneNode) or node.getBoundingBox() is None:
@ -160,7 +163,6 @@ class PlatformPhysics:
op.push() op.push()
# After moving, we have to evaluate the boundary checks for nodes # After moving, we have to evaluate the boundary checks for nodes
build_volume = Application.getInstance().getBuildVolume()
build_volume.updateNodeBoundaryCheck() build_volume.updateNodeBoundaryCheck()
def _onToolOperationStarted(self, tool): def _onToolOperationStarted(self, tool):

View File

@ -1,7 +1,8 @@
# Copyright (c) 2018 Ultimaker B.V. # Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
from typing import Optional, TYPE_CHECKING from typing import Optional, TYPE_CHECKING, cast
from UM.Application import Application from UM.Application import Application
from UM.Resources import Resources from UM.Resources import Resources
@ -12,6 +13,7 @@ from UM.View.RenderBatch import RenderBatch
from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
from cura.Scene.CuraSceneNode import CuraSceneNode
if TYPE_CHECKING: if TYPE_CHECKING:
from UM.View.GL.ShaderProgram import ShaderProgram from UM.View.GL.ShaderProgram import ShaderProgram
@ -44,9 +46,9 @@ class PreviewPass(RenderPass):
self._renderer = Application.getInstance().getRenderer() self._renderer = Application.getInstance().getRenderer()
self._shader = None #type: Optional[ShaderProgram] self._shader = None # type: Optional[ShaderProgram]
self._non_printing_shader = None #type: Optional[ShaderProgram] self._non_printing_shader = None # type: Optional[ShaderProgram]
self._support_mesh_shader = None #type: Optional[ShaderProgram] self._support_mesh_shader = None # type: Optional[ShaderProgram]
self._scene = Application.getInstance().getController().getScene() self._scene = Application.getInstance().getController().getScene()
# Set the camera to be used by this render pass # Set the camera to be used by this render pass
@ -62,6 +64,7 @@ class PreviewPass(RenderPass):
self._shader.setUniformValue("u_ambientColor", [0.1, 0.1, 0.1, 1.0]) 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_specularColor", [0.6, 0.6, 0.6, 1.0])
self._shader.setUniformValue("u_shininess", 20.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 not self._non_printing_shader:
if self._non_printing_shader: if self._non_printing_shader:
@ -83,8 +86,8 @@ class PreviewPass(RenderPass):
batch_support_mesh = RenderBatch(self._support_mesh_shader) batch_support_mesh = RenderBatch(self._support_mesh_shader)
# Fill up the batch with objects that can be sliced. # Fill up the batch with objects that can be sliced.
for node in DepthFirstIterator(self._scene.getRoot()): #type: ignore #Ignore type error because iter() should get called automatically by Python syntax. for node in DepthFirstIterator(self._scene.getRoot()):
if hasattr(node, "_outside_buildarea") and not node._outside_buildarea: if hasattr(node, "_outside_buildarea") and not getattr(node, "_outside_buildarea"):
if node.callDecoration("isSliceable") and node.getMeshData() and node.isVisible(): if node.callDecoration("isSliceable") and node.getMeshData() and node.isVisible():
per_mesh_stack = node.callDecoration("getStack") per_mesh_stack = node.callDecoration("getStack")
if node.callDecoration("isNonThumbnailVisibleMesh"): if node.callDecoration("isNonThumbnailVisibleMesh"):
@ -94,7 +97,7 @@ class PreviewPass(RenderPass):
# Support mesh # Support mesh
uniforms = {} uniforms = {}
shade_factor = 0.6 shade_factor = 0.6
diffuse_color = node.getDiffuseColor() diffuse_color = cast(CuraSceneNode, node).getDiffuseColor()
diffuse_color2 = [ diffuse_color2 = [
diffuse_color[0] * shade_factor, diffuse_color[0] * shade_factor,
diffuse_color[1] * shade_factor, diffuse_color[1] * shade_factor,
@ -106,7 +109,7 @@ class PreviewPass(RenderPass):
else: else:
# Normal scene node # Normal scene node
uniforms = {} uniforms = {}
uniforms["diffuse_color"] = prettier_color(node.getDiffuseColor()) uniforms["diffuse_color"] = prettier_color(cast(CuraSceneNode, node).getDiffuseColor())
batch.addItem(node.getWorldTransformation(), node.getMeshData(), uniforms = uniforms) batch.addItem(node.getWorldTransformation(), node.getMeshData(), uniforms = uniforms)
self.bind() self.bind()

View File

@ -20,7 +20,7 @@ class FirmwareUpdater(QObject):
self._output_device = output_device 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_file = ""
self._firmware_progress = 0 self._firmware_progress = 0
@ -43,7 +43,7 @@ class FirmwareUpdater(QObject):
## Cleanup after a succesful update ## Cleanup after a succesful update
def _cleanupAfterUpdate(self) -> None: def _cleanupAfterUpdate(self) -> None:
# Clean up for next attempt. # 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._firmware_file = ""
self._onFirmwareProgress(100) self._onFirmwareProgress(100)
self._setFirmwareUpdateState(FirmwareUpdateState.completed) self._setFirmwareUpdateState(FirmwareUpdateState.completed)

View File

@ -55,7 +55,7 @@ class GenericOutputController(PrinterOutputController):
self._preheat_hotends_timer.stop() self._preheat_hotends_timer.stop()
for extruder in self._preheat_hotends: for extruder in self._preheat_hotends:
extruder.updateIsPreheating(False) extruder.updateIsPreheating(False)
self._preheat_hotends = set() # type: Set[ExtruderOutputModel] self._preheat_hotends = set()
def moveHead(self, printer: "PrinterOutputModel", x, y, z, speed) -> None: def moveHead(self, printer: "PrinterOutputModel", x, y, z, speed) -> None:
self._output_device.sendCommand("G91") self._output_device.sendCommand("G91")
@ -159,7 +159,7 @@ class GenericOutputController(PrinterOutputController):
def _onPreheatHotendsTimerFinished(self) -> None: def _onPreheatHotendsTimerFinished(self) -> None:
for extruder in self._preheat_hotends: for extruder in self._preheat_hotends:
self.setTargetHotendTemperature(extruder.getPrinter(), extruder.getPosition(), 0) self.setTargetHotendTemperature(extruder.getPrinter(), extruder.getPosition(), 0)
self._preheat_hotends = set() #type: Set[ExtruderOutputModel] self._preheat_hotends = set()
# Cancel any ongoing preheating timers, without setting back the temperature to 0 # Cancel any ongoing preheating timers, without setting back the temperature to 0
# This can be used eg at the start of a print # This can be used eg at the start of a print
@ -167,7 +167,7 @@ class GenericOutputController(PrinterOutputController):
if self._preheat_hotends_timer.isActive(): if self._preheat_hotends_timer.isActive():
for extruder in self._preheat_hotends: for extruder in self._preheat_hotends:
extruder.updateIsPreheating(False) extruder.updateIsPreheating(False)
self._preheat_hotends = set() #type: Set[ExtruderOutputModel] self._preheat_hotends = set()
self._preheat_hotends_timer.stop() self._preheat_hotends_timer.stop()

View File

@ -25,15 +25,16 @@ class ExtruderConfigurationModel(QObject):
return self._position return self._position
def setMaterial(self, material: Optional[MaterialOutputModel]) -> None: def setMaterial(self, material: Optional[MaterialOutputModel]) -> None:
if self._hotend_id != material: if material is None or self._material == material:
self._material = material return
self.extruderConfigurationChanged.emit() self._material = material
self.extruderConfigurationChanged.emit()
@pyqtProperty(QObject, fset = setMaterial, notify = extruderConfigurationChanged) @pyqtProperty(QObject, fset = setMaterial, notify = extruderConfigurationChanged)
def activeMaterial(self) -> Optional[MaterialOutputModel]: def activeMaterial(self) -> Optional[MaterialOutputModel]:
return self._material return self._material
@pyqtProperty(QObject, fset=setMaterial, notify=extruderConfigurationChanged) @pyqtProperty(QObject, fset = setMaterial, notify = extruderConfigurationChanged)
def material(self) -> Optional[MaterialOutputModel]: def material(self) -> Optional[MaterialOutputModel]:
return self._material return self._material

View File

@ -34,3 +34,11 @@ class MaterialOutputModel(QObject):
@pyqtProperty(str, constant = True) @pyqtProperty(str, constant = True)
def name(self) -> str: def name(self) -> str:
return self._name 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

View File

@ -58,6 +58,14 @@ class PrinterConfigurationModel(QObject):
return False return False
return self._printer_type != "" 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): def __str__(self):
message_chunks = [] message_chunks = []
message_chunks.append("Printer type: " + self._printer_type) message_chunks.append("Printer type: " + self._printer_type)

View File

@ -2,13 +2,14 @@
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
from PyQt5.QtCore import pyqtSignal, pyqtProperty, QObject, QVariant, pyqtSlot, QUrl from PyQt5.QtCore import pyqtSignal, pyqtProperty, QObject, QVariant, pyqtSlot, QUrl
from typing import List, Dict, Optional from typing import List, Dict, Optional, TYPE_CHECKING
from UM.Math.Vector import Vector from UM.Math.Vector import Vector
from cura.PrinterOutput.Peripheral import Peripheral
from cura.PrinterOutput.Models.PrinterConfigurationModel import PrinterConfigurationModel from cura.PrinterOutput.Models.PrinterConfigurationModel import PrinterConfigurationModel
from cura.PrinterOutput.Models.ExtruderOutputModel import ExtruderOutputModel from cura.PrinterOutput.Models.ExtruderOutputModel import ExtruderOutputModel
from UM.Logger import Logger
MYPY = False if TYPE_CHECKING:
if MYPY:
from cura.PrinterOutput.Models.PrintJobOutputModel import PrintJobOutputModel from cura.PrinterOutput.Models.PrintJobOutputModel import PrintJobOutputModel
from cura.PrinterOutput.PrinterOutputController import PrinterOutputController from cura.PrinterOutput.PrinterOutputController import PrinterOutputController
@ -34,10 +35,11 @@ class PrinterOutputModel(QObject):
self._target_bed_temperature = 0 # type: float self._target_bed_temperature = 0 # type: float
self._name = "" self._name = ""
self._key = "" # Unique identifier self._key = "" # Unique identifier
self._unique_name = "" # Unique name (used in Connect)
self._controller = output_controller self._controller = output_controller
self._controller.canUpdateFirmwareChanged.connect(self._onControllerCanUpdateFirmwareChanged) self._controller.canUpdateFirmwareChanged.connect(self._onControllerCanUpdateFirmwareChanged)
self._extruders = [ExtruderOutputModel(printer = self, position = i) for i in range(number_of_extruders)] self._extruders = [ExtruderOutputModel(printer = self, position = i) for i in range(number_of_extruders)]
self._printer_configuration = PrinterConfigurationModel() # Indicates the current configuration setup in this printer self._active_printer_configuration = PrinterConfigurationModel() # Indicates the current configuration setup in this printer
self._head_position = Vector(0, 0, 0) self._head_position = Vector(0, 0, 0)
self._active_print_job = None # type: Optional[PrintJobOutputModel] self._active_print_job = None # type: Optional[PrintJobOutputModel]
self._firmware_version = firmware_version self._firmware_version = firmware_version
@ -45,9 +47,12 @@ class PrinterOutputModel(QObject):
self._is_preheating = False self._is_preheating = False
self._printer_type = "" self._printer_type = ""
self._buildplate = "" self._buildplate = ""
self._peripherals = [] # type: List[Peripheral]
self._printer_configuration.extruderConfigurations = [extruder.extruderConfiguration for extruder in self._active_printer_configuration.extruderConfigurations = [extruder.extruderConfiguration for extruder in
self._extruders] self._extruders]
self._active_printer_configuration.configurationChanged.connect(self.configurationChanged)
self._available_printer_configurations = [] # type: List[PrinterConfigurationModel]
self._camera_url = QUrl() # type: QUrl self._camera_url = QUrl() # type: QUrl
@ -80,7 +85,7 @@ class PrinterOutputModel(QObject):
def updateType(self, printer_type: str) -> None: def updateType(self, printer_type: str) -> None:
if self._printer_type != printer_type: if self._printer_type != printer_type:
self._printer_type = printer_type self._printer_type = printer_type
self._printer_configuration.printerType = self._printer_type self._active_printer_configuration.printerType = self._printer_type
self.typeChanged.emit() self.typeChanged.emit()
self.configurationChanged.emit() self.configurationChanged.emit()
@ -91,7 +96,7 @@ class PrinterOutputModel(QObject):
def updateBuildplate(self, buildplate: str) -> None: def updateBuildplate(self, buildplate: str) -> None:
if self._buildplate != buildplate: if self._buildplate != buildplate:
self._buildplate = buildplate self._buildplate = buildplate
self._printer_configuration.buildplateConfiguration = self._buildplate self._active_printer_configuration.buildplateConfiguration = self._buildplate
self.buildplateChanged.emit() self.buildplateChanged.emit()
self.configurationChanged.emit() self.configurationChanged.emit()
@ -186,6 +191,15 @@ class PrinterOutputModel(QObject):
self._name = name self._name = name
self.nameChanged.emit() 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. ## Update the bed temperature. This only changes it locally.
def updateBedTemperature(self, temperature: float) -> None: def updateBedTemperature(self, temperature: float) -> None:
if self._bed_temperature != temperature: if self._bed_temperature != temperature:
@ -289,9 +303,48 @@ class PrinterOutputModel(QObject):
def _onControllerCanUpdateFirmwareChanged(self) -> None: def _onControllerCanUpdateFirmwareChanged(self) -> None:
self.canUpdateFirmwareChanged.emit() self.canUpdateFirmwareChanged.emit()
# Returns the configuration (material, variant and buildplate) of the current printer # Returns the active configuration (material, variant and buildplate) of the current printer
@pyqtProperty(QObject, notify = configurationChanged) @pyqtProperty(QObject, notify = configurationChanged)
def printerConfiguration(self) -> Optional[PrinterConfigurationModel]: def printerConfiguration(self) -> Optional[PrinterConfigurationModel]:
if self._printer_configuration.isValid(): if self._active_printer_configuration.isValid():
return self._printer_configuration return self._active_printer_configuration
return None 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()

View File

@ -35,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: 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) super().__init__(device_id = device_id, connection_type = connection_type, parent = parent)
self._manager = None # type: Optional[QNetworkAccessManager] self._manager = None # type: Optional[QNetworkAccessManager]
self._last_manager_create_time = None # type: Optional[float]
self._recreate_network_manager_time = 30
self._timeout_time = 10 # After how many seconds of no response should a timeout occur? self._timeout_time = 10 # After how many seconds of no response should a timeout occur?
self._last_response_time = None # type: Optional[float] self._last_response_time = None # type: Optional[float]
@ -60,8 +58,8 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice):
self._gcode = [] # type: List[str] self._gcode = [] # type: List[str]
self._connection_state_before_timeout = None # type: Optional[ConnectionState] self._connection_state_before_timeout = None # type: Optional[ConnectionState]
def requestWrite(self, nodes: List[SceneNode], file_name: Optional[str] = None, limit_mimetypes: bool = False, def requestWrite(self, nodes: List["SceneNode"], file_name: Optional[str] = None, limit_mimetypes: bool = False,
file_handler: Optional[FileHandler] = None, **kwargs: str) -> None: file_handler: Optional["FileHandler"] = None, filter_by_machine: bool = False, **kwargs) -> None:
raise NotImplementedError("requestWrite needs to be implemented") raise NotImplementedError("requestWrite needs to be implemented")
def setAuthenticationState(self, authentication_state: AuthState) -> None: def setAuthenticationState(self, authentication_state: AuthState) -> None:
@ -133,12 +131,6 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice):
self.setConnectionState(ConnectionState.Closed) self.setConnectionState(ConnectionState.Closed)
# We need to check if the manager needs to be re-created. If we don't, we get some issues when OSX goes to
# 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: elif self._connection_state == ConnectionState.Closed:
# Go out of timeout. # Go out of timeout.
if self._connection_state_before_timeout is not None: # sanity check, but it should never be None here if self._connection_state_before_timeout is not None: # sanity check, but it should never be None here
@ -162,7 +154,7 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice):
part = QHttpPart() part = QHttpPart()
if not content_header.startswith("form-data;"): 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) part.setHeader(QNetworkRequest.ContentDispositionHeader, content_header)
if content_type is not None: if content_type is not None:
@ -317,7 +309,6 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice):
self._manager = QNetworkAccessManager() self._manager = QNetworkAccessManager()
self._manager.finished.connect(self._handleOnFinished) self._manager.finished.connect(self._handleOnFinished)
self._last_manager_create_time = time()
self._manager.authenticationRequired.connect(self._onAuthenticationRequired) self._manager.authenticationRequired.connect(self._onAuthenticationRequired)
if self._properties.get(b"temporary", b"false") != b"true": if self._properties.get(b"temporary", b"false") != b"true":

View 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

View File

@ -10,7 +10,6 @@ from UM.Logger import Logger
from UM.Signal import signalemitter from UM.Signal import signalemitter
from UM.Qt.QtApplication import QtApplication from UM.Qt.QtApplication import QtApplication
from UM.FlameProfiler import pyqtSlot from UM.FlameProfiler import pyqtSlot
from UM.Decorators import deprecated
from UM.i18n import i18nCatalog from UM.i18n import i18nCatalog
from UM.OutputDevice.OutputDevice import OutputDevice from UM.OutputDevice.OutputDevice import OutputDevice
@ -144,7 +143,7 @@ class PrinterOutputDevice(QObject, OutputDevice):
return None return None
def requestWrite(self, nodes: List["SceneNode"], file_name: Optional[str] = None, limit_mimetypes: bool = False, def requestWrite(self, nodes: List["SceneNode"], file_name: Optional[str] = None, limit_mimetypes: bool = False,
file_handler: Optional["FileHandler"] = None, **kwargs: str) -> None: file_handler: Optional["FileHandler"] = None, filter_by_machine: bool = False, **kwargs) -> None:
raise NotImplementedError("requestWrite needs to be implemented") raise NotImplementedError("requestWrite needs to be implemented")
@pyqtProperty(QObject, notify = printersChanged) @pyqtProperty(QObject, notify = printersChanged)
@ -203,10 +202,6 @@ class PrinterOutputDevice(QObject, OutputDevice):
def acceptsCommands(self) -> bool: def acceptsCommands(self) -> bool:
return self._accepts_commands 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 ## Set a flag to signal the UI that the printer is not (yet) ready to receive commands
def _setAcceptsCommands(self, accepts_commands: bool) -> None: def _setAcceptsCommands(self, accepts_commands: bool) -> None:
if self._accepts_commands != accepts_commands: if self._accepts_commands != accepts_commands:
@ -220,20 +215,28 @@ class PrinterOutputDevice(QObject, OutputDevice):
return self._unique_configurations return self._unique_configurations
def _updateUniqueConfigurations(self) -> None: def _updateUniqueConfigurations(self) -> None:
self._unique_configurations = sorted( all_configurations = set()
{printer.printerConfiguration for printer in self._printers if printer.printerConfiguration is not None}, for printer in self._printers:
key=lambda config: config.printerType, if printer.printerConfiguration is not None and printer.printerConfiguration.hasAnyMaterialLoaded():
) all_configurations.add(printer.printerConfiguration)
self.uniqueConfigurationsChanged.emit() 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 # Returns the unique configurations of the printers within this output device
@pyqtProperty("QStringList", notify = uniqueConfigurationsChanged) @pyqtProperty("QStringList", notify = uniqueConfigurationsChanged)
def uniquePrinterTypes(self) -> List[str]: def uniquePrinterTypes(self) -> List[str]:
return list(sorted(set([configuration.printerType for configuration in self._unique_configurations]))) return list(sorted(set([configuration.printerType or "" for configuration in self._unique_configurations])))
def _onPrintersChanged(self) -> None: def _onPrintersChanged(self) -> None:
for printer in self._printers: for printer in self._printers:
printer.configurationChanged.connect(self._updateUniqueConfigurations) printer.configurationChanged.connect(self._updateUniqueConfigurations)
printer.availableConfigurationsChanged.connect(self._updateUniqueConfigurations)
# At this point there may be non-updated configurations # At this point there may be non-updated configurations
self._updateUniqueConfigurations() self._updateUniqueConfigurations()

View File

@ -4,12 +4,12 @@ from cura.Scene.CuraSceneNode import CuraSceneNode
## Make a SceneNode build plate aware CuraSceneNode objects all have this decorator. ## Make a SceneNode build plate aware CuraSceneNode objects all have this decorator.
class BuildPlateDecorator(SceneNodeDecorator): class BuildPlateDecorator(SceneNodeDecorator):
def __init__(self, build_plate_number = -1): def __init__(self, build_plate_number: int = -1) -> None:
super().__init__() super().__init__()
self._build_plate_number = None self._build_plate_number = build_plate_number
self.setBuildPlateNumber(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 # Make sure that groups are set correctly
# setBuildPlateForSelection in CuraActions makes sure that no single childs are set. # setBuildPlateForSelection in CuraActions makes sure that no single childs are set.
self._build_plate_number = nr self._build_plate_number = nr
@ -19,7 +19,7 @@ class BuildPlateDecorator(SceneNodeDecorator):
for child in self._node.getChildren(): for child in self._node.getChildren():
child.callDecoration("setBuildPlateNumber", nr) child.callDecoration("setBuildPlateNumber", nr)
def getBuildPlateNumber(self): def getBuildPlateNumber(self) -> int:
return self._build_plate_number return self._build_plate_number
def __deepcopy__(self, memo): def __deepcopy__(self, memo):

View File

@ -76,27 +76,46 @@ class ConvexHullDecorator(SceneNodeDecorator):
def __deepcopy__(self, memo): def __deepcopy__(self, memo):
return ConvexHullDecorator() return ConvexHullDecorator()
## 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) ## 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]: def getConvexHull(self) -> Optional[Polygon]:
if self._node is None: if self._node is None:
return None return None
if self._node.callDecoration("isNonPrintingMesh"): if self._node.callDecoration("isNonPrintingMesh"):
return None return None
hull = self._compute2DConvexHull()
if self._global_stack and self._node is not None and hull is not None: # Parent can be None if node is just loaded.
# Parent can be None if node is just loaded. if self._isSingularOneAtATimeNode():
if self._global_stack.getProperty("print_sequence", "value") == "one_at_a_time" and not self.hasGroupAsParent(self._node): hull = self.getConvexHullHeadFull()
hull = hull.getMinkowskiHull(Polygon(numpy.array(self._global_stack.getProperty("machine_head_polygon", "value"), numpy.float32))) if hull is None:
hull = self._add2DAdhesionMargin(hull) return None
return hull 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]: def getConvexHullHeadFull(self) -> Optional[Polygon]:
if self._node is None: if self._node is None:
return None return None
return self._compute2DConvexHeadFull() if self._isSingularOneAtATimeNode():
return self._compute2DConvexHeadFull()
return None
@staticmethod @staticmethod
def hasGroupAsParent(node: "SceneNode") -> bool: def hasGroupAsParent(node: "SceneNode") -> bool:
@ -106,38 +125,47 @@ class ConvexHullDecorator(SceneNodeDecorator):
return bool(parent.callDecoration("isGroup")) return bool(parent.callDecoration("isGroup"))
## Get convex hull of the object + head size ## 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 # For one at the time this is area with intersection of mirrored head
def getConvexHullHead(self) -> Optional[Polygon]: def getConvexHullHead(self) -> Optional[Polygon]:
if self._node is None: if self._node is None:
return None return None
if self._node.callDecoration("isNonPrintingMesh"): if self._node.callDecoration("isNonPrintingMesh"):
return None return None
if self._global_stack: if self._isSingularOneAtATimeNode():
if self._global_stack.getProperty("print_sequence", "value") == "one_at_a_time" and not self.hasGroupAsParent(self._node): head_with_fans = self._compute2DConvexHeadMin()
head_with_fans = self._compute2DConvexHeadMin() if head_with_fans is None:
if head_with_fans is None: return None
return None head_with_fans_with_adhesion_margin = self._add2DAdhesionMargin(head_with_fans)
head_with_fans_with_adhesion_margin = self._add2DAdhesionMargin(head_with_fans) return head_with_fans_with_adhesion_margin
return head_with_fans_with_adhesion_margin
return None return None
## Get convex hull of the node ## 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. # For one at the time this is the area without the head.
def getConvexHullBoundary(self) -> Optional[Polygon]: def getConvexHullBoundary(self) -> Optional[Polygon]:
if self._node is None: if self._node is None:
return None return None
if self._node.callDecoration("isNonPrintingMesh"): if self._node.callDecoration("isNonPrintingMesh"):
return None return None
if self._global_stack: if self._isSingularOneAtATimeNode():
if self._global_stack.getProperty("print_sequence", "value") == "one_at_a_time" and not self.hasGroupAsParent(self._node): # Printing one at a time and it's not an object in a group
# Printing one at a time and it's not an object in a group return self._compute2DConvexHull()
return self._compute2DConvexHull()
return None 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. ## The same as recomputeConvexHull, but using a timer if it was set.
def recomputeConvexHullDelayed(self) -> None: def recomputeConvexHullDelayed(self) -> None:
if self._recompute_convex_hull_timer is not None: if self._recompute_convex_hull_timer is not None:
@ -160,10 +188,9 @@ class ConvexHullDecorator(SceneNodeDecorator):
self._convex_hull_node = None self._convex_hull_node = None
return return
convex_hull = self.getConvexHull()
if self._convex_hull_node: if self._convex_hull_node:
self._convex_hull_node.setParent(None) 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 self._convex_hull_node = hull_node
def _onSettingValueChanged(self, key: str, property_name: str) -> None: def _onSettingValueChanged(self, key: str, property_name: str) -> None:
@ -266,9 +293,13 @@ class ConvexHullDecorator(SceneNodeDecorator):
return offset_hull return offset_hull
def _getHeadAndFans(self) -> Polygon: def _getHeadAndFans(self) -> Polygon:
if self._global_stack: if not self._global_stack:
return Polygon(numpy.array(self._global_stack.getHeadAndFansCoordinates(), numpy.float32)) return Polygon()
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]: def _compute2DConvexHeadFull(self) -> Optional[Polygon]:
convex_hull = self._compute2DConvexHull() convex_hull = self._compute2DConvexHull()
@ -400,6 +431,14 @@ class ConvexHullDecorator(SceneNodeDecorator):
return True return True
return self.__isDescendant(root, node.getParent()) 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 = [ _affected_settings = [
"adhesion_type", "raft_margin", "print_sequence", "adhesion_type", "raft_margin", "print_sequence",
"skirt_gap", "skirt_line_count", "skirt_brim_line_width", "skirt_distance", "brim_line_count"] "skirt_gap", "skirt_line_count", "skirt_brim_line_width", "skirt_distance", "brim_line_count"]

View File

@ -1,6 +1,6 @@
# Copyright (c) 2015 Ultimaker B.V. # Copyright (c) 2015 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
from typing import Optional from typing import Optional, TYPE_CHECKING
from UM.Application import Application from UM.Application import Application
from UM.Math.Polygon import Polygon 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.Mesh.MeshBuilder import MeshBuilder # To create a mesh to display the convex hull with.
from UM.View.GL.OpenGL import OpenGL from UM.View.GL.OpenGL import OpenGL
if TYPE_CHECKING:
from UM.Mesh.MeshData import MeshData
class ConvexHullNode(SceneNode): class ConvexHullNode(SceneNode):
shader = None # To prevent the shader from being re-built over and over again, only load it once. 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" # The node this mesh is "watching"
self._node = node 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._node.decoratorsChanged.connect(self._onNodeDecoratorsChanged)
self._onNodeDecoratorsChanged(self._node) self._onNodeDecoratorsChanged(self._node)
@ -76,14 +80,17 @@ class ConvexHullNode(SceneNode):
if self.getParent(): if self.getParent():
if self.getMeshData() and isinstance(self._node, SceneNode) and self._node.callDecoration("getBuildPlateNumber") == Application.getInstance().getMultiBuildPlateModel().activeBuildPlate: 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) renderer.queueNode(self, transparent = True, shader = ConvexHullNode.shader, backface_cull = True, sort = -8)
if self._convex_hull_head_mesh: 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) renderer.queueNode(self, shader = ConvexHullNode.shader, transparent = True, mesh = self._convex_hull_head_mesh, backface_cull = True, sort = -8)
return True return True
def _onNodeDecoratorsChanged(self, node: SceneNode) -> None: def _onNodeDecoratorsChanged(self, node: SceneNode) -> None:
convex_hull_head = self._node.callDecoration("getConvexHullHead") convex_hull_head = self._node.callDecoration("getConvexHullHeadFull")
if convex_hull_head: if convex_hull_head:
convex_hull_head_builder = MeshBuilder() convex_hull_head_builder = MeshBuilder()
convex_hull_head_builder.addConvexPolygon(convex_hull_head.getPoints(), self._mesh_height - self._thickness) convex_hull_head_builder.addConvexPolygon(convex_hull_head.getPoints(), self._mesh_height - self._thickness)

View File

@ -1,4 +1,4 @@
# Copyright (c) 2018 Ultimaker B.V. # Copyright (c) 2019 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
from copy import deepcopy from copy import deepcopy
@ -6,13 +6,14 @@ from typing import cast, Dict, List, Optional
from UM.Application import Application from UM.Application import Application
from UM.Math.AxisAlignedBox import AxisAlignedBox from UM.Math.AxisAlignedBox import AxisAlignedBox
from UM.Math.Polygon import Polygon #For typing. from UM.Math.Polygon import Polygon # For typing.
from UM.Scene.SceneNode import SceneNode from UM.Scene.SceneNode import SceneNode
from UM.Scene.SceneNodeDecorator import SceneNodeDecorator #To cast the deepcopy of every decorator back to SceneNodeDecorator. from UM.Scene.SceneNodeDecorator import SceneNodeDecorator # To cast the deepcopy of every decorator back to SceneNodeDecorator.
import cura.CuraApplication # To get the build plate.
from cura.Settings.ExtruderStack import ExtruderStack # For typing.
from cura.Settings.SettingOverrideDecorator import SettingOverrideDecorator # For per-object settings.
import cura.CuraApplication #To get the build plate.
from cura.Settings.ExtruderStack import ExtruderStack #For typing.
from cura.Settings.SettingOverrideDecorator import SettingOverrideDecorator #For per-object settings.
## Scene nodes that are models are only seen when selecting the corresponding build plate ## Scene nodes that are models are only seen when selecting the corresponding build plate
# Note that many other nodes can just be UM SceneNode objects. # Note that many other nodes can just be UM SceneNode objects.
@ -20,7 +21,7 @@ class CuraSceneNode(SceneNode):
def __init__(self, parent: Optional["SceneNode"] = None, visible: bool = True, name: str = "", no_setting_override: bool = False) -> None: def __init__(self, parent: Optional["SceneNode"] = None, visible: bool = True, name: str = "", no_setting_override: bool = False) -> None:
super().__init__(parent = parent, visible = visible, name = name) super().__init__(parent = parent, visible = visible, name = name)
if not no_setting_override: if not no_setting_override:
self.addDecorator(SettingOverrideDecorator()) # now we always have a getActiveExtruderPosition, unless explicitly disabled self.addDecorator(SettingOverrideDecorator()) # Now we always have a getActiveExtruderPosition, unless explicitly disabled
self._outside_buildarea = False self._outside_buildarea = False
def setOutsideBuildArea(self, new_value: bool) -> None: def setOutsideBuildArea(self, new_value: bool) -> None:
@ -58,7 +59,7 @@ class CuraSceneNode(SceneNode):
if extruder_id is not None: if extruder_id is not None:
if extruder_id == extruder.getId(): if extruder_id == extruder.getId():
return extruder return extruder
else: # If the id is unknown, then return the extruder in the position 0 else: # If the id is unknown, then return the extruder in the position 0
try: try:
if extruder.getMetaDataEntry("position", default = "0") == "0": # Check if the position is zero if extruder.getMetaDataEntry("position", default = "0") == "0": # Check if the position is zero
return extruder return extruder
@ -85,24 +86,14 @@ class CuraSceneNode(SceneNode):
1.0 1.0
] ]
## Return if the provided bbox collides with the bbox of this scene node
def collidesWithBbox(self, check_bbox: AxisAlignedBox) -> bool:
bbox = self.getBoundingBox()
if bbox is not None:
# Mark the node as outside the build volume if the bounding box test fails.
if check_bbox.intersectsBox(bbox) != AxisAlignedBox.IntersectionResult.FullIntersection:
return True
return False
## Return if any area collides with the convex hull of this scene node ## Return if any area collides with the convex hull of this scene node
def collidesWithArea(self, areas: List[Polygon]) -> bool: def collidesWithAreas(self, areas: List[Polygon]) -> bool:
convex_hull = self.callDecoration("getConvexHull") convex_hull = self.callDecoration("getPrintingArea")
if convex_hull: if convex_hull:
if not convex_hull.isValid(): if not convex_hull.isValid():
return False return False
# Check for collisions between disallowed areas and the object # Check for collisions between provided areas and the object
for area in areas: for area in areas:
overlap = convex_hull.intersectsPolygon(area) overlap = convex_hull.intersectsPolygon(area)
if overlap is None: if overlap is None:

View File

@ -1,11 +1,18 @@
from UM.Scene.SceneNodeDecorator import SceneNodeDecorator from UM.Scene.SceneNodeDecorator import SceneNodeDecorator
from typing import List from typing import List, Optional
class GCodeListDecorator(SceneNodeDecorator): class GCodeListDecorator(SceneNodeDecorator):
def __init__(self) -> None: def __init__(self) -> None:
super().__init__() super().__init__()
self._gcode_list = [] # type: List[str] 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]: def getGCodeList(self) -> List[str]:
return self._gcode_list return self._gcode_list

View File

@ -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. # Cura is released under the terms of the LGPLv3 or higher.
import os import os
import urllib.parse import urllib.parse
import uuid 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.QtCore import QObject, QUrl
from PyQt5.QtWidgets import QMessageBox from PyQt5.QtWidgets import QMessageBox
from UM.i18n import i18nCatalog from UM.i18n import i18nCatalog
from UM.FlameProfiler import pyqtSlot from UM.FlameProfiler import pyqtSlot
from UM.Logger import Logger from UM.Logger import Logger
@ -17,21 +16,19 @@ from UM.MimeTypeDatabase import MimeTypeDatabase, MimeTypeNotFoundError
from UM.Platform import Platform from UM.Platform import Platform
from UM.SaveFile import SaveFile from UM.SaveFile import SaveFile
from UM.Settings.ContainerFormatError import ContainerFormatError from UM.Settings.ContainerFormatError import ContainerFormatError
from UM.Settings.ContainerRegistry import ContainerRegistry
from UM.Settings.ContainerStack import ContainerStack from UM.Settings.ContainerStack import ContainerStack
from UM.Settings.DefinitionContainer import DefinitionContainer from UM.Settings.DefinitionContainer import DefinitionContainer
from UM.Settings.InstanceContainer import InstanceContainer from UM.Settings.InstanceContainer import InstanceContainer
import cura.CuraApplication
from cura.Machines.ContainerTree import ContainerTree
if TYPE_CHECKING: if TYPE_CHECKING:
from cura.CuraApplication import CuraApplication from cura.CuraApplication import CuraApplication
from cura.Machines.ContainerNode import ContainerNode from cura.Machines.ContainerNode import ContainerNode
from cura.Machines.MaterialNode import MaterialNode from cura.Machines.MaterialNode import MaterialNode
from cura.Machines.QualityChangesGroup import QualityChangesGroup 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") catalog = i18nCatalog("cura")
@ -52,17 +49,11 @@ class ContainerManager(QObject):
except TypeError: except TypeError:
super().__init__() 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]] self._container_name_filters = {} # type: Dict[str, Dict[str, Any]]
@pyqtSlot(str, str, result=str) @pyqtSlot(str, str, result=str)
def getContainerMetaDataEntry(self, container_id: str, entry_names: str) -> 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: if not metadatas:
Logger.log("w", "Could not get metadata of container %s because it was not found.", container_id) Logger.log("w", "Could not get metadata of container %s because it was not found.", container_id)
return "" return ""
@ -91,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? # 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) @pyqtSlot("QVariant", str, str)
def setContainerMetaDataEntry(self, container_node: "ContainerNode", entry_name: str, entry_value: str) -> bool: 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", "") 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) Logger.log("w", "Cannot set metadata of read-only container %s.", root_material_id)
return False return False
root_material_query = container_registry.findContainers(id = root_material_id)
material_group = self._material_manager.getMaterialGroup(root_material_id) if not root_material_query:
if material_group is None: Logger.log("w", "Unable to find root material: {root_material}.".format(root_material = root_material_id))
Logger.log("w", "Unable to find material group for: %s.", root_material_id)
return False return False
root_material = root_material_query[0]
entries = entry_name.split("/") entries = entry_name.split("/")
entry_name = entries.pop() entry_name = entries.pop()
@ -107,7 +102,7 @@ class ContainerManager(QObject):
sub_item_changed = False sub_item_changed = False
if entries: if entries:
root_name = entries.pop(0) root_name = entries.pop(0)
root = material_group.root_material_node.getMetaDataEntry(root_name) root = root_material.getMetaDataEntry(root_name)
item = root item = root
for _ in range(len(entries)): for _ in range(len(entries)):
@ -120,16 +115,14 @@ class ContainerManager(QObject):
entry_name = root_name entry_name = root_name
entry_value = root entry_value = root
container = material_group.root_material_node.getContainer() root_material.setMetaDataEntry(entry_name, entry_value)
if container is not None: 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.setMetaDataEntry(entry_name, entry_value) root_material.metaDataChanged.emit(root_material)
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)
return True return True
@pyqtSlot(str, result = str) @pyqtSlot(str, result = str)
def makeUniqueName(self, original_name: str) -> 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 ## Get a list of string that can be used as name filters for a Qt File Dialog
# #
@ -184,7 +177,7 @@ class ContainerManager(QObject):
else: else:
mime_type = self._container_name_filters[file_type]["mime"] 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: if not containers:
return {"status": "error", "message": "Container not found"} return {"status": "error", "message": "Container not found"}
container = containers[0] container = containers[0]
@ -242,18 +235,19 @@ class ContainerManager(QObject):
except MimeTypeNotFoundError: except MimeTypeNotFoundError:
return {"status": "error", "message": "Could not determine mime type of file"} 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: if not container_type:
return {"status": "error", "message": "Could not find a container to handle the specified file."} 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 = 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) container = container_type(container_id)
try: try:
with open(file_url, "rt", encoding = "utf-8") as f: with open(file_url, "rt", encoding = "utf-8") as f:
container.deserialize(f.read()) container.deserialize(f.read(), file_url)
except PermissionError: except PermissionError:
return {"status": "error", "message": "Permission denied when trying to read the file."} return {"status": "error", "message": "Permission denied when trying to read the file."}
except ContainerFormatError: except ContainerFormatError:
@ -263,7 +257,7 @@ class ContainerManager(QObject):
container.setDirty(True) container.setDirty(True)
self._container_registry.addContainer(container) container_registry.addContainer(container)
return {"status": "success", "message": "Successfully imported container {0}".format(container.getName())} return {"status": "success", "message": "Successfully imported container {0}".format(container.getName())}
@ -275,44 +269,55 @@ class ContainerManager(QObject):
# \return \type{bool} True if successful, False if not. # \return \type{bool} True if successful, False if not.
@pyqtSlot(result = bool) @pyqtSlot(result = bool)
def updateQualityChanges(self) -> 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: if not global_stack:
return False return False
self._machine_manager.blurSettings.emit() application.getMachineManager().blurSettings.emit()
current_quality_changes_name = global_stack.qualityChanges.getName() current_quality_changes_name = global_stack.qualityChanges.getName()
current_quality_type = global_stack.quality.getMetaDataEntry("quality_type") current_quality_type = global_stack.quality.getMetaDataEntry("quality_type")
extruder_stacks = list(global_stack.extruders.values()) 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: 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. # Find the quality_changes container for this stack and merge the contents of the top container into it.
quality_changes = stack.qualityChanges quality_changes = stack.qualityChanges
if quality_changes.getId() == "empty_quality_changes": if quality_changes.getId() == "empty_quality_changes":
quality_changes = self._quality_manager._createQualityChanges(current_quality_type, current_quality_changes_name, quality_changes = InstanceContainer(container_registry.uniqueName((stack.getId() + "_" + current_quality_changes_name).lower().replace(" ", "_")))
global_stack, stack) quality_changes.setName(current_quality_changes_name)
self._container_registry.addContainer(quality_changes) 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 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()) Logger.log("e", "Could not update quality of a nonexistant or read only quality profile in stack %s", stack.getId())
continue continue
self._performMerge(quality_changes, stack.getTop()) self._performMerge(quality_changes, stack.getTop())
self._machine_manager.activeQualityChangesGroupChanged.emit() cura.CuraApplication.CuraApplication.getInstance().getMachineManager().activeQualityChangesGroupChanged.emit()
return True return True
## Clear the top-most (user) containers of the active stacks. ## Clear the top-most (user) containers of the active stacks.
@pyqtSlot() @pyqtSlot()
def clearUserContainers(self) -> None: def clearUserContainers(self) -> None:
self._machine_manager.blurSettings.emit() machine_manager = cura.CuraApplication.CuraApplication.getInstance().getMachineManager()
machine_manager.blurSettings.emit()
send_emits_containers = [] send_emits_containers = []
# Go through global and extruder stacks and clear their topmost container (the user settings). # 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()) extruder_stacks = list(global_stack.extruders.values())
for stack in [global_stack] + extruder_stacks: for stack in [global_stack] + extruder_stacks:
container = stack.userChanges container = stack.userChanges
@ -320,40 +325,38 @@ class ContainerManager(QObject):
send_emits_containers.append(container) send_emits_containers.append(container)
# user changes are possibly added to make the current setup match the current enabled extruders # 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: for container in send_emits_containers:
container.sendPostponedEmits() container.sendPostponedEmits()
## Get a list of materials that have the same GUID as the reference material ## 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. # \param material_node The node representing the material for which to get
# \return \type{list} a list of names of materials with the same GUID # 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") @pyqtSlot("QVariant", bool, result = "QStringList")
def getLinkedMaterials(self, material_node: "MaterialNode", exclude_self: bool = False): def getLinkedMaterials(self, material_node: "MaterialNode", exclude_self: bool = False) -> List[str]:
guid = material_node.getMetaDataEntry("GUID", "") same_guid = ContainerRegistry.getInstance().findInstanceContainersMetadata(GUID = material_node.guid)
if exclude_self:
self_root_material_id = material_node.getMetaDataEntry("base_file") return list({meta["name"] for meta in same_guid if meta["base_file"] != material_node.base_file})
material_group_list = self._material_manager.getMaterialGroupListByGUID(guid) else:
return list({meta["name"] for meta in same_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
## Unlink a material from all other materials by creating a new 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. # \param material_id \type{str} the id of the material to create a new GUID for.
@pyqtSlot("QVariant") @pyqtSlot("QVariant")
def unlinkMaterial(self, material_node: "MaterialNode") -> None: def unlinkMaterial(self, material_node: "MaterialNode") -> None:
# Get the material group # Get the material group
material_group = self._material_manager.getMaterialGroup(material_node.getMetaDataEntry("base_file", "")) if material_node.container is None: # Failed to lazy-load this container.
return
if material_group is None: 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) Logger.log("w", "Unable to find material group for %s", material_node)
return return
root_material = root_material_query[0]
# Generate a new GUID # Generate a new GUID
new_guid = str(uuid.uuid4()) new_guid = str(uuid.uuid4())
@ -361,9 +364,7 @@ class ContainerManager(QObject):
# Update the GUID # Update the GUID
# NOTE: We only need to set the root material container because XmlMaterialProfile.setMetaDataEntry() will # NOTE: We only need to set the root material container because XmlMaterialProfile.setMetaDataEntry() will
# take care of the derived containers too # take care of the derived containers too
container = material_group.root_material_node.getContainer() root_material.setMetaDataEntry("GUID", new_guid)
if container is not None:
container.setMetaDataEntry("GUID", new_guid)
def _performMerge(self, merge_into: InstanceContainer, merge: InstanceContainer, clear_settings: bool = True) -> None: def _performMerge(self, merge_into: InstanceContainer, merge: InstanceContainer, clear_settings: bool = True) -> None:
if merge == merge_into: if merge == merge_into:
@ -377,14 +378,16 @@ class ContainerManager(QObject):
def _updateContainerNameFilters(self) -> None: def _updateContainerNameFilters(self) -> None:
self._container_name_filters = {} 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 # Ignore default container types since those are not plugins
if container_type in (InstanceContainer, ContainerStack, DefinitionContainer): if container_type in (InstanceContainer, ContainerStack, DefinitionContainer):
continue continue
serialize_type = "" serialize_type = ""
try: try:
plugin_metadata = self._plugin_registry.getMetaData(plugin_id) plugin_metadata = plugin_registry.getMetaData(plugin_id)
if plugin_metadata: if plugin_metadata:
serialize_type = plugin_metadata["settings_container"]["type"] serialize_type = plugin_metadata["settings_container"]["type"]
else: else:
@ -392,7 +395,7 @@ class ContainerManager(QObject):
except KeyError as e: except KeyError as e:
continue continue
mime_type = self._container_registry.getMimeTypeForContainer(container_type) mime_type = container_registry.getMimeTypeForContainer(container_type)
if mime_type is None: if mime_type is None:
continue continue
entry = { entry = {
@ -428,7 +431,7 @@ class ContainerManager(QObject):
path = file_url.toLocalFile() path = file_url.toLocalFile()
if not path: if not path:
return {"status": "error", "message": catalog.i18nc("@info:status", "Invalid file URL:") + " " + str(file_url)} 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) @pyqtSlot(QObject, QUrl, str)
def exportQualityChangesGroup(self, quality_changes_group: "QualityChangesGroup", file_url: QUrl, file_type: str) -> None: def exportQualityChangesGroup(self, quality_changes_group: "QualityChangesGroup", file_url: QUrl, file_type: str) -> None:
@ -438,8 +441,11 @@ class ContainerManager(QObject):
if not path: if not path:
return return
container_list = [n.getContainer() for n in quality_changes_group.getAllNodes() if n.getContainer() is not None] container_registry = cura.CuraApplication.CuraApplication.getInstance().getContainerRegistry()
self._container_registry.exportQualityProfile(container_list, path, file_type) 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 __instance = None # type: ContainerManager

View File

@ -5,7 +5,7 @@ import os
import re import re
import configparser import configparser
from typing import Any, cast, Dict, Optional from typing import Any, cast, Dict, Optional, List, Union
from PyQt5.QtWidgets import QMessageBox from PyQt5.QtWidgets import QMessageBox
from UM.Decorators import override from UM.Decorators import override
@ -20,14 +20,16 @@ from UM.Logger import Logger
from UM.Message import Message from UM.Message import Message
from UM.Platform import Platform from UM.Platform import Platform
from UM.PluginRegistry import PluginRegistry # For getting the possible profile writers to write with. 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.Resources import Resources
from UM.Util import parseBool
from cura.ReaderWriters.ProfileWriter import ProfileWriter
from . import ExtruderStack from . import ExtruderStack
from . import GlobalStack from . import GlobalStack
import cura.CuraApplication 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 cura.ReaderWriters.ProfileReader import NoProfileException, ProfileReader
from UM.i18n import i18nCatalog from UM.i18n import i18nCatalog
@ -50,10 +52,10 @@ class CuraContainerRegistry(ContainerRegistry):
# This will also try to convert a ContainerStack to either Extruder or # This will also try to convert a ContainerStack to either Extruder or
# Global stack based on metadata information. # Global stack based on metadata information.
@override(ContainerRegistry) @override(ContainerRegistry)
def addContainer(self, container): def addContainer(self, container: ContainerInterface) -> None:
# Note: Intentional check with type() because we want to ignore subclasses # Note: Intentional check with type() because we want to ignore subclasses
if type(container) == ContainerStack: if type(container) == ContainerStack:
container = self._convertContainerStack(container) container = self._convertContainerStack(cast(ContainerStack, container))
if isinstance(container, InstanceContainer) and type(container) != type(self.getEmptyInstanceContainer()): if isinstance(container, InstanceContainer) and type(container) != type(self.getEmptyInstanceContainer()):
# Check against setting version of the definition. # Check against setting version of the definition.
@ -61,7 +63,7 @@ class CuraContainerRegistry(ContainerRegistry):
actual_setting_version = int(container.getMetaDataEntry("setting_version", default = 0)) actual_setting_version = int(container.getMetaDataEntry("setting_version", default = 0))
if required_setting_version != actual_setting_version: 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)) 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) super().addContainer(container)
@ -71,9 +73,9 @@ class CuraContainerRegistry(ContainerRegistry):
# \param new_name \type{string} Base name, which may not be unique # \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 # \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 # \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() 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: if num_check:
new_name = num_check.group(1) new_name = num_check.group(1)
if new_name == "": 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 # 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_type \type{string} Type of the container (machine, quality, ...)
# \param container_name \type{string} Name to check # \param container_name \type{string} Name to check
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 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 \ return self.findContainersMetadata(container_type = container_class, id = container_name, type = container_type, ignore_case = True) or \
@ -100,11 +102,12 @@ class CuraContainerRegistry(ContainerRegistry):
## Exports an profile to a file ## 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_name \type{str} the full path and filename to export to.
# \param file_type \type{str} the file type with the format "<description> (*.<extension>)" # \param file_type \type{str} the file type with the format "<description> (*.<extension>)"
# \return True if the export succeeded, false otherwise. # \return True if the export succeeded, false otherwise.
def exportQualityProfile(self, container_list, file_name, file_type) -> bool: 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. # Parse the fileType to deduce what plugin can save the file format.
# fileType has the format "<description> (*.<extension>)" # fileType has the format "<description> (*.<extension>)"
split = file_type.rfind(" (*.") # Find where the description ends and the extension starts. split = file_type.rfind(" (*.") # Find where the description ends and the extension starts.
@ -126,6 +129,8 @@ class CuraContainerRegistry(ContainerRegistry):
profile_writer = self._findProfileWriter(extension, description) profile_writer = self._findProfileWriter(extension, description)
try: try:
if profile_writer is None:
raise Exception("Unable to find a profile writer")
success = profile_writer.write(file_name, container_list) success = profile_writer.write(file_name, container_list)
except Exception as e: except Exception as e:
Logger.log("e", "Failed to export profile to %s: %s", file_name, str(e)) Logger.log("e", "Failed to export profile to %s: %s", file_name, str(e))
@ -150,7 +155,7 @@ class CuraContainerRegistry(ContainerRegistry):
# \param extension # \param extension
# \param description # \param description
# \return The plugin object matching the given extension and 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() plugin_registry = PluginRegistry.getInstance()
for plugin_id, meta_data in self._getIOPlugins("profile_writer"): 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. for supported_type in meta_data["profile_writer"]: # All file types this plugin can supposedly write.
@ -158,7 +163,7 @@ class CuraContainerRegistry(ContainerRegistry):
if supported_extension == extension: # This plugin supports a file type with the same extension. if supported_extension == extension: # This plugin supports a file type with the same extension.
supported_description = supported_type.get("description", None) supported_description = supported_type.get("description", None)
if supported_description == description: # The description is also identical. Assume it's the same file type. 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 return None
## Imports a profile from a file ## Imports a profile from a file
@ -174,6 +179,7 @@ class CuraContainerRegistry(ContainerRegistry):
global_stack = Application.getInstance().getGlobalContainerStack() global_stack = Application.getInstance().getGlobalContainerStack()
if not global_stack: 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)} 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 = [] machine_extruders = []
for position in sorted(global_stack.extruders): for position in sorted(global_stack.extruders):
@ -223,7 +229,7 @@ class CuraContainerRegistry(ContainerRegistry):
# Make sure we have a profile_definition in the file: # Make sure we have a profile_definition in the file:
if profile_definition is None: if profile_definition is None:
break break
machine_definitions = self.findDefinitionContainers(id = profile_definition) machine_definitions = self.findContainers(id = profile_definition)
if not machine_definitions: if not machine_definitions:
Logger.log("e", "Incorrect profile [%s]. Unknown machine type [%s]", file_name, profile_definition) Logger.log("e", "Incorrect profile [%s]. Unknown machine type [%s]", file_name, profile_definition)
return {"status": "error", return {"status": "error",
@ -233,17 +239,17 @@ class CuraContainerRegistry(ContainerRegistry):
# Get the expected machine definition. # Get the expected machine definition.
# i.e.: We expect gcode for a UM2 Extended to be defined as normal UM2 gcode... # i.e.: We expect gcode for a UM2 Extended to be defined as normal UM2 gcode...
profile_definition = getMachineDefinitionIDForQualitySearch(machine_definition) has_machine_quality = parseBool(machine_definition.getMetaDataEntry("has_machine_quality", "false"))
expected_machine_definition = getMachineDefinitionIDForQualitySearch(global_stack.definition) 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): # And check if the profile_definition matches either one (showing error if not):
if profile_definition != expected_machine_definition: 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) 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))
return { "status": "error", global_profile.setMetaDataEntry("definition", expected_machine_definition)
"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)} for extruder_profile in extruder_profiles:
extruder_profile.setMetaDataEntry("definition", expected_machine_definition)
# Fix the global quality profile's definition field in case it's not correct
global_profile.setMetaDataEntry("definition", expected_machine_definition)
quality_name = global_profile.getName() quality_name = global_profile.getName()
quality_type = global_profile.getMetaDataEntry("quality_type") quality_type = global_profile.getMetaDataEntry("quality_type")
@ -266,7 +272,6 @@ class CuraContainerRegistry(ContainerRegistry):
profile.setMetaDataEntry("type", "quality_changes") profile.setMetaDataEntry("type", "quality_changes")
profile.setMetaDataEntry("definition", expected_machine_definition) profile.setMetaDataEntry("definition", expected_machine_definition)
profile.setMetaDataEntry("quality_type", quality_type) profile.setMetaDataEntry("quality_type", quality_type)
profile.setMetaDataEntry("position", "0")
profile.setDirty(True) profile.setDirty(True)
if idx == 0: if idx == 0:
# Move all per-extruder settings to the first extruder's quality_changes # Move all per-extruder settings to the first extruder's quality_changes
@ -283,13 +288,14 @@ class CuraContainerRegistry(ContainerRegistry):
profile.addInstance(new_instance) profile.addInstance(new_instance)
profile.setDirty(True) profile.setDirty(True)
global_profile.removeInstance(qc_setting_key, postpone_emit=True) global_profile.removeInstance(qc_setting_key, postpone_emit = True)
extruder_profiles.append(profile) extruder_profiles.append(profile)
for profile in extruder_profiles: for profile in extruder_profiles:
profile_or_list.append(profile) profile_or_list.append(profile)
# Import all profiles # Import all profiles
profile_ids_added = [] # type: List[str]
for profile_index, profile in enumerate(profile_or_list): for profile_index, profile in enumerate(profile_or_list):
if profile_index == 0: if profile_index == 0:
# This is assumed to be the global profile # This is assumed to be the global profile
@ -310,11 +316,15 @@ class CuraContainerRegistry(ContainerRegistry):
result = self._configureProfile(profile, profile_id, new_name, expected_machine_definition) result = self._configureProfile(profile, profile_id, new_name, expected_machine_definition)
if result is not None: if result is not None:
return {"status": "error", "message": catalog.i18nc( # Remove any profiles that did got added.
"@info:status Don't translate the XML tags <filename> or <message>!", for profile_id in profile_ids_added:
"Failed to import profile from <filename>{0}</filename>:", self.removeContainer(profile_id)
file_name) + " <message>" + result + "</message>"}
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())} 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 # This message is throw when the profile reader doesn't find any profile in the file
@ -324,7 +334,7 @@ class CuraContainerRegistry(ContainerRegistry):
return {"status": "error", "message": catalog.i18nc("@info:status", "Profile {0} has an unknown file type or is corrupted.", file_name)} return {"status": "error", "message": catalog.i18nc("@info:status", "Profile {0} has an unknown file type or is corrupted.", file_name)}
@override(ContainerRegistry) @override(ContainerRegistry)
def load(self): def load(self) -> None:
super().load() super().load()
self._registerSingleExtrusionMachinesExtruderStacks() self._registerSingleExtrusionMachinesExtruderStacks()
self._connectUpgradedExtruderStacksToMachines() self._connectUpgradedExtruderStacksToMachines()
@ -377,21 +387,40 @@ class CuraContainerRegistry(ContainerRegistry):
global_stack = Application.getInstance().getGlobalContainerStack() global_stack = Application.getInstance().getGlobalContainerStack()
if global_stack is None: if global_stack is None:
return None return None
definition_id = getMachineDefinitionIDForQualitySearch(global_stack.definition) definition_id = ContainerTree.getInstance().machines[global_stack.definition.getId()].quality_definition
profile.setDefinition(definition_id) profile.setDefinition(definition_id)
# Check to make sure the imported profile actually makes sense in context of the current configuration. # 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 # 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. # successfully imported but then fail to show up.
quality_manager = cura.CuraApplication.CuraApplication.getInstance()._quality_manager quality_group_dict = ContainerTree.getInstance().getCurrentQualityGroups()
quality_group_dict = quality_manager.getQualityGroupsForMachineDefinition(global_stack) # "not_supported" profiles can be imported.
if quality_type not in quality_group_dict: 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) return catalog.i18nc("@info:status", "Could not find a quality type {0} for the current configuration.", quality_type)
ContainerRegistry.getInstance().addContainer(profile) ContainerRegistry.getInstance().addContainer(profile)
return None 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 ## Gets a list of profile writer plugins
# \return List of tuples of (plugin_id, meta_data). # \return List of tuples of (plugin_id, meta_data).
def _getIOPlugins(self, io_type): def _getIOPlugins(self, io_type):
@ -406,7 +435,7 @@ class CuraContainerRegistry(ContainerRegistry):
return result return result
## Convert an "old-style" pure ContainerStack to either an Extruder or Global stack. ## Convert an "old-style" pure ContainerStack to either an Extruder or Global stack.
def _convertContainerStack(self, container): def _convertContainerStack(self, container: ContainerStack) -> Union[ExtruderStack.ExtruderStack, GlobalStack.GlobalStack]:
assert type(container) == ContainerStack assert type(container) == ContainerStack
container_type = container.getMetaDataEntry("type") container_type = container.getMetaDataEntry("type")
@ -430,14 +459,14 @@ class CuraContainerRegistry(ContainerRegistry):
return new_stack return new_stack
def _registerSingleExtrusionMachinesExtruderStacks(self): def _registerSingleExtrusionMachinesExtruderStacks(self) -> None:
machines = self.findContainerStacks(type = "machine", machine_extruder_trains = {"0": "fdmextruder"}) machines = self.findContainerStacks(type = "machine", machine_extruder_trains = {"0": "fdmextruder"})
for machine in machines: for machine in machines:
extruder_stacks = self.findContainerStacks(type = "extruder_train", machine = machine.getId()) extruder_stacks = self.findContainerStacks(type = "extruder_train", machine = machine.getId())
if not extruder_stacks: if not extruder_stacks:
self.addExtruderStackForSingleExtrusionMachine(machine, "fdmextruder") 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 # 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 # 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. # is added, we check to see if an extruder stack needs to be added.
@ -587,6 +616,7 @@ class CuraContainerRegistry(ContainerRegistry):
extruder_quality_changes_container.setMetaDataEntry("setting_version", application.SettingVersion) extruder_quality_changes_container.setMetaDataEntry("setting_version", application.SettingVersion)
extruder_quality_changes_container.setMetaDataEntry("position", extruder_definition.getMetaDataEntry("position")) 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("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()) extruder_quality_changes_container.setDefinition(machine_quality_changes.getDefinition().getId())
self.addContainer(extruder_quality_changes_container) self.addContainer(extruder_quality_changes_container)
@ -671,7 +701,7 @@ class CuraContainerRegistry(ContainerRegistry):
return extruder_stack return extruder_stack
def _findQualityChangesContainerInCuraFolder(self, name): def _findQualityChangesContainerInCuraFolder(self, name: str) -> Optional[InstanceContainer]:
quality_changes_dir = Resources.getPath(cura.CuraApplication.CuraApplication.ResourceTypes.QualityChangesInstanceContainer) quality_changes_dir = Resources.getPath(cura.CuraApplication.CuraApplication.ResourceTypes.QualityChangesInstanceContainer)
instance_container = None instance_container = None
@ -684,7 +714,7 @@ class CuraContainerRegistry(ContainerRegistry):
parser = configparser.ConfigParser(interpolation = None) parser = configparser.ConfigParser(interpolation = None)
try: try:
parser.read([file_path]) parser.read([file_path])
except: except Exception:
# Skip, it is not a valid stack file # Skip, it is not a valid stack file
continue continue
@ -716,7 +746,7 @@ class CuraContainerRegistry(ContainerRegistry):
# due to problems with loading order, some stacks may not have the proper next stack # 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 # set after upgrading, because the proper global stack was not yet loaded. This method
# makes sure those extruders also get the right stack set. # 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) extruder_stacks = self.findContainers(container_type = ExtruderStack.ExtruderStack)
for extruder_stack in extruder_stacks: for extruder_stack in extruder_stacks:
if extruder_stack.getNextStack(): if extruder_stack.getNextStack():

View File

@ -87,6 +87,19 @@ class CuraContainerStack(ContainerStack):
def qualityChanges(self) -> InstanceContainer: def qualityChanges(self) -> InstanceContainer:
return cast(InstanceContainer, self._containers[_ContainerIndexes.QualityChanges]) 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. ## Set the quality container.
# #
# \param new_quality The new quality container. It is expected to have a "type" metadata entry with the value "quality". # \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: class _ContainerIndexes:
UserChanges = 0 UserChanges = 0
QualityChanges = 1 QualityChanges = 1
Quality = 2 Intent = 2
Material = 3 Quality = 3
Variant = 4 Material = 4
DefinitionChanges = 5 Variant = 5
Definition = 6 DefinitionChanges = 6
Definition = 7
# Simple hash map to map from index to "type" metadata entry # Simple hash map to map from index to "type" metadata entry
IndexTypeMap = { IndexTypeMap = {
UserChanges: "user", UserChanges: "user",
QualityChanges: "quality_changes", QualityChanges: "quality_changes",
Intent: "intent",
Quality: "quality", Quality: "quality",
Material: "material", Material: "material",
Variant: "variant", Variant: "variant",

View File

@ -40,8 +40,8 @@ class CuraFormulaFunctions:
global_stack = machine_manager.activeMachine global_stack = machine_manager.activeMachine
try: try:
extruder_stack = global_stack.extruders[str(extruder_position)] extruder_stack = global_stack.extruderList[int(extruder_position)]
except KeyError: except IndexError:
if extruder_position != 0: if extruder_position != 0:
Logger.log("w", "Value for %s of extruder %s was requested, but that extruder is not available. Returning the result form extruder 0 instead" % (property_key, extruder_position)) Logger.log("w", "Value for %s of extruder %s was requested, but that extruder is not available. Returning the result form extruder 0 instead" % (property_key, extruder_position))
# This fixes a very specific fringe case; If a profile was created for a custom printer and one of the # This fixes a very specific fringe case; If a profile was created for a custom printer and one of the
@ -104,11 +104,14 @@ class CuraFormulaFunctions:
machine_manager = self._application.getMachineManager() machine_manager = self._application.getMachineManager()
global_stack = machine_manager.activeMachine global_stack = machine_manager.activeMachine
extruder_stack = global_stack.extruders[str(extruder_position)] try:
extruder_stack = global_stack.extruderList[extruder_position]
except IndexError:
Logger.log("w", "Unable to find extruder on in index %s", extruder_position)
else:
context = self.createContextForDefaultValueEvaluation(extruder_stack)
context = self.createContextForDefaultValueEvaluation(extruder_stack) return self.getValueInExtruder(extruder_position, property_key, context = context)
return self.getValueInExtruder(extruder_position, property_key, context = context)
# Gets all default setting values as a list from all extruders of the currently active machine. # Gets all default setting values as a list from all extruders of the currently active machine.
# The default values are those excluding the values in the user_changes container. # The default values are those excluding the values in the user_changes container.

View File

@ -1,4 +1,4 @@
# Copyright (c) 2018 Ultimaker B.V. # Copyright (c) 2019 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
from typing import Optional from typing import Optional
@ -8,7 +8,8 @@ from UM.Logger import Logger
from UM.Settings.Interfaces import DefinitionContainerInterface from UM.Settings.Interfaces import DefinitionContainerInterface
from UM.Settings.InstanceContainer import InstanceContainer from UM.Settings.InstanceContainer import InstanceContainer
from cura.Machines.VariantType import VariantType from cura.Machines.ContainerTree import ContainerTree
from cura.Machines.MachineNode import MachineNode
from .GlobalStack import GlobalStack from .GlobalStack import GlobalStack
from .ExtruderStack import ExtruderStack from .ExtruderStack import ExtruderStack
@ -26,9 +27,8 @@ class CuraStackBuilder:
def createMachine(cls, name: str, definition_id: str) -> Optional[GlobalStack]: def createMachine(cls, name: str, definition_id: str) -> Optional[GlobalStack]:
from cura.CuraApplication import CuraApplication from cura.CuraApplication import CuraApplication
application = CuraApplication.getInstance() application = CuraApplication.getInstance()
variant_manager = application.getVariantManager()
quality_manager = application.getQualityManager()
registry = application.getContainerRegistry() registry = application.getContainerRegistry()
container_tree = ContainerTree.getInstance()
definitions = registry.findDefinitionContainers(id = definition_id) definitions = registry.findDefinitionContainers(id = definition_id)
if not definitions: if not definitions:
@ -37,14 +37,7 @@ class CuraStackBuilder:
return None return None
machine_definition = definitions[0] machine_definition = definitions[0]
machine_node = container_tree.machines[machine_definition.getId()]
# get variant container for the global stack
global_variant_container = application.empty_variant_container
global_variant_node = variant_manager.getDefaultVariantNode(machine_definition, VariantType.BUILD_PLATE)
if global_variant_node:
global_variant_container = global_variant_node.getContainer()
if not global_variant_container:
global_variant_container = application.empty_variant_container
generated_name = registry.createUniqueName("machine", "", name, machine_definition.getName()) generated_name = registry.createUniqueName("machine", "", name, machine_definition.getName())
# Make sure the new name does not collide with any definition or (quality) profile # Make sure the new name does not collide with any definition or (quality) profile
@ -56,9 +49,9 @@ class CuraStackBuilder:
new_global_stack = cls.createGlobalStack( new_global_stack = cls.createGlobalStack(
new_stack_id = generated_name, new_stack_id = generated_name,
definition = machine_definition, definition = machine_definition,
variant_container = global_variant_container, variant_container = application.empty_variant_container,
material_container = application.empty_material_container, material_container = application.empty_material_container,
quality_container = application.empty_quality_container, quality_container = machine_node.preferredGlobalQuality().container,
) )
new_global_stack.setName(generated_name) new_global_stack.setName(generated_name)
@ -67,33 +60,9 @@ class CuraStackBuilder:
for position in extruder_dict: for position in extruder_dict:
cls.createExtruderStackWithDefaultSetup(new_global_stack, position) cls.createExtruderStackWithDefaultSetup(new_global_stack, position)
for new_extruder in new_global_stack.extruders.values(): #Only register the extruders if we're sure that all of them are correct. for new_extruder in new_global_stack.extruders.values(): # Only register the extruders if we're sure that all of them are correct.
registry.addContainer(new_extruder) registry.addContainer(new_extruder)
preferred_quality_type = machine_definition.getMetaDataEntry("preferred_quality_type")
quality_group_dict = quality_manager.getQualityGroups(new_global_stack)
if not quality_group_dict:
# There is no available quality group, set all quality containers to empty.
new_global_stack.quality = application.empty_quality_container
for extruder_stack in new_global_stack.extruders.values():
extruder_stack.quality = application.empty_quality_container
else:
# Set the quality containers to the preferred quality type if available, otherwise use the first quality
# type that's available.
if preferred_quality_type not in quality_group_dict:
Logger.log("w", "The preferred quality {quality_type} doesn't exist for this set-up. Choosing a random one.".format(quality_type = preferred_quality_type))
preferred_quality_type = next(iter(quality_group_dict))
quality_group = quality_group_dict.get(preferred_quality_type)
new_global_stack.quality = quality_group.node_for_global.getContainer()
if not new_global_stack.quality:
new_global_stack.quality = application.empty_quality_container
for position, extruder_stack in new_global_stack.extruders.items():
if position in quality_group.nodes_for_extruders and quality_group.nodes_for_extruders[position].getContainer():
extruder_stack.quality = quality_group.nodes_for_extruders[position].getContainer()
else:
extruder_stack.quality = application.empty_quality_container
# Register the global stack after the extruder stacks are created. This prevents the registry from adding another # Register the global stack after the extruder stacks are created. This prevents the registry from adding another
# extruder stack because the global stack didn't have one yet (which is enforced since Cura 3.1). # extruder stack because the global stack didn't have one yet (which is enforced since Cura 3.1).
registry.addContainer(new_global_stack) registry.addContainer(new_global_stack)
@ -108,36 +77,32 @@ class CuraStackBuilder:
def createExtruderStackWithDefaultSetup(cls, global_stack: "GlobalStack", extruder_position: int) -> None: def createExtruderStackWithDefaultSetup(cls, global_stack: "GlobalStack", extruder_position: int) -> None:
from cura.CuraApplication import CuraApplication from cura.CuraApplication import CuraApplication
application = CuraApplication.getInstance() application = CuraApplication.getInstance()
variant_manager = application.getVariantManager()
material_manager = application.getMaterialManager()
registry = application.getContainerRegistry() registry = application.getContainerRegistry()
# get variant container for extruders # Get the extruder definition.
extruder_variant_container = application.empty_variant_container
extruder_variant_node = variant_manager.getDefaultVariantNode(global_stack.definition, VariantType.NOZZLE,
global_stack = global_stack)
extruder_variant_name = None
if extruder_variant_node:
extruder_variant_container = extruder_variant_node.getContainer()
if not extruder_variant_container:
extruder_variant_container = application.empty_variant_container
extruder_variant_name = extruder_variant_container.getName()
extruder_definition_dict = global_stack.getMetaDataEntry("machine_extruder_trains") extruder_definition_dict = global_stack.getMetaDataEntry("machine_extruder_trains")
extruder_definition_id = extruder_definition_dict[str(extruder_position)] extruder_definition_id = extruder_definition_dict[str(extruder_position)]
try: try:
extruder_definition = registry.findDefinitionContainers(id = extruder_definition_id)[0] extruder_definition = registry.findDefinitionContainers(id = extruder_definition_id)[0]
except IndexError as e: except IndexError:
# It still needs to break, but we want to know what extruder ID made it break. # It still needs to break, but we want to know what extruder ID made it break.
Logger.log("e", "Unable to find extruder with the id %s", extruder_definition_id) msg = "Unable to find extruder definition with the id [%s]" % extruder_definition_id
raise e Logger.logException("e", msg)
raise IndexError(msg)
# get material container for extruders # Find out what filament diameter we need.
material_container = application.empty_material_container approximate_diameter = round(extruder_definition.getProperty("material_diameter", "value")) # Can't be modified by definition changes since we are just initialising the stack here.
material_node = material_manager.getDefaultMaterial(global_stack, str(extruder_position), extruder_variant_name,
extruder_definition = extruder_definition) # Find the preferred containers.
if material_node and material_node.getContainer(): machine_node = ContainerTree.getInstance().machines[global_stack.definition.getId()]
material_container = material_node.getContainer() extruder_variant_node = machine_node.variants.get(machine_node.preferred_variant_name)
if not extruder_variant_node:
Logger.log("w", "Could not find preferred nozzle {nozzle_name}. Falling back to {fallback}.".format(nozzle_name = machine_node.preferred_variant_name, fallback = next(iter(machine_node.variants))))
extruder_variant_node = next(iter(machine_node.variants.values()))
extruder_variant_container = extruder_variant_node.container
material_node = extruder_variant_node.preferredMaterial(approximate_diameter)
material_container = material_node.container
quality_node = material_node.preferredQuality()
new_extruder_id = registry.uniqueName(extruder_definition_id) new_extruder_id = registry.uniqueName(extruder_definition_id)
new_extruder = cls.createExtruderStack( new_extruder = cls.createExtruderStack(
@ -147,7 +112,7 @@ class CuraStackBuilder:
position = extruder_position, position = extruder_position,
variant_container = extruder_variant_container, variant_container = extruder_variant_container,
material_container = material_container, material_container = material_container,
quality_container = application.empty_quality_container quality_container = quality_node.container
) )
new_extruder.setNextStack(global_stack) new_extruder.setNextStack(global_stack)
@ -189,6 +154,7 @@ class CuraStackBuilder:
stack.variant = variant_container stack.variant = variant_container
stack.material = material_container stack.material = material_container
stack.quality = quality_container stack.quality = quality_container
stack.intent = application.empty_intent_container
stack.qualityChanges = application.empty_quality_changes_container stack.qualityChanges = application.empty_quality_changes_container
stack.userChanges = user_container stack.userChanges = user_container
@ -237,6 +203,7 @@ class CuraStackBuilder:
stack.variant = variant_container stack.variant = variant_container
stack.material = material_container stack.material = material_container
stack.quality = quality_container stack.quality = quality_container
stack.intent = application.empty_intent_container
stack.qualityChanges = application.empty_quality_changes_container stack.qualityChanges = application.empty_quality_changes_container
stack.userChanges = user_container stack.userChanges = user_container

View File

@ -1,4 +1,4 @@
# Copyright (c) 2018 Ultimaker B.V. # Copyright (c) 2019 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
from PyQt5.QtCore import pyqtSignal, pyqtProperty, QObject, QVariant # For communicating data and events to Qt. from PyQt5.QtCore import pyqtSignal, pyqtProperty, QObject, QVariant # For communicating data and events to Qt.
@ -12,7 +12,6 @@ from UM.Scene.SceneNode import SceneNode
from UM.Scene.Selection import Selection from UM.Scene.Selection import Selection
from UM.Scene.Iterator.BreadthFirstIterator import BreadthFirstIterator from UM.Scene.Iterator.BreadthFirstIterator import BreadthFirstIterator
from UM.Settings.ContainerRegistry import ContainerRegistry # Finding containers by ID. from UM.Settings.ContainerRegistry import ContainerRegistry # Finding containers by ID.
from UM.Settings.ContainerStack import ContainerStack
from typing import Any, cast, Dict, List, Optional, TYPE_CHECKING, Union from typing import Any, cast, Dict, List, Optional, TYPE_CHECKING, Union
@ -42,8 +41,6 @@ class ExtruderManager(QObject):
# TODO; I have no idea why this is a union of ID's and extruder stacks. This needs to be fixed at some point. # TODO; I have no idea why this is a union of ID's and extruder stacks. This needs to be fixed at some point.
self._selected_object_extruders = [] # type: List[Union[str, "ExtruderStack"]] self._selected_object_extruders = [] # type: List[Union[str, "ExtruderStack"]]
self._addCurrentMachineExtruders()
Selection.selectionChanged.connect(self.resetSelectedObjectExtruders) Selection.selectionChanged.connect(self.resetSelectedObjectExtruders)
## Signal to notify other components when the list of extruders for a machine definition changes. ## Signal to notify other components when the list of extruders for a machine definition changes.
@ -74,7 +71,7 @@ class ExtruderManager(QObject):
global_container_stack = self._application.getGlobalContainerStack() global_container_stack = self._application.getGlobalContainerStack()
if global_container_stack: if global_container_stack:
extruder_stack_ids = {position: extruder.id for position, extruder in global_container_stack.extruders.items()} extruder_stack_ids = {extruder.getMetaDataEntry("position", ""): extruder.id for extruder in global_container_stack.extruderList}
return extruder_stack_ids return extruder_stack_ids
@ -91,16 +88,6 @@ class ExtruderManager(QObject):
def activeExtruderIndex(self) -> int: def activeExtruderIndex(self) -> int:
return self._active_extruder_index return self._active_extruder_index
## Gets the extruder name of an extruder of the currently active machine.
#
# \param index The index of the extruder whose name to get.
@pyqtSlot(int, result = str)
def getExtruderName(self, index: int) -> str:
try:
return self.getActiveExtruderStacks()[index].getName()
except IndexError:
return ""
## Emitted whenever the selectedObjectExtruders property changes. ## Emitted whenever the selectedObjectExtruders property changes.
selectedObjectExtrudersChanged = pyqtSignal() selectedObjectExtrudersChanged = pyqtSignal()
@ -114,7 +101,7 @@ class ExtruderManager(QObject):
selected_nodes = [] # type: List["SceneNode"] selected_nodes = [] # type: List["SceneNode"]
for node in Selection.getAllSelectedObjects(): for node in Selection.getAllSelectedObjects():
if node.callDecoration("isGroup"): if node.callDecoration("isGroup"):
for grouped_node in BreadthFirstIterator(node): #type: ignore #Ignore type error because iter() should get called automatically by Python syntax. for grouped_node in BreadthFirstIterator(node):
if grouped_node.callDecoration("isGroup"): if grouped_node.callDecoration("isGroup"):
continue continue
@ -131,7 +118,7 @@ class ExtruderManager(QObject):
elif current_extruder_trains: elif current_extruder_trains:
object_extruders.add(current_extruder_trains[0].getId()) object_extruders.add(current_extruder_trains[0].getId())
self._selected_object_extruders = list(object_extruders) # type: List[Union[str, "ExtruderStack"]] self._selected_object_extruders = list(object_extruders)
return self._selected_object_extruders return self._selected_object_extruders
@ -140,7 +127,7 @@ class ExtruderManager(QObject):
# This will trigger a recalculation of the extruders used for the # This will trigger a recalculation of the extruders used for the
# selection. # selection.
def resetSelectedObjectExtruders(self) -> None: def resetSelectedObjectExtruders(self) -> None:
self._selected_object_extruders = [] # type: List[Union[str, "ExtruderStack"]] self._selected_object_extruders = []
self.selectedObjectExtrudersChanged.emit() self.selectedObjectExtrudersChanged.emit()
@pyqtSlot(result = QObject) @pyqtSlot(result = QObject)
@ -180,7 +167,7 @@ class ExtruderManager(QObject):
# \param setting_key \type{str} The setting to get the property of. # \param setting_key \type{str} The setting to get the property of.
# \param property \type{str} The property to get. # \param property \type{str} The property to get.
# \return \type{List} the list of results # \return \type{List} the list of results
def getAllExtruderSettings(self, setting_key: str, prop: str) -> List: def getAllExtruderSettings(self, setting_key: str, prop: str) -> List[Any]:
result = [] result = []
for extruder_stack in self.getActiveExtruderStacks(): for extruder_stack in self.getActiveExtruderStacks():
@ -205,7 +192,7 @@ class ExtruderManager(QObject):
# list. # list.
# #
# \return A list of extruder stacks. # \return A list of extruder stacks.
def getUsedExtruderStacks(self) -> List["ContainerStack"]: def getUsedExtruderStacks(self) -> List["ExtruderStack"]:
global_stack = self._application.getGlobalContainerStack() global_stack = self._application.getGlobalContainerStack()
container_registry = ContainerRegistry.getInstance() container_registry = ContainerRegistry.getInstance()
@ -321,48 +308,47 @@ class ExtruderManager(QObject):
self.resetSelectedObjectExtruders() self.resetSelectedObjectExtruders()
## Adds the extruders of the currently active machine. ## Adds the extruders to the selected machine.
def _addCurrentMachineExtruders(self) -> None: def addMachineExtruders(self, global_stack: GlobalStack) -> None:
global_stack = self._application.getGlobalContainerStack()
extruders_changed = False extruders_changed = False
container_registry = ContainerRegistry.getInstance()
global_stack_id = global_stack.getId()
if global_stack: # Gets the extruder trains that we just created as well as any that still existed.
container_registry = ContainerRegistry.getInstance() extruder_trains = container_registry.findContainerStacks(type = "extruder_train", machine = global_stack_id)
global_stack_id = global_stack.getId()
# Gets the extruder trains that we just created as well as any that still existed. # Make sure the extruder trains for the new machine can be placed in the set of sets
extruder_trains = container_registry.findContainerStacks(type = "extruder_train", machine = global_stack_id) if global_stack_id not in self._extruder_trains:
self._extruder_trains[global_stack_id] = {}
extruders_changed = True
# Make sure the extruder trains for the new machine can be placed in the set of sets # Register the extruder trains by position
if global_stack_id not in self._extruder_trains: for extruder_train in extruder_trains:
self._extruder_trains[global_stack_id] = {} extruder_position = extruder_train.getMetaDataEntry("position")
extruders_changed = True self._extruder_trains[global_stack_id][extruder_position] = extruder_train
# Register the extruder trains by position # regardless of what the next stack is, we have to set it again, because of signal routing. ???
for extruder_train in extruder_trains: extruder_train.setParent(global_stack)
extruder_position = extruder_train.getMetaDataEntry("position") extruder_train.setNextStack(global_stack)
self._extruder_trains[global_stack_id][extruder_position] = extruder_train extruders_changed = True
# regardless of what the next stack is, we have to set it again, because of signal routing. ??? self.fixSingleExtrusionMachineExtruderDefinition(global_stack)
extruder_train.setParent(global_stack) if extruders_changed:
extruder_train.setNextStack(global_stack) self.extrudersChanged.emit(global_stack_id)
extruders_changed = True
self.fixSingleExtrusionMachineExtruderDefinition(global_stack)
if extruders_changed:
self.extrudersChanged.emit(global_stack_id)
self.setActiveExtruderIndex(0)
self.activeExtruderChanged.emit()
# After 3.4, all single-extrusion machines have their own extruder definition files instead of reusing # After 3.4, all single-extrusion machines have their own extruder definition files instead of reusing
# "fdmextruder". We need to check a machine here so its extruder definition is correct according to this. # "fdmextruder". We need to check a machine here so its extruder definition is correct according to this.
def fixSingleExtrusionMachineExtruderDefinition(self, global_stack: "GlobalStack") -> None: def fixSingleExtrusionMachineExtruderDefinition(self, global_stack: "GlobalStack") -> None:
container_registry = ContainerRegistry.getInstance() container_registry = ContainerRegistry.getInstance()
expected_extruder_definition_0_id = global_stack.getMetaDataEntry("machine_extruder_trains")["0"] expected_extruder_definition_0_id = global_stack.getMetaDataEntry("machine_extruder_trains")["0"]
extruder_stack_0 = global_stack.extruders.get("0") try:
extruder_stack_0 = global_stack.extruderList[0]
except IndexError:
extruder_stack_0 = None
# At this point, extruder stacks for this machine may not have been loaded yet. In this case, need to look in # At this point, extruder stacks for this machine may not have been loaded yet. In this case, need to look in
# the container registry as well. # the container registry as well.
if not global_stack.extruders: if not global_stack.extruderList:
extruder_trains = container_registry.findContainerStacks(type = "extruder_train", extruder_trains = container_registry.findContainerStacks(type = "extruder_train",
machine = global_stack.getId()) machine = global_stack.getId())
if extruder_trains: if extruder_trains:
@ -380,7 +366,13 @@ class ExtruderManager(QObject):
elif extruder_stack_0.definition.getId() != expected_extruder_definition_0_id: elif extruder_stack_0.definition.getId() != expected_extruder_definition_0_id:
Logger.log("e", "Single extruder printer [{printer}] expected extruder [{expected}], but got [{got}]. I'm making it [{expected}].".format( Logger.log("e", "Single extruder printer [{printer}] expected extruder [{expected}], but got [{got}]. I'm making it [{expected}].".format(
printer = global_stack.getId(), expected = expected_extruder_definition_0_id, got = extruder_stack_0.definition.getId())) printer = global_stack.getId(), expected = expected_extruder_definition_0_id, got = extruder_stack_0.definition.getId()))
extruder_definition = container_registry.findDefinitionContainers(id = expected_extruder_definition_0_id)[0] try:
extruder_definition = container_registry.findDefinitionContainers(id = expected_extruder_definition_0_id)[0]
except IndexError:
# It still needs to break, but we want to know what extruder ID made it break.
msg = "Unable to find extruder definition with the id [%s]" % expected_extruder_definition_0_id
Logger.logException("e", msg)
raise IndexError(msg)
extruder_stack_0.definition = extruder_definition extruder_stack_0.definition = extruder_definition
## Get all extruder values for a certain setting. ## Get all extruder values for a certain setting.

View File

@ -51,6 +51,10 @@ class ExtruderStack(CuraContainerStack):
def getNextStack(self) -> Optional["GlobalStack"]: def getNextStack(self) -> Optional["GlobalStack"]:
return super().getNextStack() return super().getNextStack()
@pyqtProperty(int, constant = True)
def position(self) -> int:
return int(self.getMetaDataEntry("position"))
def setEnabled(self, enabled: bool) -> None: def setEnabled(self, enabled: bool) -> None:
if self.getMetaDataEntry("enabled", True) == enabled: # No change. if self.getMetaDataEntry("enabled", True) == enabled: # No change.
return # Don't emit a signal then. return # Don't emit a signal then.
@ -135,12 +139,15 @@ class ExtruderStack(CuraContainerStack):
if limit_to_extruder == -1: if limit_to_extruder == -1:
limit_to_extruder = int(cura.CuraApplication.CuraApplication.getInstance().getMachineManager().defaultExtruderPosition) limit_to_extruder = int(cura.CuraApplication.CuraApplication.getInstance().getMachineManager().defaultExtruderPosition)
limit_to_extruder = str(limit_to_extruder) limit_to_extruder = str(limit_to_extruder)
if (limit_to_extruder is not None and limit_to_extruder != "-1") and self.getMetaDataEntry("position") != str(limit_to_extruder): if (limit_to_extruder is not None and limit_to_extruder != "-1") and self.getMetaDataEntry("position") != str(limit_to_extruder):
if str(limit_to_extruder) in self.getNextStack().extruders: try:
result = self.getNextStack().extruders[str(limit_to_extruder)].getProperty(key, property_name, context) result = self.getNextStack().extruderList[int(limit_to_extruder)].getProperty(key, property_name, context)
if result is not None: if result is not None:
context.popContainer() context.popContainer()
return result return result
except IndexError:
pass
result = super().getProperty(key, property_name, context) result = super().getProperty(key, property_name, context)
context.popContainer() context.popContainer()

View File

@ -1,4 +1,4 @@
# Copyright (c) 2018 Ultimaker B.V. # Copyright (c) 2019 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
from collections import defaultdict from collections import defaultdict
@ -8,7 +8,7 @@ import uuid
from PyQt5.QtCore import pyqtProperty, pyqtSlot, pyqtSignal from PyQt5.QtCore import pyqtProperty, pyqtSlot, pyqtSignal
from UM.Decorators import override from UM.Decorators import deprecated, override
from UM.MimeTypeDatabase import MimeType, MimeTypeDatabase from UM.MimeTypeDatabase import MimeType, MimeTypeDatabase
from UM.Settings.ContainerStack import ContainerStack from UM.Settings.ContainerStack import ContainerStack
from UM.Settings.SettingInstance import InstanceState from UM.Settings.SettingInstance import InstanceState
@ -20,6 +20,7 @@ from UM.Platform import Platform
from UM.Util import parseBool from UM.Util import parseBool
import cura.CuraApplication import cura.CuraApplication
from cura.PrinterOutput.PrinterOutputDevice import ConnectionType
from . import Exceptions from . import Exceptions
from .CuraContainerStack import CuraContainerStack from .CuraContainerStack import CuraContainerStack
@ -61,12 +62,13 @@ class GlobalStack(CuraContainerStack):
# #
# \return The extruders registered with this stack. # \return The extruders registered with this stack.
@pyqtProperty("QVariantMap", notify = extrudersChanged) @pyqtProperty("QVariantMap", notify = extrudersChanged)
@deprecated("Please use extruderList instead.", "4.4")
def extruders(self) -> Dict[str, "ExtruderStack"]: def extruders(self) -> Dict[str, "ExtruderStack"]:
return self._extruders return self._extruders
@pyqtProperty("QVariantList", notify = extrudersChanged) @pyqtProperty("QVariantList", notify = extrudersChanged)
def extruderList(self) -> List["ExtruderStack"]: def extruderList(self) -> List["ExtruderStack"]:
result_tuple_list = sorted(list(self.extruders.items()), key=lambda x: int(x[0])) result_tuple_list = sorted(list(self._extruders.items()), key=lambda x: int(x[0]))
result_list = [item[1] for item in result_tuple_list] result_list = [item[1] for item in result_tuple_list]
machine_extruder_count = self.getProperty("machine_extruder_count", "value") machine_extruder_count = self.getProperty("machine_extruder_count", "value")
@ -107,6 +109,19 @@ class GlobalStack(CuraContainerStack):
pass pass
return result return result
# Returns a boolean indicating if this machine has a remote connection. A machine is considered as remotely
# connected if its connection types contain one of the following values:
# - ConnectionType.NetworkConnection
# - ConnectionType.CloudConnection
@pyqtProperty(bool, notify = configuredConnectionTypesChanged)
def hasRemoteConnection(self) -> bool:
has_remote_connection = False
for connection_type in self.configuredConnectionTypes:
has_remote_connection |= connection_type in [ConnectionType.NetworkConnection.value,
ConnectionType.CloudConnection.value]
return has_remote_connection
## \sa configuredConnectionTypes ## \sa configuredConnectionTypes
def addConfiguredConnectionType(self, connection_type: int) -> None: def addConfiguredConnectionType(self, connection_type: int) -> None:
configured_connection_types = self.configuredConnectionTypes configured_connection_types = self.configuredConnectionTypes
@ -118,7 +133,7 @@ class GlobalStack(CuraContainerStack):
## \sa configuredConnectionTypes ## \sa configuredConnectionTypes
def removeConfiguredConnectionType(self, connection_type: int) -> None: def removeConfiguredConnectionType(self, connection_type: int) -> None:
configured_connection_types = self.configuredConnectionTypes configured_connection_types = self.configuredConnectionTypes
if connection_type in self.configured_connection_types: if connection_type in configured_connection_types:
# Store the values as a string. # Store the values as a string.
configured_connection_types.remove(connection_type) configured_connection_types.remove(connection_type)
self.setMetaDataEntry("connection_type", ",".join([str(c_type) for c_type in configured_connection_types])) self.setMetaDataEntry("connection_type", ",".join([str(c_type) for c_type in configured_connection_types]))
@ -130,6 +145,14 @@ class GlobalStack(CuraContainerStack):
return "machine_stack" return "machine_stack"
return configuration_type return configuration_type
def getIntentCategory(self) -> str:
intent_category = "default"
for extruder in self.extruderList:
category = extruder.intent.getMetaDataEntry("intent_category", "default")
if category != "default" and category != intent_category:
intent_category = category
return intent_category
def getBuildplateName(self) -> Optional[str]: def getBuildplateName(self) -> Optional[str]:
name = None name = None
if self.variant.getId() != "empty_variant": if self.variant.getId() != "empty_variant":
@ -264,18 +287,18 @@ class GlobalStack(CuraContainerStack):
def getHeadAndFansCoordinates(self): def getHeadAndFansCoordinates(self):
return self.getProperty("machine_head_with_fans_polygon", "value") return self.getProperty("machine_head_with_fans_polygon", "value")
def getHasMaterials(self) -> bool: @pyqtProperty(bool, constant = True)
def hasMaterials(self) -> bool:
return parseBool(self.getMetaDataEntry("has_materials", False)) return parseBool(self.getMetaDataEntry("has_materials", False))
def getHasVariants(self) -> bool: @pyqtProperty(bool, constant = True)
def hasVariants(self) -> bool:
return parseBool(self.getMetaDataEntry("has_variants", False)) return parseBool(self.getMetaDataEntry("has_variants", False))
def getHasVariantsBuildPlates(self) -> bool: @pyqtProperty(bool, constant = True)
def hasVariantBuildplates(self) -> bool:
return parseBool(self.getMetaDataEntry("has_variant_buildplates", False)) return parseBool(self.getMetaDataEntry("has_variant_buildplates", False))
def getHasMachineQuality(self) -> bool:
return parseBool(self.getMetaDataEntry("has_machine_quality", False))
## Get default firmware file name if one is specified in the firmware ## Get default firmware file name if one is specified in the firmware
@pyqtSlot(result = str) @pyqtSlot(result = str)
def getDefaultFirmwareName(self) -> str: def getDefaultFirmwareName(self) -> str:
@ -302,6 +325,17 @@ class GlobalStack(CuraContainerStack):
Logger.log("w", "Firmware file %s not found.", hex_file) Logger.log("w", "Firmware file %s not found.", hex_file)
return "" return ""
def getName(self) -> str:
return self._metadata.get("group_name", self._metadata.get("name", ""))
def setName(self, name: "str") -> None:
super().setName(name)
nameChanged = pyqtSignal()
name = pyqtProperty(str, fget=getName, fset=setName, notify=nameChanged)
## private: ## private:
global_stack_mime = MimeType( global_stack_mime = MimeType(
name = "application/x-cura-globalstack", name = "application/x-cura-globalstack",

View File

@ -0,0 +1,179 @@
# Copyright (c) 2019 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from PyQt5.QtCore import QObject, pyqtProperty, pyqtSignal, pyqtSlot
from typing import Any, Dict, List, Optional, Set, Tuple, TYPE_CHECKING
from UM.Logger import Logger
from UM.Settings.InstanceContainer import InstanceContainer
import cura.CuraApplication
from cura.Machines.ContainerTree import ContainerTree
from cura.Settings.cura_empty_instance_containers import empty_intent_container
if TYPE_CHECKING:
from UM.Settings.InstanceContainer import InstanceContainer
## Front-end for querying which intents are available for a certain
# configuration.
class IntentManager(QObject):
__instance = None
## This class is a singleton.
@classmethod
def getInstance(cls):
if not cls.__instance:
cls.__instance = IntentManager()
return cls.__instance
intentCategoryChanged = pyqtSignal() #Triggered when we switch categories.
## Gets the metadata dictionaries of all intent profiles for a given
# configuration.
#
# \param definition_id ID of the printer.
# \param nozzle_name Name of the nozzle.
# \param material_base_file The base_file of the material.
# \return A list of metadata dictionaries matching the search criteria, or
# an empty list if nothing was found.
def intentMetadatas(self, definition_id: str, nozzle_name: str, material_base_file: str) -> List[Dict[str, Any]]:
intent_metadatas = [] # type: List[Dict[str, Any]]
try:
materials = ContainerTree.getInstance().machines[definition_id].variants[nozzle_name].materials
except KeyError:
Logger.log("w", "Unable to find the machine %s or the variant %s", definition_id, nozzle_name)
materials = {}
if material_base_file not in materials:
return intent_metadatas
material_node = materials[material_base_file]
for quality_node in material_node.qualities.values():
for intent_node in quality_node.intents.values():
intent_metadatas.append(intent_node.getMetadata())
return intent_metadatas
## Collects and returns all intent categories available for the given
# parameters. Note that the 'default' category is always available.
#
# \param definition_id ID of the printer.
# \param nozzle_name Name of the nozzle.
# \param material_id ID of the material.
# \return A set of intent category names.
def intentCategories(self, definition_id: str, nozzle_id: str, material_id: str) -> List[str]:
categories = set()
for intent in self.intentMetadatas(definition_id, nozzle_id, material_id):
categories.add(intent["intent_category"])
categories.add("default") #The "empty" intent is not an actual profile specific to the configuration but we do want it to appear in the categories list.
return list(categories)
## List of intents to be displayed in the interface.
#
# For the interface this will have to be broken up into the different
# intent categories. That is up to the model there.
#
# \return A list of tuples of intent_category and quality_type. The actual
# instance may vary per extruder.
def getCurrentAvailableIntents(self) -> List[Tuple[str, str]]:
application = cura.CuraApplication.CuraApplication.getInstance()
global_stack = application.getGlobalContainerStack()
if global_stack is None:
return [("default", "normal")]
# TODO: We now do this (return a default) if the global stack is missing, but not in the code below,
# even though there should always be defaults. The problem then is what to do with the quality_types.
# Currently _also_ inconsistent with 'currentAvailableIntentCategories', which _does_ return default.
quality_groups = ContainerTree.getInstance().getCurrentQualityGroups()
available_quality_types = {quality_group.quality_type for quality_group in quality_groups.values() if quality_group.node_for_global is not None}
final_intent_ids = set() # type: Set[str]
current_definition_id = global_stack.definition.getId()
for extruder_stack in global_stack.extruderList:
if not extruder_stack.isEnabled:
continue
nozzle_name = extruder_stack.variant.getMetaDataEntry("name")
material_id = extruder_stack.material.getMetaDataEntry("base_file")
final_intent_ids |= {metadata["id"] for metadata in self.intentMetadatas(current_definition_id, nozzle_name, material_id) if metadata.get("quality_type") in available_quality_types}
result = set() # type: Set[Tuple[str, str]]
for intent_id in final_intent_ids:
intent_metadata = application.getContainerRegistry().findContainersMetadata(id = intent_id)[0]
result.add((intent_metadata["intent_category"], intent_metadata["quality_type"]))
return list(result)
## List of intent categories available in either of the extruders.
#
# This is purposefully inconsistent with the way that the quality types
# are listed. The quality types will show all quality types available in
# the printer using any configuration. This will only list the intent
# categories that are available using the current configuration (but the
# union over the extruders).
# \return List of all categories in the current configurations of all
# extruders.
def currentAvailableIntentCategories(self) -> List[str]:
global_stack = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack()
if global_stack is None:
return ["default"]
current_definition_id = global_stack.definition.getId()
final_intent_categories = set() # type: Set[str]
for extruder_stack in global_stack.extruderList:
if not extruder_stack.isEnabled:
continue
nozzle_name = extruder_stack.variant.getMetaDataEntry("name")
material_id = extruder_stack.material.getMetaDataEntry("base_file")
final_intent_categories.update(self.intentCategories(current_definition_id, nozzle_name, material_id))
return list(final_intent_categories)
## The intent that gets selected by default when no intent is available for
# the configuration, an extruder can't match the intent that the user
# selects, or just when creating a new printer.
def getDefaultIntent(self) -> "InstanceContainer":
return empty_intent_container
@pyqtProperty(str, notify = intentCategoryChanged)
def currentIntentCategory(self) -> str:
application = cura.CuraApplication.CuraApplication.getInstance()
active_extruder_stack = application.getMachineManager().activeStack
if active_extruder_stack is None:
return ""
return active_extruder_stack.intent.getMetaDataEntry("intent_category", "")
## Apply intent on the stacks.
@pyqtSlot(str, str)
def selectIntent(self, intent_category: str, quality_type: str) -> None:
Logger.log("i", "Attempting to set intent_category to [%s] and quality type to [%s]", intent_category, quality_type)
old_intent_category = self.currentIntentCategory
application = cura.CuraApplication.CuraApplication.getInstance()
global_stack = application.getGlobalContainerStack()
if global_stack is None:
return
current_definition_id = global_stack.definition.getId()
machine_node = ContainerTree.getInstance().machines[current_definition_id]
for extruder_stack in global_stack.extruderList:
nozzle_name = extruder_stack.variant.getMetaDataEntry("name")
material_id = extruder_stack.material.getMetaDataEntry("base_file")
material_node = machine_node.variants[nozzle_name].materials[material_id]
# Since we want to switch to a certain quality type, check the tree if we have one.
quality_node = None
for q_node in material_node.qualities.values():
if q_node.quality_type == quality_type:
quality_node = q_node
if quality_node is None:
Logger.log("w", "Unable to find quality_type [%s] for extruder [%s]", quality_type, extruder_stack.getId())
continue
# Check that quality node if we can find a matching intent.
intent_id = None
for id, intent_node in quality_node.intents.items():
if intent_node.intent_category == intent_category:
intent_id = id
intent = application.getContainerRegistry().findContainers(id = intent_id)
if intent:
extruder_stack.intent = intent[0]
else:
extruder_stack.intent = self.getDefaultIntent()
application.getMachineManager().setQualityGroupByQualityType(quality_type)
if old_intent_category != intent_category:
self.intentCategoryChanged.emit()

File diff suppressed because it is too large Load Diff

View File

@ -12,6 +12,10 @@ from .CuraContainerStack import CuraContainerStack
class PerObjectContainerStack(CuraContainerStack): class PerObjectContainerStack(CuraContainerStack):
def isDirty(self):
# This stack should never be auto saved, so always return that there is nothing to save.
return False
@override(CuraContainerStack) @override(CuraContainerStack)
def getProperty(self, key: str, property_name: str, context: Optional[PropertyEvaluationContext] = None) -> Any: def getProperty(self, key: str, property_name: str, context: Optional[PropertyEvaluationContext] = None) -> Any:
if context is None: if context is None:

View File

@ -28,20 +28,21 @@ if TYPE_CHECKING:
class SettingInheritanceManager(QObject): class SettingInheritanceManager(QObject):
def __init__(self, parent = None) -> None: def __init__(self, parent = None) -> None:
super().__init__(parent) super().__init__(parent)
Application.getInstance().globalContainerStackChanged.connect(self._onGlobalContainerChanged)
self._global_container_stack = None # type: Optional[ContainerStack] self._global_container_stack = None # type: Optional[ContainerStack]
self._settings_with_inheritance_warning = [] # type: List[str] self._settings_with_inheritance_warning = [] # type: List[str]
self._active_container_stack = None # type: Optional[ExtruderStack] self._active_container_stack = None # type: Optional[ExtruderStack]
self._onGlobalContainerChanged()
ExtruderManager.getInstance().activeExtruderChanged.connect(self._onActiveExtruderChanged)
self._onActiveExtruderChanged()
self._update_timer = QTimer() self._update_timer = QTimer()
self._update_timer.setInterval(500) self._update_timer.setInterval(500)
self._update_timer.setSingleShot(True) self._update_timer.setSingleShot(True)
self._update_timer.timeout.connect(self._update) self._update_timer.timeout.connect(self._update)
Application.getInstance().globalContainerStackChanged.connect(self._onGlobalContainerChanged)
ExtruderManager.getInstance().activeExtruderChanged.connect(self._onActiveExtruderChanged)
self._onGlobalContainerChanged()
self._onActiveExtruderChanged()
settingsWithIntheritanceChanged = pyqtSignal() settingsWithIntheritanceChanged = pyqtSignal()
## Get the keys of all children settings with an override. ## Get the keys of all children settings with an override.
@ -88,8 +89,8 @@ class SettingInheritanceManager(QObject):
self.settingsWithIntheritanceChanged.emit() self.settingsWithIntheritanceChanged.emit()
@pyqtSlot() @pyqtSlot()
def forceUpdate(self) -> None: def scheduleUpdate(self) -> None:
self._update() self._update_timer.start()
def _onActiveExtruderChanged(self) -> None: def _onActiveExtruderChanged(self) -> None:
new_active_stack = ExtruderManager.getInstance().getActiveExtruderStack() new_active_stack = ExtruderManager.getInstance().getActiveExtruderStack()
@ -106,7 +107,7 @@ class SettingInheritanceManager(QObject):
if self._active_container_stack is not None: if self._active_container_stack is not None:
self._active_container_stack.propertyChanged.connect(self._onPropertyChanged) self._active_container_stack.propertyChanged.connect(self._onPropertyChanged)
self._active_container_stack.containersChanged.connect(self._onContainersChanged) self._active_container_stack.containersChanged.connect(self._onContainersChanged)
self._update() # Ensure that the settings_with_inheritance_warning list is populated. self._update_timer.start() # Ensure that the settings_with_inheritance_warning list is populated.
def _onPropertyChanged(self, key: str, property_name: str) -> None: def _onPropertyChanged(self, key: str, property_name: str) -> None:
if (property_name == "value" or property_name == "enabled") and self._global_container_stack: if (property_name == "value" or property_name == "enabled") and self._global_container_stack:

View File

@ -39,8 +39,8 @@ class SimpleModeSettingsManager(QObject):
user_setting_keys.update(global_stack.userChanges.getAllKeys()) user_setting_keys.update(global_stack.userChanges.getAllKeys())
# check user settings in the extruder stacks # check user settings in the extruder stacks
if global_stack.extruders: if global_stack.extruderList:
for extruder_stack in global_stack.extruders.values(): for extruder_stack in global_stack.extruderList:
user_setting_keys.update(extruder_stack.userChanges.getAllKeys()) user_setting_keys.update(extruder_stack.userChanges.getAllKeys())
# remove settings that are visible in recommended (we don't show the reset button for those) # remove settings that are visible in recommended (we don't show the reset button for those)

View File

@ -25,6 +25,9 @@ EMPTY_MATERIAL_CONTAINER_ID = "empty_material"
empty_material_container = copy.deepcopy(empty_container) empty_material_container = copy.deepcopy(empty_container)
empty_material_container.setMetaDataEntry("id", EMPTY_MATERIAL_CONTAINER_ID) empty_material_container.setMetaDataEntry("id", EMPTY_MATERIAL_CONTAINER_ID)
empty_material_container.setMetaDataEntry("type", "material") empty_material_container.setMetaDataEntry("type", "material")
empty_material_container.setMetaDataEntry("base_file", "empty_material")
empty_material_container.setMetaDataEntry("GUID", "FFFFFFFF-FFFF-FFFF-FFFF-FFFFFFFFFFFF")
empty_material_container.setMetaDataEntry("material", "empty")
# Empty quality # Empty quality
EMPTY_QUALITY_CONTAINER_ID = "empty_quality" EMPTY_QUALITY_CONTAINER_ID = "empty_quality"
@ -41,6 +44,15 @@ empty_quality_changes_container = copy.deepcopy(empty_container)
empty_quality_changes_container.setMetaDataEntry("id", EMPTY_QUALITY_CHANGES_CONTAINER_ID) empty_quality_changes_container.setMetaDataEntry("id", EMPTY_QUALITY_CHANGES_CONTAINER_ID)
empty_quality_changes_container.setMetaDataEntry("type", "quality_changes") empty_quality_changes_container.setMetaDataEntry("type", "quality_changes")
empty_quality_changes_container.setMetaDataEntry("quality_type", "not_supported") empty_quality_changes_container.setMetaDataEntry("quality_type", "not_supported")
empty_quality_changes_container.setMetaDataEntry("intent_category", "not_supported")
# Empty intent
EMPTY_INTENT_CONTAINER_ID = "empty_intent"
empty_intent_container = copy.deepcopy(empty_container)
empty_intent_container.setMetaDataEntry("id", EMPTY_INTENT_CONTAINER_ID)
empty_intent_container.setMetaDataEntry("type", "intent")
empty_intent_container.setMetaDataEntry("intent_category", "default")
empty_intent_container.setName(catalog.i18nc("@info:No intent profile selected", "Default"))
# All empty container IDs set # All empty container IDs set
@ -51,6 +63,7 @@ ALL_EMPTY_CONTAINER_ID_SET = {
EMPTY_MATERIAL_CONTAINER_ID, EMPTY_MATERIAL_CONTAINER_ID,
EMPTY_QUALITY_CONTAINER_ID, EMPTY_QUALITY_CONTAINER_ID,
EMPTY_QUALITY_CHANGES_CONTAINER_ID, EMPTY_QUALITY_CHANGES_CONTAINER_ID,
EMPTY_INTENT_CONTAINER_ID
} }
@ -73,4 +86,6 @@ __all__ = ["EMPTY_CONTAINER_ID",
"empty_quality_container", "empty_quality_container",
"ALL_EMPTY_CONTAINER_ID_SET", "ALL_EMPTY_CONTAINER_ID_SET",
"isEmptyContainer", "isEmptyContainer",
"EMPTY_INTENT_CONTAINER_ID",
"empty_intent_container"
] ]

View File

@ -87,7 +87,7 @@ class SingleInstance:
if command == "clear-all": if command == "clear-all":
self._application.callLater(lambda: self._application.deleteAll()) self._application.callLater(lambda: self._application.deleteAll())
# Command: Load a model file # Command: Load a model or project file
elif command == "open": elif command == "open":
self._application.callLater(lambda f = payload["filePath"]: self._application._openFile(f)) self._application.callLater(lambda f = payload["filePath"]: self._application._openFile(f))

View File

@ -66,7 +66,7 @@ class Snapshot:
looking_from_offset = Vector(-1, 1, 2) looking_from_offset = Vector(-1, 1, 2)
if size > 0: if size > 0:
# determine the watch distance depending on the size # determine the watch distance depending on the size
looking_from_offset = looking_from_offset * size * 1.3 looking_from_offset = looking_from_offset * size * 1.75
camera.setPosition(look_at + looking_from_offset) camera.setPosition(look_at + looking_from_offset)
camera.lookAt(look_at) camera.lookAt(look_at)
@ -85,8 +85,10 @@ class Snapshot:
preview_pass.setCamera(camera) preview_pass.setCamera(camera)
preview_pass.render() preview_pass.render()
pixel_output = preview_pass.getOutput() pixel_output = preview_pass.getOutput()
try:
min_x, max_x, min_y, max_y = Snapshot.getImageBoundaries(pixel_output) min_x, max_x, min_y, max_y = Snapshot.getImageBoundaries(pixel_output)
except ValueError:
return None
size = max((max_x - min_x) / render_width, (max_y - min_y) / render_height) size = max((max_x - min_x) / render_width, (max_y - min_y) / render_height)
if size > 0.5 or satisfied: if size > 0.5 or satisfied:

View File

@ -7,14 +7,21 @@ from PyQt5.QtWidgets import QSplashScreen
from UM.Resources import Resources from UM.Resources import Resources
from UM.Application import Application from UM.Application import Application
from cura import ApplicationMetadata
class CuraSplashScreen(QSplashScreen): class CuraSplashScreen(QSplashScreen):
def __init__(self): def __init__(self):
super().__init__() super().__init__()
self._scale = 0.7 self._scale = 0.7
self._version_y_offset = 0 # when extra visual elements are in the background image, move version text down
if ApplicationMetadata.IsEnterpriseVersion:
splash_image = QPixmap(Resources.getPath(Resources.Images, "cura_enterprise.png"))
self._version_y_offset = 26
else:
splash_image = QPixmap(Resources.getPath(Resources.Images, "cura.png"))
splash_image = QPixmap(Resources.getPath(Resources.Images, "cura.png"))
self.setPixmap(splash_image) self.setPixmap(splash_image)
self._current_message = "" self._current_message = ""
@ -52,30 +59,27 @@ class CuraSplashScreen(QSplashScreen):
painter.setRenderHint(QPainter.Antialiasing, True) painter.setRenderHint(QPainter.Antialiasing, True)
version = Application.getInstance().getVersion().split("-") version = Application.getInstance().getVersion().split("-")
buildtype = Application.getInstance().getBuildType()
if buildtype:
version[0] += " (%s)" % buildtype
# draw version text # Draw version text
font = QFont() # Using system-default font here font = QFont() # Using system-default font here
font.setPixelSize(37) font.setPixelSize(18)
painter.setFont(font) painter.setFont(font)
painter.drawText(215, 66, 330 * self._scale, 230 * self._scale, Qt.AlignLeft | Qt.AlignTop, version[0]) painter.drawText(60, 70 + self._version_y_offset, 330 * self._scale, 230 * self._scale, Qt.AlignLeft | Qt.AlignTop, version[0])
if len(version) > 1: if len(version) > 1:
font.setPixelSize(16) font.setPixelSize(16)
painter.setFont(font) painter.setFont(font)
painter.setPen(QColor(200, 200, 200, 255)) painter.setPen(QColor(200, 200, 200, 255))
painter.drawText(247, 105, 330 * self._scale, 255 * self._scale, Qt.AlignLeft | Qt.AlignTop, version[1]) painter.drawText(247, 105 + self._version_y_offset, 330 * self._scale, 255 * self._scale, Qt.AlignLeft | Qt.AlignTop, version[1])
painter.setPen(QColor(255, 255, 255, 255)) painter.setPen(QColor(255, 255, 255, 255))
# draw the loading image # Draw the loading image
pen = QPen() pen = QPen()
pen.setWidth(6 * self._scale) pen.setWidth(6 * self._scale)
pen.setColor(QColor(32, 166, 219, 255)) pen.setColor(QColor(32, 166, 219, 255))
painter.setPen(pen) painter.setPen(pen)
painter.drawArc(60, 150, 32 * self._scale, 32 * self._scale, self._loading_image_rotation_angle * 16, 300 * 16) painter.drawArc(60, 150, 32 * self._scale, 32 * self._scale, self._loading_image_rotation_angle * 16, 300 * 16)
# draw message text # Draw message text
if self._current_message: if self._current_message:
font = QFont() # Using system-default font here font = QFont() # Using system-default font here
font.setPixelSize(13) font.setPixelSize(13)

View File

@ -2,11 +2,12 @@
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
from typing import Optional, TYPE_CHECKING from typing import Optional, TYPE_CHECKING
from PyQt5.QtCore import QObject, pyqtSlot from PyQt5.QtCore import QObject, pyqtSlot
from UM.i18n import i18nCatalog from UM.i18n import i18nCatalog
from cura.Machines.ContainerTree import ContainerTree
if TYPE_CHECKING: if TYPE_CHECKING:
from cura.CuraApplication import CuraApplication from cura.CuraApplication import CuraApplication
@ -42,7 +43,7 @@ class MachineSettingsManager(QObject):
# it was moved to the machine manager instead. Now this method just calls the machine manager. # it was moved to the machine manager instead. Now this method just calls the machine manager.
self._application.getMachineManager().setActiveMachineExtruderCount(extruder_count) self._application.getMachineManager().setActiveMachineExtruderCount(extruder_count)
# Function for the Machine Settings panel (QML) to update after the usre changes "Number of Extruders". # Function for the Machine Settings panel (QML) to update after the user changes "Number of Extruders".
# #
# fieldOfView: The Ultimaker 2 family (not 2+) does not have materials in Cura by default, because the material is # fieldOfView: The Ultimaker 2 family (not 2+) does not have materials in Cura by default, because the material is
# to be set on the printer. But when switching to Marlin flavor, the printer firmware can not change/insert material # to be set on the printer. But when switching to Marlin flavor, the printer firmware can not change/insert material
@ -51,8 +52,6 @@ class MachineSettingsManager(QObject):
@pyqtSlot() @pyqtSlot()
def updateHasMaterialsMetadata(self): def updateHasMaterialsMetadata(self):
machine_manager = self._application.getMachineManager() machine_manager = self._application.getMachineManager()
material_manager = self._application.getMaterialManager()
global_stack = machine_manager.activeMachine global_stack = machine_manager.activeMachine
definition = global_stack.definition definition = global_stack.definition
@ -76,7 +75,10 @@ class MachineSettingsManager(QObject):
# set materials # set materials
for position in extruder_positions: for position in extruder_positions:
if has_materials: if has_materials:
material_node = material_manager.getDefaultMaterial(global_stack, position, None) extruder = global_stack.extruderList[int(position)]
approximate_diameter = extruder.getApproximateMaterialDiameter()
variant_node = ContainerTree.getInstance().machines[global_stack.definition.getId()].variants[extruder.variant.getName()]
material_node = variant_node.preferredMaterial(approximate_diameter)
machine_manager.setMaterial(position, material_node) machine_manager.setMaterial(position, material_node)
self.forceUpdate() self.forceUpdate()

View File

@ -50,6 +50,7 @@ class ObjectsModel(ListModel):
Application.getInstance().getController().getScene().sceneChanged.connect(self._updateSceneDelayed) Application.getInstance().getController().getScene().sceneChanged.connect(self._updateSceneDelayed)
Application.getInstance().getPreferences().preferenceChanged.connect(self._updateDelayed) Application.getInstance().getPreferences().preferenceChanged.connect(self._updateDelayed)
Selection.selectionChanged.connect(self._updateDelayed)
self._update_timer = QTimer() self._update_timer = QTimer()
self._update_timer.setInterval(200) self._update_timer.setInterval(200)

View File

@ -197,11 +197,7 @@ class PrintInformation(QObject):
material_preference_values = json.loads(self._application.getInstance().getPreferences().getValue("cura/material_settings")) material_preference_values = json.loads(self._application.getInstance().getPreferences().getValue("cura/material_settings"))
extruder_stacks = global_stack.extruders for index, extruder_stack in enumerate(global_stack.extruderList):
for position in extruder_stacks:
extruder_stack = extruder_stacks[position]
index = int(position)
if index >= len(self._material_amounts): if index >= len(self._material_amounts):
continue continue
amount = self._material_amounts[index] amount = self._material_amounts[index]
@ -302,9 +298,7 @@ class PrintInformation(QObject):
# Only update the job name when it's not user-specified. # Only update the job name when it's not user-specified.
if not self._is_user_specified_job_name: if not self._is_user_specified_job_name:
if self._pre_sliced: if self._application.getInstance().getPreferences().getValue("cura/jobname_prefix") and not self._pre_sliced:
self._job_name = catalog.i18nc("@label", "Pre-sliced file {0}", base_name)
elif self._application.getInstance().getPreferences().getValue("cura/jobname_prefix"):
# Don't add abbreviation if it already has the exact same abbreviation. # Don't add abbreviation if it already has the exact same abbreviation.
if base_name.startswith(self._abbr_machine + "_"): if base_name.startswith(self._abbr_machine + "_"):
self._job_name = base_name self._job_name = base_name

30
cura/Utils/Decorators.py Normal file
View File

@ -0,0 +1,30 @@
# Copyright (c) 2019 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
import functools
import re
from typing import Callable
# An API version must be a semantic version "x.y.z" where ".z" is optional. So the valid formats are as follows:
# - x.y.z
# - x.y
SEMANTIC_VERSION_REGEX = re.compile(r"^[0-9]+\.[0-9]+(\.[0-9]+)?$")
## Decorator for functions that belong to a set of APIs. For now, this should only be used for officially supported
# APIs, meaning that those APIs should be versioned and maintained.
#
# \param since_version The earliest version since when this API becomes supported. This means that since this version,
# this API function is supposed to behave the same. This parameter is not used. It's just a
# documentation.
def api(since_version: str) -> Callable:
# Make sure that APi versions are semantic versions
if not SEMANTIC_VERSION_REGEX.fullmatch(since_version):
raise ValueError("API since_version [%s] is not a semantic version." % since_version)
def api_decorator(function):
@functools.wraps(function)
def api_wrapper(*args, **kwargs):
return function(*args, **kwargs)
return api_wrapper
return api_decorator

View File

@ -1,6 +1,6 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# Copyright (c) 2018 Ultimaker B.V. # Copyright (c) 2019 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
import argparse import argparse
@ -32,7 +32,8 @@ if not known_args["debug"]:
elif Platform.isOSX(): elif Platform.isOSX():
return os.path.expanduser("~/Library/Logs/" + CuraAppName) return os.path.expanduser("~/Library/Logs/" + CuraAppName)
if hasattr(sys, "frozen"): # Do not redirect stdout and stderr to files if we are running CLI.
if hasattr(sys, "frozen") and "cli" not in os.path.basename(sys.argv[0]).lower():
dirpath = get_cura_dir_path() dirpath = get_cura_dir_path()
os.makedirs(dirpath, exist_ok = True) os.makedirs(dirpath, exist_ok = True)
sys.stdout = open(os.path.join(dirpath, "stdout.log"), "w", encoding = "utf-8") sys.stdout = open(os.path.join(dirpath, "stdout.log"), "w", encoding = "utf-8")
@ -59,6 +60,14 @@ if Platform.isWindows() and hasattr(sys, "frozen"):
except KeyError: except KeyError:
pass pass
# GITHUB issue #6194: https://github.com/Ultimaker/Cura/issues/6194
# With AppImage 2 on Linux, the current working directory will be somewhere in /tmp/<rand>/usr, which is owned
# by root. For some reason, QDesktopServices.openUrl() requires to have a usable current working directory,
# otherwise it doesn't work. This is a workaround on Linux that before we call QDesktopServices.openUrl(), we
# switch to a directory where the user has the ownership.
if Platform.isLinux() and hasattr(sys, "frozen"):
os.chdir(os.path.expanduser("~"))
# WORKAROUND: GITHUB-704 GITHUB-708 # WORKAROUND: GITHUB-704 GITHUB-708
# It looks like setuptools creates a .pth file in # It looks like setuptools creates a .pth file in
# the default /usr/lib which causes the default site-packages # the default /usr/lib which causes the default site-packages
@ -122,7 +131,10 @@ def exceptHook(hook_type, value, traceback):
# Set exception hook to use the crash dialog handler # Set exception hook to use the crash dialog handler
sys.excepthook = exceptHook sys.excepthook = exceptHook
# Enable dumping traceback for all threads # Enable dumping traceback for all threads
faulthandler.enable(all_threads = True) if sys.stderr:
faulthandler.enable(file = sys.stderr, all_threads = True)
else:
faulthandler.enable(file = sys.stdout, all_threads = True)
# Workaround for a race condition on certain systems where there # Workaround for a race condition on certain systems where there
# is a race condition between Arcus and PyQt. Importing Arcus # is a race condition between Arcus and PyQt. Importing Arcus
@ -132,5 +144,37 @@ import Arcus #@UnusedImport
import Savitar #@UnusedImport import Savitar #@UnusedImport
from cura.CuraApplication import CuraApplication from cura.CuraApplication import CuraApplication
# WORKAROUND: CURA-6739
# The CTM file loading module in Trimesh requires the OpenCTM library to be dynamically loaded. It uses
# ctypes.util.find_library() to find libopenctm.dylib, but this doesn't seem to look in the ".app" application folder
# on Mac OS X. Adding the search path to environment variables such as DYLD_LIBRARY_PATH and DYLD_FALLBACK_LIBRARY_PATH
# makes it work. The workaround here uses DYLD_FALLBACK_LIBRARY_PATH.
if Platform.isOSX() and getattr(sys, "frozen", False):
old_env = os.environ.get("DYLD_FALLBACK_LIBRARY_PATH", "")
# This is where libopenctm.so is in the .app folder.
search_path = os.path.join(CuraApplication.getInstallPrefix(), "MacOS")
path_list = old_env.split(":")
if search_path not in path_list:
path_list.append(search_path)
os.environ["DYLD_FALLBACK_LIBRARY_PATH"] = ":".join(path_list)
import trimesh.exchange.load
os.environ["DYLD_FALLBACK_LIBRARY_PATH"] = old_env
# WORKAROUND: CURA-6739
# Similar CTM file loading fix for Linux, but NOTE THAT this doesn't work directly with Python 3.5.7. There's a fix
# for ctypes.util.find_library() in Python 3.6 and 3.7. That fix makes sure that find_library() will check
# LD_LIBRARY_PATH. With Python 3.5, that fix needs to be backported to make this workaround work.
if Platform.isLinux() and getattr(sys, "frozen", False):
old_env = os.environ.get("LD_LIBRARY_PATH", "")
# This is where libopenctm.so is in the AppImage.
search_path = os.path.join(CuraApplication.getInstallPrefix(), "bin")
path_list = old_env.split(":")
if search_path not in path_list:
path_list.append(search_path)
os.environ["LD_LIBRARY_PATH"] = ":".join(path_list)
import trimesh.exchange.load
os.environ["LD_LIBRARY_PATH"] = old_env
app = CuraApplication() app = CuraApplication()
app.run() app.run()

View File

@ -20,7 +20,16 @@ cd "${PROJECT_DIR}"
# Check the branch to use: # Check the branch to use:
# 1. Use the Uranium branch with the branch same if it exists. # 1. Use the Uranium branch with the branch same if it exists.
# 2. Otherwise, use the default branch name "master" # 2. Otherwise, use the default branch name "master"
URANIUM_BRANCH="${CI_COMMIT_REF_NAME:-master}" echo "GITHUB_REF: ${GITHUB_REF}"
echo "GITHUB_BASE_REF: ${GITHUB_BASE_REF}"
GIT_REF_NAME="${GITHUB_REF}"
if [ -n "${GITHUB_BASE_REF}" ]; then
GIT_REF_NAME="${GITHUB_BASE_REF}"
fi
GIT_REF_NAME="$(basename "${GIT_REF_NAME}")"
URANIUM_BRANCH="${GIT_REF_NAME:-master}"
output="$(git ls-remote --heads https://github.com/Ultimaker/Uranium.git "${URANIUM_BRANCH}")" output="$(git ls-remote --heads https://github.com/Ultimaker/Uranium.git "${URANIUM_BRANCH}")"
if [ -z "${output}" ]; then if [ -z "${output}" ]; then
echo "Could not find Uranium banch ${URANIUM_BRANCH}, fallback to use master." echo "Could not find Uranium banch ${URANIUM_BRANCH}, fallback to use master."

View File

@ -19,12 +19,12 @@ from UM.Scene.SceneNode import SceneNode #For typing.
from UM.MimeTypeDatabase import MimeTypeDatabase, MimeType from UM.MimeTypeDatabase import MimeTypeDatabase, MimeType
from cura.CuraApplication import CuraApplication from cura.CuraApplication import CuraApplication
from cura.Machines.ContainerTree import ContainerTree
from cura.Settings.ExtruderManager import ExtruderManager from cura.Settings.ExtruderManager import ExtruderManager
from cura.Scene.CuraSceneNode import CuraSceneNode from cura.Scene.CuraSceneNode import CuraSceneNode
from cura.Scene.BuildPlateDecorator import BuildPlateDecorator from cura.Scene.BuildPlateDecorator import BuildPlateDecorator
from cura.Scene.SliceableObjectDecorator import SliceableObjectDecorator from cura.Scene.SliceableObjectDecorator import SliceableObjectDecorator
from cura.Scene.ZOffsetDecorator import ZOffsetDecorator from cura.Scene.ZOffsetDecorator import ZOffsetDecorator
from cura.Machines.QualityManager import getMachineDefinitionIDForQualitySearch
try: try:
@ -131,7 +131,7 @@ class ThreeMFReader(MeshReader):
um_node.callDecoration("setActiveExtruder", default_stack.getId()) um_node.callDecoration("setActiveExtruder", default_stack.getId())
# Get the definition & set it # Get the definition & set it
definition_id = getMachineDefinitionIDForQualitySearch(global_container_stack.definition) definition_id = ContainerTree.getInstance().machines[global_container_stack.definition.getId()].quality_definition
um_node.callDecoration("getStack").getTop().setDefinition(definition_id) um_node.callDecoration("getStack").getTop().setDefinition(definition_id)
setting_container = um_node.callDecoration("getStack").getTop() setting_container = um_node.callDecoration("getStack").getTop()

View File

@ -1,10 +1,10 @@
# Copyright (c) 2018 Ultimaker B.V. # Copyright (c) 2019 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
from configparser import ConfigParser from configparser import ConfigParser
import zipfile import zipfile
import os import os
from typing import Dict, List, Tuple, cast from typing import cast, Dict, List, Optional, Tuple
import xml.etree.ElementTree as ET import xml.etree.ElementTree as ET
@ -14,7 +14,6 @@ from UM.Application import Application
from UM.Logger import Logger from UM.Logger import Logger
from UM.Message import Message from UM.Message import Message
from UM.i18n import i18nCatalog from UM.i18n import i18nCatalog
from UM.Signal import postponeSignals, CompressTechnique
from UM.Settings.ContainerFormatError import ContainerFormatError from UM.Settings.ContainerFormatError import ContainerFormatError
from UM.Settings.ContainerStack import ContainerStack from UM.Settings.ContainerStack import ContainerStack
from UM.Settings.DefinitionContainer import DefinitionContainer from UM.Settings.DefinitionContainer import DefinitionContainer
@ -24,22 +23,25 @@ from UM.MimeTypeDatabase import MimeTypeDatabase, MimeType
from UM.Job import Job from UM.Job import Job
from UM.Preferences import Preferences from UM.Preferences import Preferences
from cura.Machines.VariantType import VariantType from cura.Machines.ContainerTree import ContainerTree
from cura.Settings.CuraStackBuilder import CuraStackBuilder from cura.Settings.CuraStackBuilder import CuraStackBuilder
from cura.Settings.ExtruderManager import ExtruderManager from cura.Settings.ExtruderManager import ExtruderManager
from cura.Settings.ExtruderStack import ExtruderStack from cura.Settings.ExtruderStack import ExtruderStack
from cura.Settings.GlobalStack import GlobalStack from cura.Settings.GlobalStack import GlobalStack
from cura.Settings.IntentManager import IntentManager
from cura.Settings.CuraContainerStack import _ContainerIndexes from cura.Settings.CuraContainerStack import _ContainerIndexes
from cura.CuraApplication import CuraApplication from cura.CuraApplication import CuraApplication
from cura.Utils.Threading import call_on_qt_thread from cura.Utils.Threading import call_on_qt_thread
from PyQt5.QtCore import QCoreApplication
from .WorkspaceDialog import WorkspaceDialog from .WorkspaceDialog import WorkspaceDialog
i18n_catalog = i18nCatalog("cura") i18n_catalog = i18nCatalog("cura")
class ContainerInfo: class ContainerInfo:
def __init__(self, file_name: str, serialized: str, parser: ConfigParser) -> None: def __init__(self, file_name: Optional[str], serialized: Optional[str], parser: Optional[ConfigParser]) -> None:
self.file_name = file_name self.file_name = file_name
self.serialized = serialized self.serialized = serialized
self.parser = parser self.parser = parser
@ -59,7 +61,11 @@ class MachineInfo:
self.container_id = None self.container_id = None
self.name = None self.name = None
self.definition_id = None self.definition_id = None
self.metadata_dict = {} # type: Dict[str, str]
self.quality_type = None self.quality_type = None
self.intent_category = None
self.custom_quality_name = None self.custom_quality_name = None
self.quality_changes_info = None self.quality_changes_info = None
self.variant_info = None self.variant_info = None
@ -79,6 +85,7 @@ class ExtruderInfo:
self.definition_changes_info = None self.definition_changes_info = None
self.user_changes_info = None self.user_changes_info = None
self.intent_info = None
## Base implementation for reading 3MF workspace files. ## Base implementation for reading 3MF workspace files.
@ -227,6 +234,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
else: else:
Logger.log("w", "Unknown definition container type %s for %s", Logger.log("w", "Unknown definition container type %s for %s",
definition_container_type, definition_container_file) definition_container_type, definition_container_file)
QCoreApplication.processEvents() # Ensure that the GUI does not freeze.
Job.yieldThread() Job.yieldThread()
if machine_definition_container_count != 1: if machine_definition_container_count != 1:
@ -253,12 +261,15 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
containers_found_dict["material"] = True containers_found_dict["material"] = True
if not self._container_registry.isReadOnly(container_id): # Only non readonly materials can be in conflict if not self._container_registry.isReadOnly(container_id): # Only non readonly materials can be in conflict
material_conflict = True material_conflict = True
QCoreApplication.processEvents() # Ensure that the GUI does not freeze.
Job.yieldThread() Job.yieldThread()
# Check if any quality_changes instance container is in conflict. # Check if any quality_changes instance container is in conflict.
instance_container_files = [name for name in cura_file_names if name.endswith(self._instance_container_suffix)] instance_container_files = [name for name in cura_file_names if name.endswith(self._instance_container_suffix)]
quality_name = "" quality_name = ""
custom_quality_name = "" custom_quality_name = ""
intent_name = ""
intent_category = ""
num_settings_overridden_by_quality_changes = 0 # How many settings are changed by the quality changes num_settings_overridden_by_quality_changes = 0 # How many settings are changed by the quality changes
num_user_settings = 0 num_user_settings = 0
quality_changes_conflict = False quality_changes_conflict = False
@ -316,13 +327,17 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
elif container_type == "quality": elif container_type == "quality":
if not quality_name: if not quality_name:
quality_name = parser["general"]["name"] quality_name = parser["general"]["name"]
elif container_type == "intent":
if not intent_name:
intent_name = parser["general"]["name"]
intent_category = parser["metadata"]["intent_category"]
elif container_type == "user": elif container_type == "user":
num_user_settings += len(parser["values"]) num_user_settings += len(parser["values"])
elif container_type in self._ignored_instance_container_types: elif container_type in self._ignored_instance_container_types:
# Ignore certain instance container types # Ignore certain instance container types
Logger.log("w", "Ignoring instance container [%s] with type [%s]", container_id, container_type) Logger.log("w", "Ignoring instance container [%s] with type [%s]", container_id, container_type)
continue continue
QCoreApplication.processEvents() # Ensure that the GUI does not freeze.
Job.yieldThread() Job.yieldThread()
if self._machine_info.quality_changes_info.global_info is None: if self._machine_info.quality_changes_info.global_info is None:
@ -341,7 +356,10 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
# To simplify this, only check if the global stack exists or not # To simplify this, only check if the global stack exists or not
global_stack_id = self._stripFileToId(global_stack_file) global_stack_id = self._stripFileToId(global_stack_file)
serialized = archive.open(global_stack_file).read().decode("utf-8") serialized = archive.open(global_stack_file).read().decode("utf-8")
serialized = GlobalStack._updateSerialized(serialized, global_stack_file)
machine_name = self._getMachineNameFromSerializedStack(serialized) machine_name = self._getMachineNameFromSerializedStack(serialized)
self._machine_info.metadata_dict = self._getMetaDataDictFromSerializedStack(serialized)
stacks = self._container_registry.findContainerStacks(name = machine_name, type = "machine") stacks = self._container_registry.findContainerStacks(name = machine_name, type = "machine")
self._is_same_machine_type = True self._is_same_machine_type = True
existing_global_stack = None existing_global_stack = None
@ -397,7 +415,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
variant_id = parser["containers"][str(_ContainerIndexes.Variant)] variant_id = parser["containers"][str(_ContainerIndexes.Variant)]
if variant_id not in ("empty", "empty_variant"): if variant_id not in ("empty", "empty_variant"):
self._machine_info.variant_info = instance_container_info_dict[variant_id] self._machine_info.variant_info = instance_container_info_dict[variant_id]
QCoreApplication.processEvents() # Ensure that the GUI does not freeze.
Job.yieldThread() Job.yieldThread()
# if the global stack is found, we check if there are conflicts in the extruder stacks # if the global stack is found, we check if there are conflicts in the extruder stacks
@ -419,18 +437,26 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
if parser.has_option("metadata", "enabled"): if parser.has_option("metadata", "enabled"):
extruder_info.enabled = parser["metadata"]["enabled"] extruder_info.enabled = parser["metadata"]["enabled"]
if variant_id not in ("empty", "empty_variant"): if variant_id not in ("empty", "empty_variant"):
extruder_info.variant_info = instance_container_info_dict[variant_id] if variant_id in instance_container_info_dict:
extruder_info.variant_info = instance_container_info_dict[variant_id]
if material_id not in ("empty", "empty_material"): if material_id not in ("empty", "empty_material"):
root_material_id = reverse_material_id_dict[material_id] root_material_id = reverse_material_id_dict[material_id]
extruder_info.root_material_id = root_material_id extruder_info.root_material_id = root_material_id
definition_changes_id = parser["containers"][str(_ContainerIndexes.DefinitionChanges)] definition_changes_id = parser["containers"][str(_ContainerIndexes.DefinitionChanges)]
if definition_changes_id not in ("empty", "empty_definition_changes"): if definition_changes_id not in ("empty", "empty_definition_changes"):
extruder_info.definition_changes_info = instance_container_info_dict[definition_changes_id] extruder_info.definition_changes_info = instance_container_info_dict[definition_changes_id]
user_changes_id = parser["containers"][str(_ContainerIndexes.UserChanges)] user_changes_id = parser["containers"][str(_ContainerIndexes.UserChanges)]
if user_changes_id not in ("empty", "empty_user_changes"): if user_changes_id not in ("empty", "empty_user_changes"):
extruder_info.user_changes_info = instance_container_info_dict[user_changes_id] extruder_info.user_changes_info = instance_container_info_dict[user_changes_id]
self._machine_info.extruder_info_dict[position] = extruder_info self._machine_info.extruder_info_dict[position] = extruder_info
intent_id = parser["containers"][str(_ContainerIndexes.Intent)]
if intent_id not in ("empty", "empty_intent"):
extruder_info.intent_info = instance_container_info_dict[intent_id]
if not machine_conflict and containers_found_dict["machine"]: if not machine_conflict and containers_found_dict["machine"]:
if position not in global_stack.extruders: if position not in global_stack.extruders:
continue continue
@ -495,6 +521,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
self._machine_info.definition_id = machine_definition_id self._machine_info.definition_id = machine_definition_id
self._machine_info.quality_type = quality_type self._machine_info.quality_type = quality_type
self._machine_info.custom_quality_name = quality_name self._machine_info.custom_quality_name = quality_name
self._machine_info.intent_category = intent_category
if machine_conflict and not self._is_same_machine_type: if machine_conflict and not self._is_same_machine_type:
machine_conflict = False machine_conflict = False
@ -515,6 +542,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
self._dialog.setNumVisibleSettings(num_visible_settings) self._dialog.setNumVisibleSettings(num_visible_settings)
self._dialog.setQualityName(quality_name) self._dialog.setQualityName(quality_name)
self._dialog.setQualityType(quality_type) self._dialog.setQualityType(quality_type)
self._dialog.setIntentName(intent_name)
self._dialog.setNumSettingsOverriddenByQualityChanges(num_settings_overridden_by_quality_changes) self._dialog.setNumSettingsOverriddenByQualityChanges(num_settings_overridden_by_quality_changes)
self._dialog.setNumUserSettings(num_user_settings) self._dialog.setNumUserSettings(num_user_settings)
self._dialog.setActiveMode(active_mode) self._dialog.setActiveMode(active_mode)
@ -558,26 +586,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
# \param file_name # \param file_name
@call_on_qt_thread @call_on_qt_thread
def read(self, file_name): def read(self, file_name):
container_registry = ContainerRegistry.getInstance()
signals = [container_registry.containerAdded,
container_registry.containerRemoved,
container_registry.containerMetaDataChanged]
#
# We now have different managers updating their lookup tables upon container changes. It is critical to make
# sure that the managers have a complete set of data when they update.
#
# In project loading, lots of the container-related signals are loosely emitted, which can create timing gaps
# for incomplete data update or other kinds of issues to happen.
#
# To avoid this, we postpone all signals so they don't get emitted immediately. But, please also be aware that,
# because of this, do not expect to have the latest data in the lookup tables in project loading.
#
with postponeSignals(*signals, compress = CompressTechnique.NoCompression):
return self._read(file_name)
def _read(self, file_name):
application = CuraApplication.getInstance() application = CuraApplication.getInstance()
material_manager = application.getMaterialManager()
archive = zipfile.ZipFile(file_name, "r") archive = zipfile.ZipFile(file_name, "r")
@ -648,6 +657,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
definition_container = self._container_registry.findDefinitionContainers(id = "fdmprinter")[0] #Fall back to defaults. definition_container = self._container_registry.findDefinitionContainers(id = "fdmprinter")[0] #Fall back to defaults.
self._container_registry.addContainer(definition_container) self._container_registry.addContainer(definition_container)
Job.yieldThread() Job.yieldThread()
QCoreApplication.processEvents() # Ensure that the GUI does not freeze.
Logger.log("d", "Workspace loading is checking materials...") Logger.log("d", "Workspace loading is checking materials...")
# Get all the material files and check if they exist. If not, add them. # Get all the material files and check if they exist. If not, add them.
@ -674,7 +684,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
if self._resolve_strategies["material"] == "override": if self._resolve_strategies["material"] == "override":
# Remove the old materials and then deserialize the one from the project # Remove the old materials and then deserialize the one from the project
root_material_id = material_container.getMetaDataEntry("base_file") root_material_id = material_container.getMetaDataEntry("base_file")
material_manager.removeMaterialByRootId(root_material_id) application.getContainerRegistry().removeContainer(root_material_id)
elif self._resolve_strategies["material"] == "new": elif self._resolve_strategies["material"] == "new":
# Note that we *must* deserialize it with a new ID, as multiple containers will be # Note that we *must* deserialize it with a new ID, as multiple containers will be
# auto created & added. # auto created & added.
@ -697,6 +707,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
material_container.setDirty(True) material_container.setDirty(True)
self._container_registry.addContainer(material_container) self._container_registry.addContainer(material_container)
Job.yieldThread() Job.yieldThread()
QCoreApplication.processEvents() # Ensure that the GUI does not freeze.
# Handle quality changes if any # Handle quality changes if any
self._processQualityChanges(global_stack) self._processQualityChanges(global_stack)
@ -727,9 +738,6 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
if self._machine_info.quality_changes_info is None: if self._machine_info.quality_changes_info is None:
return return
application = CuraApplication.getInstance()
quality_manager = application.getQualityManager()
# If we have custom profiles, load them # If we have custom profiles, load them
quality_changes_name = self._machine_info.quality_changes_info.name quality_changes_name = self._machine_info.quality_changes_info.name
if self._machine_info.quality_changes_info is not None: if self._machine_info.quality_changes_info is not None:
@ -737,12 +745,12 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
self._machine_info.quality_changes_info.name) self._machine_info.quality_changes_info.name)
# Get the correct extruder definition IDs for quality changes # Get the correct extruder definition IDs for quality changes
from cura.Machines.QualityManager import getMachineDefinitionIDForQualitySearch machine_definition_id_for_quality = ContainerTree.getInstance().machines[global_stack.definition.getId()].quality_definition
machine_definition_id_for_quality = getMachineDefinitionIDForQualitySearch(global_stack.definition)
machine_definition_for_quality = self._container_registry.findDefinitionContainers(id = machine_definition_id_for_quality)[0] machine_definition_for_quality = self._container_registry.findDefinitionContainers(id = machine_definition_id_for_quality)[0]
quality_changes_info = self._machine_info.quality_changes_info quality_changes_info = self._machine_info.quality_changes_info
quality_changes_quality_type = quality_changes_info.global_info.parser["metadata"]["quality_type"] quality_changes_quality_type = quality_changes_info.global_info.parser["metadata"]["quality_type"]
quality_changes_intent_category_per_extruder = {position: info.parser["metadata"].get("intent_category", "default") for position, info in quality_changes_info.extruder_info_dict.items()}
quality_changes_name = quality_changes_info.name quality_changes_name = quality_changes_info.name
create_new = self._resolve_strategies.get("quality_changes") != "override" create_new = self._resolve_strategies.get("quality_changes") != "override"
@ -753,13 +761,12 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
quality_changes_name = self._container_registry.uniqueName(quality_changes_name) quality_changes_name = self._container_registry.uniqueName(quality_changes_name)
for position, container_info in container_info_dict.items(): for position, container_info in container_info_dict.items():
extruder_stack = None extruder_stack = None
intent_category = None # type: Optional[str]
if position is not None: if position is not None:
extruder_stack = global_stack.extruders[position] extruder_stack = global_stack.extruders[position]
container = quality_manager._createQualityChanges(quality_changes_quality_type, intent_category = quality_changes_intent_category_per_extruder[position]
quality_changes_name, container = self._createNewQualityChanges(quality_changes_quality_type, intent_category, quality_changes_name, global_stack, extruder_stack)
global_stack, extruder_stack)
container_info.container = container container_info.container = container
container.setDirty(True)
self._container_registry.addContainer(container) self._container_registry.addContainer(container)
Logger.log("d", "Created new quality changes container [%s]", container.getId()) Logger.log("d", "Created new quality changes container [%s]", container.getId())
@ -787,11 +794,10 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
if not global_stack.extruders: if not global_stack.extruders:
ExtruderManager.getInstance().fixSingleExtrusionMachineExtruderDefinition(global_stack) ExtruderManager.getInstance().fixSingleExtrusionMachineExtruderDefinition(global_stack)
extruder_stack = global_stack.extruders["0"] extruder_stack = global_stack.extruders["0"]
intent_category = quality_changes_intent_category_per_extruder["0"]
container = quality_manager._createQualityChanges(quality_changes_quality_type, quality_changes_name, container = self._createNewQualityChanges(quality_changes_quality_type, intent_category, quality_changes_name, global_stack, extruder_stack)
global_stack, extruder_stack)
container_info.container = container container_info.container = container
container.setDirty(True)
self._container_registry.addContainer(container) self._container_registry.addContainer(container)
Logger.log("d", "Created new quality changes container [%s]", container.getId()) Logger.log("d", "Created new quality changes container [%s]", container.getId())
@ -817,10 +823,9 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
if container_info.container is None: if container_info.container is None:
extruder_stack = global_stack.extruders[position] extruder_stack = global_stack.extruders[position]
container = quality_manager._createQualityChanges(quality_changes_quality_type, quality_changes_name, intent_category = quality_changes_intent_category_per_extruder[position]
global_stack, extruder_stack) container = self._createNewQualityChanges(quality_changes_quality_type, intent_category, quality_changes_name, global_stack, extruder_stack)
container_info.container = container container_info.container = container
container.setDirty(True)
self._container_registry.addContainer(container) self._container_registry.addContainer(container)
for key, value in container_info.parser["values"].items(): for key, value in container_info.parser["values"].items():
@ -828,7 +833,47 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
self._machine_info.quality_changes_info.name = quality_changes_name self._machine_info.quality_changes_info.name = quality_changes_name
def _clearStack(self, stack): ## Helper class to create a new quality changes profile.
#
# This will then later be filled with the appropriate data.
# \param quality_type The quality type of the new profile.
# \param intent_category The intent category of the new profile.
# \param name The name for the profile. This will later be made unique so
# it doesn't need to be unique yet.
# \param global_stack The global stack showing the configuration that the
# profile should be created for.
# \param extruder_stack The extruder stack showing the configuration that
# the profile should be created for. If this is None, it will be created
# for the global stack.
def _createNewQualityChanges(self, quality_type: str, intent_category: Optional[str], name: str, global_stack: GlobalStack, extruder_stack: Optional[ExtruderStack]) -> InstanceContainer:
container_registry = CuraApplication.getInstance().getContainerRegistry()
base_id = global_stack.definition.getId() if extruder_stack is None else extruder_stack.getId()
new_id = base_id + "_" + 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(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[global_stack.definition.getId()].quality_definition
quality_changes.setDefinition(machine_definition_id)
quality_changes.setMetaDataEntry("setting_version", CuraApplication.getInstance().SettingVersion)
quality_changes.setDirty(True)
return quality_changes
@staticmethod
def _clearStack(stack):
application = CuraApplication.getInstance() application = CuraApplication.getInstance()
stack.definitionChanges.clear() stack.definitionChanges.clear()
@ -887,41 +932,30 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
extruder_stack.userChanges.setProperty(key, "value", value) extruder_stack.userChanges.setProperty(key, "value", value)
def _applyVariants(self, global_stack, extruder_stack_dict): def _applyVariants(self, global_stack, extruder_stack_dict):
application = CuraApplication.getInstance() machine_node = ContainerTree.getInstance().machines[global_stack.definition.getId()]
variant_manager = application.getVariantManager()
# Take the global variant from the machine info if available.
if self._machine_info.variant_info is not None: if self._machine_info.variant_info is not None:
parser = self._machine_info.variant_info.parser variant_name = self._machine_info.variant_info.parser["general"]["name"]
variant_name = parser["general"]["name"] if variant_name in machine_node.variants:
global_stack.variant = machine_node.variants[variant_name].container
variant_type = VariantType.BUILD_PLATE else:
Logger.log("w", "Could not find global variant '{0}'.".format(variant_name))
node = variant_manager.getVariantNode(global_stack.definition.getId(), variant_name, variant_type)
if node is not None and node.getContainer() is not None:
global_stack.variant = node.getContainer()
for position, extruder_stack in extruder_stack_dict.items(): for position, extruder_stack in extruder_stack_dict.items():
if position not in self._machine_info.extruder_info_dict: if position not in self._machine_info.extruder_info_dict:
continue continue
extruder_info = self._machine_info.extruder_info_dict[position] extruder_info = self._machine_info.extruder_info_dict[position]
if extruder_info.variant_info is None: if extruder_info.variant_info is None:
continue # If there is no variant_info, try to use the default variant. Otherwise, any available variant.
parser = extruder_info.variant_info.parser node = machine_node.variants.get(machine_node.preferred_variant_name, next(iter(machine_node.variants.values())))
else:
variant_name = parser["general"]["name"] variant_name = extruder_info.variant_info.parser["general"]["name"]
variant_type = VariantType.NOZZLE node = ContainerTree.getInstance().machines[global_stack.definition.getId()].variants[variant_name]
extruder_stack.variant = node.container
node = variant_manager.getVariantNode(global_stack.definition.getId(), variant_name, variant_type)
if node is not None and node.getContainer() is not None:
extruder_stack.variant = node.getContainer()
def _applyMaterials(self, global_stack, extruder_stack_dict): def _applyMaterials(self, global_stack, extruder_stack_dict):
application = CuraApplication.getInstance() machine_node = ContainerTree.getInstance().machines[global_stack.definition.getId()]
material_manager = application.getMaterialManager()
# Force update lookup tables first
material_manager.initialize()
for position, extruder_stack in extruder_stack_dict.items(): for position, extruder_stack in extruder_stack_dict.items():
if position not in self._machine_info.extruder_info_dict: if position not in self._machine_info.extruder_info_dict:
continue continue
@ -932,18 +966,8 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
root_material_id = extruder_info.root_material_id root_material_id = extruder_info.root_material_id
root_material_id = self._old_new_materials.get(root_material_id, root_material_id) root_material_id = self._old_new_materials.get(root_material_id, root_material_id)
build_plate_id = global_stack.variant.getId() material_node = machine_node.variants[extruder_stack.variant.getName()].materials[root_material_id]
extruder_stack.material = material_node.container # type: InstanceContainer
# get material diameter of this extruder
machine_material_diameter = extruder_stack.getCompatibleMaterialDiameter()
material_node = material_manager.getMaterialNode(global_stack.definition.getId(),
extruder_stack.variant.getName(),
build_plate_id,
machine_material_diameter,
root_material_id)
if material_node is not None and material_node.getContainer() is not None:
extruder_stack.material = material_node.getContainer() # type: InstanceContainer
def _applyChangesToMachine(self, global_stack, extruder_stack_dict): def _applyChangesToMachine(self, global_stack, extruder_stack_dict):
# Clear all first # Clear all first
@ -959,10 +983,12 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
# prepare the quality to select # prepare the quality to select
self._quality_changes_to_apply = None self._quality_changes_to_apply = None
self._quality_type_to_apply = None self._quality_type_to_apply = None
self._intent_category_to_apply = None
if self._machine_info.quality_changes_info is not None: if self._machine_info.quality_changes_info is not None:
self._quality_changes_to_apply = self._machine_info.quality_changes_info.name self._quality_changes_to_apply = self._machine_info.quality_changes_info.name
else: else:
self._quality_type_to_apply = self._machine_info.quality_type self._quality_type_to_apply = self._machine_info.quality_type
self._intent_category_to_apply = self._machine_info.intent_category
# Set enabled/disabled for extruders # Set enabled/disabled for extruders
for position, extruder_stack in extruder_stack_dict.items(): for position, extruder_stack in extruder_stack_dict.items():
@ -973,34 +999,38 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
extruder_stack.setMetaDataEntry("enabled", "True") extruder_stack.setMetaDataEntry("enabled", "True")
extruder_stack.setMetaDataEntry("enabled", str(extruder_info.enabled)) extruder_stack.setMetaDataEntry("enabled", str(extruder_info.enabled))
# Set metadata fields that are missing from the global stack
for key, value in self._machine_info.metadata_dict.items():
if key not in global_stack.getMetaData():
global_stack.setMetaDataEntry(key, value)
def _updateActiveMachine(self, global_stack): def _updateActiveMachine(self, global_stack):
# Actually change the active machine. # Actually change the active machine.
machine_manager = Application.getInstance().getMachineManager() machine_manager = Application.getInstance().getMachineManager()
material_manager = Application.getInstance().getMaterialManager() container_tree = ContainerTree.getInstance()
quality_manager = Application.getInstance().getQualityManager()
# Force update the lookup maps first
material_manager.initialize()
quality_manager.initialize()
machine_manager.setActiveMachine(global_stack.getId()) machine_manager.setActiveMachine(global_stack.getId())
# Set metadata fields that are missing from the global stack
for key, value in self._machine_info.metadata_dict.items():
if key not in global_stack.getMetaData():
global_stack.setMetaDataEntry(key, value)
if self._quality_changes_to_apply: if self._quality_changes_to_apply:
quality_changes_group_dict = quality_manager.getQualityChangesGroups(global_stack) quality_changes_group_list = container_tree.getCurrentQualityChangesGroups()
if self._quality_changes_to_apply not in quality_changes_group_dict: quality_changes_group = next((qcg for qcg in quality_changes_group_list if qcg.name == self._quality_changes_to_apply), None)
if not quality_changes_group:
Logger.log("e", "Could not find quality_changes [%s]", self._quality_changes_to_apply) Logger.log("e", "Could not find quality_changes [%s]", self._quality_changes_to_apply)
return return
quality_changes_group = quality_changes_group_dict[self._quality_changes_to_apply]
machine_manager.setQualityChangesGroup(quality_changes_group, no_dialog = True) machine_manager.setQualityChangesGroup(quality_changes_group, no_dialog = True)
else: else:
self._quality_type_to_apply = self._quality_type_to_apply.lower() self._quality_type_to_apply = self._quality_type_to_apply.lower()
quality_group_dict = quality_manager.getQualityGroups(global_stack) quality_group_dict = container_tree.getCurrentQualityGroups()
if self._quality_type_to_apply in quality_group_dict: if self._quality_type_to_apply in quality_group_dict:
quality_group = quality_group_dict[self._quality_type_to_apply] quality_group = quality_group_dict[self._quality_type_to_apply]
else: else:
Logger.log("i", "Could not find quality type [%s], switch to default", self._quality_type_to_apply) Logger.log("i", "Could not find quality type [%s], switch to default", self._quality_type_to_apply)
preferred_quality_type = global_stack.getMetaDataEntry("preferred_quality_type") preferred_quality_type = global_stack.getMetaDataEntry("preferred_quality_type")
quality_group_dict = quality_manager.getQualityGroups(global_stack)
quality_group = quality_group_dict.get(preferred_quality_type) quality_group = quality_group_dict.get(preferred_quality_type)
if quality_group is None: if quality_group is None:
Logger.log("e", "Could not get preferred quality type [%s]", preferred_quality_type) Logger.log("e", "Could not get preferred quality type [%s]", preferred_quality_type)
@ -1008,10 +1038,16 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
if quality_group is not None: if quality_group is not None:
machine_manager.setQualityGroup(quality_group, no_dialog = True) machine_manager.setQualityGroup(quality_group, no_dialog = True)
# Also apply intent if available
available_intent_category_list = IntentManager.getInstance().currentAvailableIntentCategories()
if self._intent_category_to_apply is not None and self._intent_category_to_apply in available_intent_category_list:
machine_manager.setIntentByCategory(self._intent_category_to_apply)
# Notify everything/one that is to notify about changes. # Notify everything/one that is to notify about changes.
global_stack.containersChanged.emit(global_stack.getTop()) global_stack.containersChanged.emit(global_stack.getTop())
def _stripFileToId(self, file): @staticmethod
def _stripFileToId(file):
mime_type = MimeTypeDatabase.getMimeTypeForFile(file) mime_type = MimeTypeDatabase.getMimeTypeForFile(file)
file = mime_type.stripExtension(file) file = mime_type.stripExtension(file)
return file.replace("Cura/", "") return file.replace("Cura/", "")
@ -1020,7 +1056,8 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
return self._container_registry.getContainerForMimeType(MimeTypeDatabase.getMimeType("application/x-ultimaker-material-profile")) return self._container_registry.getContainerForMimeType(MimeTypeDatabase.getMimeType("application/x-ultimaker-material-profile"))
## Get the list of ID's of all containers in a container stack by partially parsing it's serialized data. ## Get the list of ID's of all containers in a container stack by partially parsing it's serialized data.
def _getContainerIdListFromSerialized(self, serialized): @staticmethod
def _getContainerIdListFromSerialized(serialized):
parser = ConfigParser(interpolation = None, empty_lines_in_values = False) parser = ConfigParser(interpolation = None, empty_lines_in_values = False)
parser.read_string(serialized) parser.read_string(serialized)
@ -1041,12 +1078,20 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
return container_ids return container_ids
def _getMachineNameFromSerializedStack(self, serialized): @staticmethod
def _getMachineNameFromSerializedStack(serialized):
parser = ConfigParser(interpolation = None, empty_lines_in_values = False) parser = ConfigParser(interpolation = None, empty_lines_in_values = False)
parser.read_string(serialized) parser.read_string(serialized)
return parser["general"].get("name", "") return parser["general"].get("name", "")
def _getMaterialLabelFromSerialized(self, serialized): @staticmethod
def _getMetaDataDictFromSerializedStack(serialized: str) -> Dict[str, str]:
parser = ConfigParser(interpolation = None, empty_lines_in_values = False)
parser.read_string(serialized)
return dict(parser["metadata"])
@staticmethod
def _getMaterialLabelFromSerialized(serialized):
data = ET.fromstring(serialized) data = ET.fromstring(serialized)
metadata = data.iterfind("./um:metadata/um:name/um:label", {"um": "http://www.ultimaker.com/material"}) metadata = data.iterfind("./um:metadata/um:name/um:label", {"um": "http://www.ultimaker.com/material"})
for entry in metadata: for entry in metadata:

View File

@ -43,6 +43,7 @@ class WorkspaceDialog(QObject):
self._quality_name = "" self._quality_name = ""
self._num_settings_overridden_by_quality_changes = 0 self._num_settings_overridden_by_quality_changes = 0
self._quality_type = "" self._quality_type = ""
self._intent_name = ""
self._machine_name = "" self._machine_name = ""
self._machine_type = "" self._machine_type = ""
self._variant_type = "" self._variant_type = ""
@ -60,6 +61,7 @@ class WorkspaceDialog(QObject):
hasVisibleSettingsFieldChanged = pyqtSignal() hasVisibleSettingsFieldChanged = pyqtSignal()
numSettingsOverridenByQualityChangesChanged = pyqtSignal() numSettingsOverridenByQualityChangesChanged = pyqtSignal()
qualityTypeChanged = pyqtSignal() qualityTypeChanged = pyqtSignal()
intentNameChanged = pyqtSignal()
machineNameChanged = pyqtSignal() machineNameChanged = pyqtSignal()
materialLabelsChanged = pyqtSignal() materialLabelsChanged = pyqtSignal()
objectsOnPlateChanged = pyqtSignal() objectsOnPlateChanged = pyqtSignal()
@ -166,6 +168,15 @@ class WorkspaceDialog(QObject):
self._quality_name = quality_name self._quality_name = quality_name
self.qualityNameChanged.emit() self.qualityNameChanged.emit()
@pyqtProperty(str, notify = intentNameChanged)
def intentName(self) -> str:
return self._intent_name
def setIntentName(self, intent_name: str) -> None:
if self._intent_name != intent_name:
self._intent_name = intent_name
self.intentNameChanged.emit()
@pyqtProperty(str, notify=activeModeChanged) @pyqtProperty(str, notify=activeModeChanged)
def activeMode(self): def activeMode(self):
return self._active_mode return self._active_mode

View File

@ -1,10 +1,10 @@
// Copyright (c) 2016 Ultimaker B.V. // Copyright (c) 2016 Ultimaker B.V.
// Cura is released under the terms of the LGPLv3 or higher. // Cura is released under the terms of the LGPLv3 or higher.
import QtQuick 2.1 import QtQuick 2.10
import QtQuick.Controls 1.1 import QtQuick.Controls 1.4
import QtQuick.Layouts 1.1 import QtQuick.Layouts 1.3
import QtQuick.Window 2.1 import QtQuick.Window 2.2
import UM 1.1 as UM import UM 1.1 as UM
@ -13,8 +13,8 @@ UM.Dialog
id: base id: base
title: catalog.i18nc("@title:window", "Open Project") title: catalog.i18nc("@title:window", "Open Project")
minimumWidth: 500 * screenScaleFactor minimumWidth: UM.Theme.getSize("popup_dialog").width
minimumHeight: 450 * screenScaleFactor minimumHeight: UM.Theme.getSize("popup_dialog").height
width: minimumWidth width: minimumWidth
height: minimumHeight height: minimumHeight
@ -24,7 +24,7 @@ UM.Dialog
onClosing: manager.notifyClosed() onClosing: manager.notifyClosed()
onVisibleChanged: onVisibleChanged:
{ {
if(visible) if (visible)
{ {
machineResolveComboBox.currentIndex = 0 machineResolveComboBox.currentIndex = 0
qualityChangesResolveComboBox.currentIndex = 0 qualityChangesResolveComboBox.currentIndex = 0
@ -55,8 +55,8 @@ UM.Dialog
// See http://stackoverflow.com/questions/7659442/listelement-fields-as-properties // See http://stackoverflow.com/questions/7659442/listelement-fields-as-properties
Component.onCompleted: Component.onCompleted:
{ {
append({"key": "override", "label": catalog.i18nc("@action:ComboBox option", "Update existing")}); append({"key": "override", "label": catalog.i18nc("@action:ComboBox Update/override existing profile", "Update existing")});
append({"key": "new", "label": catalog.i18nc("@action:ComboBox option", "Create new")}); append({"key": "new", "label": catalog.i18nc("@action:ComboBox Save settings in a new profile", "Create new")});
} }
} }
@ -223,6 +223,21 @@ UM.Dialog
} }
} }
Row Row
{
width: parent.width
height: childrenRect.height
Label
{
text: catalog.i18nc("@action:label", "Intent")
width: (parent.width / 3) | 0
}
Label
{
text: manager.intentName
width: (parent.width / 3) | 0
}
}
Row
{ {
width: parent.width width: parent.width
height: manager.numUserSettings != 0 ? childrenRect.height : 0 height: manager.numUserSettings != 0 ? childrenRect.height : 0

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