mirror of
https://github.com/Ultimaker/Cura.git
synced 2026-02-28 23:34:42 -07:00
Merge branch 'master' into default_union_off_for_surface_mode
This commit is contained in:
commit
820925c9aa
1772 changed files with 19338 additions and 14934 deletions
48
.github/ISSUE_TEMPLATE.md
vendored
48
.github/ISSUE_TEMPLATE.md
vendored
|
|
@ -1,33 +1,49 @@
|
|||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us fix issues.
|
||||
title: ''
|
||||
labels: 'Type: Bug'
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
<!--
|
||||
The following template is useful for filing new issues. Processing an issue will go much faster when this is filled out, and issues which do not use this template WILL BE REMOVED.
|
||||
Processing an issue will go much faster when this is filled out, and issues which do not use this template WILL BE REMOVED and no fix will be considered!
|
||||
|
||||
Before filing, PLEASE check if the issue already exists (either open or closed) by using the search bar on the issues page. If it does, comment there. Even if it's closed, we can reopen it based on your comment.
|
||||
|
||||
Also, please note the application version in the title of the issue. For example: "[3.2.1] Cannot connect to 3rd-party printer". Please do not write things like "Request:" or "[BUG]" in the title; this is what labels are for.
|
||||
|
||||
It is also helpful to attach a project (.3mf or .curaproject) file and Cura log file so we can debug issues quicker.
|
||||
Information about how to find the log file can be found at https://github.com/Ultimaker/Cura/wiki/Cura-Preferences-and-Settings-Locations. To upload a project, try changing the extension to e.g. .curaproject.3mf.zip so that github accepts uploading the file. Otherwise we recommend http://wetransfer.com, but other file hosts like Google Drive or Dropbox work well too.
|
||||
Also, please note the application version in the title of the issue. For example: "[3.2.1] Cannot connect to 3rd-party printer". Please do NOT write things like "Request:" or "[BUG]" in the title; this is what labels are for.
|
||||
|
||||
Thank you for using Cura!
|
||||
-->
|
||||
|
||||
**Application Version**
|
||||
<!-- The version of the application this issue occurs with -->
|
||||
**Application version**
|
||||
(The version of the application this issue occurs with.)
|
||||
|
||||
**Platform**
|
||||
<!-- Information about the operating system the issue occurs on -->
|
||||
(Information about the operating system the issue occurs on. Include at least the operating system and maybe GPU.)
|
||||
|
||||
**Printer**
|
||||
<!-- Which printer was selected in Cura. Please attach project file as .curaproject.3mf.zip -->
|
||||
(Which printer was selected in Cura?)
|
||||
|
||||
**Steps to Reproduce**
|
||||
<!-- Add the steps needed that lead up to the issue -->
|
||||
**Reproduction steps**
|
||||
1. (Something you did.)
|
||||
2. (Something you did next.)
|
||||
|
||||
**Actual Results**
|
||||
<!-- What happens after the above steps have been followed -->
|
||||
**Screenshot(s)**
|
||||
(Image showing the problem, perhaps before/after images.)
|
||||
|
||||
**Actual results**
|
||||
(What happens after the above steps have been followed.)
|
||||
|
||||
**Expected results**
|
||||
<!-- What should happen after the above steps have been followed -->
|
||||
(What should happen after the above steps have been followed.)
|
||||
|
||||
**Additional Information**
|
||||
<!-- Extra information relevant to the issue, like screenshots -->
|
||||
**Project file**
|
||||
(For slicing bugs, provide a project which clearly shows the bug, by going to File->Save. For big files you may need to use WeTransfer or similar file sharing sites.)
|
||||
|
||||
**Log file**
|
||||
(See https://github.com/Ultimaker/Cura#logging-issues to find the log file to upload, or copy a relevant snippet from it.)
|
||||
|
||||
**Additional information**
|
||||
(Extra information relevant to the issue.)
|
||||
|
|
|
|||
28
.github/ISSUE_TEMPLATE/bug-report.md
vendored
28
.github/ISSUE_TEMPLATE/bug-report.md
vendored
|
|
@ -14,30 +14,36 @@ Before filing, PLEASE check if the issue already exists (either open or closed)
|
|||
|
||||
Also, please note the application version in the title of the issue. For example: "[3.2.1] Cannot connect to 3rd-party printer". Please do NOT write things like "Request:" or "[BUG]" in the title; this is what labels are for.
|
||||
|
||||
It is also helpful to attach a project (.3mf or .curaproject) file and Cura log file so we can debug issues quicker. Information about how to find the log file can be found at https://github.com/Ultimaker/Cura#logging-issues
|
||||
|
||||
To upload a project, try changing the extension to e.g. .curaproject.3mf.zip so that GitHub accepts uploading the file. Otherwise, we recommend http://wetransfer.com, but other file hosts like Google Drive or Dropbox work well too.
|
||||
|
||||
Thank you for using Cura!
|
||||
-->
|
||||
|
||||
**Application version**
|
||||
<!-- The version of the application this issue occurs with -->
|
||||
(The version of the application this issue occurs with.)
|
||||
|
||||
**Platform**
|
||||
<!-- Information about the operating system the issue occurs on. Include at least the operating system. In the case of visual glitches/issues, also include information about your graphics drivers and GPU. -->
|
||||
(Information about the operating system the issue occurs on. Include at least the operating system and maybe GPU.)
|
||||
|
||||
**Printer**
|
||||
<!-- Which printer was selected in Cura? If possible, please attach project file as .curaproject.3mf.zip -->
|
||||
(Which printer was selected in Cura?)
|
||||
|
||||
**Reproduction steps**
|
||||
<!-- How did you encounter the bug? -->
|
||||
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 -->
|
||||
(What happens after the above steps have been followed.)
|
||||
|
||||
**Expected results**
|
||||
<!-- What should happen after the above steps have been followed -->
|
||||
(What should happen after the above steps have been followed.)
|
||||
|
||||
**Project file**
|
||||
(For slicing bugs, provide a project which clearly shows the bug, by going to File->Save. For big files you may need to use WeTransfer or similar file sharing sites.)
|
||||
|
||||
**Log file**
|
||||
(See https://github.com/Ultimaker/Cura#logging-issues to find the log file to upload, or copy a relevant snippet from it.)
|
||||
|
||||
**Additional information**
|
||||
<!-- Extra information relevant to the issue, like screenshots. Don't forget to attach the log files with this issue report. -->
|
||||
(Extra information relevant to the issue.)
|
||||
|
|
|
|||
11
.github/ISSUE_TEMPLATE/feature_request.md
vendored
11
.github/ISSUE_TEMPLATE/feature_request.md
vendored
|
|
@ -8,15 +8,16 @@ assignees: ''
|
|||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
<!-- A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] -->
|
||||
(A clear and concise description of what the problem is. Ex. I'm always frustrated when [...])
|
||||
|
||||
**Describe the solution you'd like**
|
||||
<!--A clear and concise description of what you want to happen. If possible, describe why you think this is a good solution.-->
|
||||
(A clear and concise description of what you want to happen. If possible, describe why you think this is a good solution.)
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
<!-- A clear and concise description of any alternative solutions or features you've considered. Again, if possible, think about why these alternatives are not working out. -->
|
||||
(A clear and concise description of any alternative solutions or features you've considered. Again, if possible, think about why these alternatives are not working out.)
|
||||
|
||||
**Affected users and/or printers**
|
||||
<!-- Who do you think will benefit from this? Is everyone going to benefit from these changes? Only a few people? -->
|
||||
(Who do you think will benefit from this? Is everyone going to benefit from these changes? Or specific kinds of users?)
|
||||
|
||||
**Additional context**
|
||||
<!-- Add any other context or screenshots about the feature request here. -->
|
||||
(Add any other context or screenshots about the feature request here.)
|
||||
|
|
|
|||
13
.github/workflows/cicd.yml
vendored
Normal file
13
.github/workflows/cicd.yml
vendored
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
---
|
||||
name: CI/CD
|
||||
on: [push, pull_request]
|
||||
jobs:
|
||||
build:
|
||||
name: Build and test
|
||||
runs-on: ubuntu-latest
|
||||
container: ultimaker/cura-build-environment
|
||||
steps:
|
||||
- name: Checkout master
|
||||
uses: actions/checkout@v1.2.0
|
||||
- name: Build and test
|
||||
run: docker/build.sh
|
||||
|
|
@ -49,7 +49,7 @@ endif()
|
|||
|
||||
|
||||
if(NOT ${URANIUM_DIR} STREQUAL "")
|
||||
set(CMAKE_MODULE_PATH "${URANIUM_DIR}/cmake")
|
||||
set(CMAKE_MODULE_PATH "${CMAKE_MODULE_PATH};${URANIUM_DIR}/cmake")
|
||||
endif()
|
||||
if(NOT ${URANIUM_SCRIPTS_DIR} STREQUAL "")
|
||||
list(APPEND CMAKE_MODULE_PATH ${URANIUM_DIR}/cmake)
|
||||
|
|
@ -63,8 +63,8 @@ endif()
|
|||
|
||||
install(DIRECTORY resources
|
||||
DESTINATION ${CMAKE_INSTALL_DATADIR}/cura)
|
||||
install(DIRECTORY plugins
|
||||
DESTINATION lib${LIB_SUFFIX}/cura)
|
||||
|
||||
include(CuraPluginInstall)
|
||||
|
||||
if(NOT APPLE AND NOT WIN32)
|
||||
install(FILES cura_app.py
|
||||
|
|
|
|||
99
cmake/CuraPluginInstall.cmake
Normal file
99
cmake/CuraPluginInstall.cmake
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
# Copyright (c) 2019 Ultimaker B.V.
|
||||
# CuraPluginInstall.cmake is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
#
|
||||
# This module detects all plugins that need to be installed and adds them using the CMake install() command.
|
||||
# It detects all plugin folder in the path "plugins/*" where there's a "plugin.json" in it.
|
||||
#
|
||||
# Plugins can be configured to NOT BE INSTALLED via the variable "CURA_NO_INSTALL_PLUGINS" as a list of string in the
|
||||
# form of "a;b;c" or "a,b,c". By default all plugins will be installed.
|
||||
#
|
||||
|
||||
# FIXME: Remove the code for CMake <3.12 once we have switched over completely.
|
||||
# FindPython3 is a new module since CMake 3.12. It deprecates FindPythonInterp and FindPythonLibs. The FindPython3
|
||||
# module is copied from the CMake repository here so in CMake <3.12 we can still use it.
|
||||
if(${CMAKE_VERSION} VERSION_LESS 3.12)
|
||||
# Use FindPythonInterp and FindPythonLibs for CMake <3.12
|
||||
find_package(PythonInterp 3 REQUIRED)
|
||||
|
||||
set(Python3_EXECUTABLE ${PYTHON_EXECUTABLE})
|
||||
else()
|
||||
# Use FindPython3 for CMake >=3.12
|
||||
find_package(Python3 REQUIRED COMPONENTS Interpreter)
|
||||
endif()
|
||||
|
||||
# Options or configuration variables
|
||||
set(CURA_NO_INSTALL_PLUGINS "" CACHE STRING "A list of plugins that should not be installed, separated with ';' or ','.")
|
||||
|
||||
file(GLOB_RECURSE _plugin_json_list ${CMAKE_SOURCE_DIR}/plugins/*/plugin.json)
|
||||
list(LENGTH _plugin_json_list _plugin_json_list_len)
|
||||
|
||||
# Sort the lists alphabetically so we can handle cases like this:
|
||||
# - plugins/my_plugin/plugin.json
|
||||
# - plugins/my_plugin/my_module/plugin.json
|
||||
# In this case, only "plugins/my_plugin" should be added via install().
|
||||
set(_no_install_plugin_list ${CURA_NO_INSTALL_PLUGINS})
|
||||
# Sanitize the string so the comparison will be case-insensitive.
|
||||
string(STRIP "${_no_install_plugin_list}" _no_install_plugin_list)
|
||||
string(TOLOWER "${_no_install_plugin_list}" _no_install_plugin_list)
|
||||
|
||||
# WORKAROUND counterpart of what's in cura-build.
|
||||
string(REPLACE "," ";" _no_install_plugin_list "${_no_install_plugin_list}")
|
||||
|
||||
list(LENGTH _no_install_plugin_list _no_install_plugin_list_len)
|
||||
|
||||
if(_no_install_plugin_list_len GREATER 0)
|
||||
list(SORT _no_install_plugin_list)
|
||||
endif()
|
||||
if(_plugin_json_list_len GREATER 0)
|
||||
list(SORT _plugin_json_list)
|
||||
endif()
|
||||
|
||||
# Check all plugin directories and add them via install() if needed.
|
||||
set(_install_plugin_list "")
|
||||
foreach(_plugin_json_path ${_plugin_json_list})
|
||||
get_filename_component(_plugin_dir ${_plugin_json_path} DIRECTORY)
|
||||
file(RELATIVE_PATH _rel_plugin_dir ${CMAKE_CURRENT_SOURCE_DIR} ${_plugin_dir})
|
||||
get_filename_component(_plugin_dir_name ${_plugin_dir} NAME)
|
||||
|
||||
# Make plugin name comparison case-insensitive
|
||||
string(TOLOWER "${_plugin_dir_name}" _plugin_dir_name_lowercase)
|
||||
|
||||
# Check if this plugin needs to be skipped for installation
|
||||
set(_add_plugin ON) # Indicates if this plugin should be added to the build or not.
|
||||
set(_is_no_install_plugin OFF) # If this plugin will not be added, this indicates if it's because the plugin is
|
||||
# specified in the NO_INSTALL_PLUGINS list.
|
||||
if(_no_install_plugin_list)
|
||||
if("${_plugin_dir_name_lowercase}" IN_LIST _no_install_plugin_list)
|
||||
set(_add_plugin OFF)
|
||||
set(_is_no_install_plugin ON)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
# Make sure this is not a subdirectory in a plugin that's already in the install list
|
||||
if(_add_plugin)
|
||||
foreach(_known_install_plugin_dir ${_install_plugin_list})
|
||||
if(_plugin_dir MATCHES "${_known_install_plugin_dir}.+")
|
||||
set(_add_plugin OFF)
|
||||
break()
|
||||
endif()
|
||||
endforeach()
|
||||
endif()
|
||||
|
||||
if(_add_plugin)
|
||||
message(STATUS "[+] PLUGIN TO INSTALL: ${_rel_plugin_dir}")
|
||||
get_filename_component(_rel_plugin_parent_dir ${_rel_plugin_dir} DIRECTORY)
|
||||
install(DIRECTORY ${_rel_plugin_dir}
|
||||
DESTINATION lib${LIB_SUFFIX}/cura/${_rel_plugin_parent_dir}
|
||||
PATTERN "__pycache__" EXCLUDE
|
||||
PATTERN "*.qmlc" EXCLUDE
|
||||
)
|
||||
list(APPEND _install_plugin_list ${_plugin_dir})
|
||||
elseif(_is_no_install_plugin)
|
||||
message(STATUS "[-] PLUGIN TO REMOVE : ${_rel_plugin_dir}")
|
||||
execute_process(COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/cmake/mod_bundled_packages_json.py
|
||||
-d ${CMAKE_CURRENT_SOURCE_DIR}/resources/bundled_packages
|
||||
${_plugin_dir_name}
|
||||
RESULT_VARIABLE _mod_json_result)
|
||||
endif()
|
||||
endforeach()
|
||||
69
cmake/mod_bundled_packages_json.py
Executable file
69
cmake/mod_bundled_packages_json.py
Executable file
|
|
@ -0,0 +1,69 @@
|
|||
#!/usr/bin/env python3
|
||||
#
|
||||
# This script removes the given package entries in the bundled_packages JSON files. This is used by the PluginInstall
|
||||
# CMake module.
|
||||
#
|
||||
|
||||
import argparse
|
||||
import collections
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
|
||||
|
||||
## Finds all JSON files in the given directory recursively and returns a list of those files in absolute paths.
|
||||
#
|
||||
# \param work_dir The directory to look for JSON files recursively.
|
||||
# \return A list of JSON files in absolute paths that are found in the given directory.
|
||||
def find_json_files(work_dir: str) -> list:
|
||||
json_file_list = []
|
||||
for root, dir_names, file_names in os.walk(work_dir):
|
||||
for file_name in file_names:
|
||||
abs_path = os.path.abspath(os.path.join(root, file_name))
|
||||
json_file_list.append(abs_path)
|
||||
return json_file_list
|
||||
|
||||
|
||||
## Removes the given entries from the given JSON file. The file will modified in-place.
|
||||
#
|
||||
# \param file_path The JSON file to modify.
|
||||
# \param entries A list of strings as entries to remove.
|
||||
# \return None
|
||||
def remove_entries_from_json_file(file_path: str, entries: list) -> None:
|
||||
try:
|
||||
with open(file_path, "r", encoding = "utf-8") as f:
|
||||
package_dict = json.load(f, object_hook = collections.OrderedDict)
|
||||
except Exception as e:
|
||||
msg = "Failed to load '{file_path}' as a JSON file. This file will be ignored Exception: {e}"\
|
||||
.format(file_path = file_path, e = e)
|
||||
sys.stderr.write(msg + os.linesep)
|
||||
return
|
||||
|
||||
for entry in entries:
|
||||
if entry in package_dict:
|
||||
del package_dict[entry]
|
||||
print("[INFO] Remove entry [{entry}] from [{file_path}]".format(file_path = file_path, entry = entry))
|
||||
|
||||
try:
|
||||
with open(file_path, "w", encoding = "utf-8", newline = "\n") as f:
|
||||
json.dump(package_dict, f, indent = 4)
|
||||
except Exception as e:
|
||||
msg = "Failed to write '{file_path}' as a JSON file. Exception: {e}".format(file_path = file_path, e = e)
|
||||
raise IOError(msg)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser("mod_bundled_packages_json")
|
||||
parser.add_argument("-d", "--dir", dest = "work_dir",
|
||||
help = "The directory to look for bundled packages JSON files, recursively.")
|
||||
parser.add_argument("entries", metavar = "ENTRIES", type = str, nargs = "+")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
json_file_list = find_json_files(args.work_dir)
|
||||
for json_file_path in json_file_list:
|
||||
remove_entries_from_json_file(json_file_path, args.entries)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
@ -9,7 +9,11 @@ DEFAULT_CURA_DISPLAY_NAME = "Ultimaker Cura"
|
|||
DEFAULT_CURA_VERSION = "master"
|
||||
DEFAULT_CURA_BUILD_TYPE = ""
|
||||
DEFAULT_CURA_DEBUG_MODE = False
|
||||
DEFAULT_CURA_SDK_VERSION = "6.3.0"
|
||||
|
||||
# Each release has a fixed SDK version coupled with it. It doesn't make sense to make it configurable because, for
|
||||
# example Cura 3.2 with SDK version 6.1 will not work. So the SDK version is hard-coded here and left out of the
|
||||
# CuraVersion.py.in template.
|
||||
CuraSDKVersion = "7.0.0"
|
||||
|
||||
try:
|
||||
from cura.CuraVersion import CuraAppName # type: ignore
|
||||
|
|
@ -32,6 +36,9 @@ try:
|
|||
except ImportError:
|
||||
CuraVersion = DEFAULT_CURA_VERSION # [CodeStyle: Reflecting imported value]
|
||||
|
||||
# CURA-6569
|
||||
# This string indicates what type of version it is. For example, "enterprise". By default it's empty which indicates
|
||||
# a default/normal Cura build.
|
||||
try:
|
||||
from cura.CuraVersion import CuraBuildType # type: ignore
|
||||
except ImportError:
|
||||
|
|
@ -42,7 +49,7 @@ try:
|
|||
except ImportError:
|
||||
CuraDebugMode = DEFAULT_CURA_DEBUG_MODE
|
||||
|
||||
# Each release has a fixed SDK version coupled with it. It doesn't make sense to make it configurable because, for
|
||||
# example Cura 3.2 with SDK version 6.1 will not work. So the SDK version is hard-coded here and left out of the
|
||||
# CuraVersion.py.in template.
|
||||
CuraSDKVersion = "6.3.0"
|
||||
# CURA-6569
|
||||
# Various convenience flags indicating what kind of Cura build it is.
|
||||
__ENTERPRISE_VERSION_TYPE = "enterprise"
|
||||
IsEnterpriseVersion = CuraBuildType.lower() == __ENTERPRISE_VERSION_TYPE
|
||||
|
|
|
|||
|
|
@ -26,7 +26,6 @@ catalog = i18nCatalog("cura")
|
|||
|
||||
import numpy
|
||||
import math
|
||||
import copy
|
||||
|
||||
from typing import List, Optional, TYPE_CHECKING, Any, Set, cast, Iterable, Dict
|
||||
|
||||
|
|
@ -126,10 +125,6 @@ class BuildVolume(SceneNode):
|
|||
# Therefore this works.
|
||||
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
|
||||
self._machine_manager.extruderChanged.connect(self.updateNodeBoundaryCheck)
|
||||
|
||||
|
|
@ -1107,7 +1102,7 @@ class BuildVolume(SceneNode):
|
|||
|
||||
# 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.
|
||||
return 0.1
|
||||
|
||||
bed_adhesion_size = self._calculateBedAdhesionSize(used_extruders)
|
||||
support_expansion = self._calculateSupportExpansion(self._global_container_stack)
|
||||
|
|
|
|||
|
|
@ -25,6 +25,8 @@ from UM.View.GL.OpenGL import OpenGL
|
|||
from UM.i18n import i18nCatalog
|
||||
from UM.Resources import Resources
|
||||
|
||||
from cura import ApplicationMetadata
|
||||
|
||||
catalog = i18nCatalog("cura")
|
||||
|
||||
MYPY = False
|
||||
|
|
@ -181,6 +183,7 @@ class CrashHandler:
|
|||
self.cura_version = catalog.i18nc("@label unknown version of Cura", "Unknown")
|
||||
|
||||
crash_info = "<b>" + catalog.i18nc("@label Cura version number", "Cura version") + ":</b> " + str(self.cura_version) + "<br/>"
|
||||
crash_info += "<b>" + catalog.i18nc("@label Cura build type", "Cura build type") + ":</b> " + str(ApplicationMetadata.CuraBuildType) + "<br/>"
|
||||
crash_info += "<b>" + catalog.i18nc("@label Type of platform", "Platform") + ":</b> " + str(platform.platform()) + "<br/>"
|
||||
crash_info += "<b>" + catalog.i18nc("@label", "Qt version") + ":</b> " + str(QT_VERSION_STR) + "<br/>"
|
||||
crash_info += "<b>" + catalog.i18nc("@label", "PyQt version") + ":</b> " + str(PYQT_VERSION_STR) + "<br/>"
|
||||
|
|
@ -191,6 +194,7 @@ class CrashHandler:
|
|||
group.setLayout(layout)
|
||||
|
||||
self.data["cura_version"] = self.cura_version
|
||||
self.data["cura_build_type"] = ApplicationMetadata.CuraBuildType
|
||||
self.data["os"] = {"type": platform.system(), "version": platform.version()}
|
||||
self.data["qt_version"] = QT_VERSION_STR
|
||||
self.data["pyqt_version"] = PYQT_VERSION_STR
|
||||
|
|
|
|||
|
|
@ -73,9 +73,6 @@ from cura.Scene import ZOffsetDecorator
|
|||
|
||||
from cura.Machines.ContainerTree import ContainerTree
|
||||
from cura.Machines.MachineErrorChecker import MachineErrorChecker
|
||||
import cura.Machines.MaterialManager #Imported like this to prevent circular imports.
|
||||
import cura.Machines.QualityManager #Imported like this to prevent circular imports.
|
||||
from cura.Machines.VariantManager import VariantManager
|
||||
|
||||
from cura.Machines.Models.BuildPlateModel import BuildPlateModel
|
||||
from cura.Machines.Models.CustomQualityProfilesDropDownMenuModel import CustomQualityProfilesDropDownMenuModel
|
||||
|
|
@ -148,7 +145,7 @@ class CuraApplication(QtApplication):
|
|||
# SettingVersion represents the set of settings available in the machine/extruder definitions.
|
||||
# You need to make sure that this version number needs to be increased if there is any non-backwards-compatible
|
||||
# changes of the settings.
|
||||
SettingVersion = 10
|
||||
SettingVersion = 11
|
||||
|
||||
Created = False
|
||||
|
||||
|
|
@ -227,7 +224,7 @@ class CuraApplication(QtApplication):
|
|||
self._quality_management_model = None
|
||||
|
||||
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._add_printer_pages_model = AddPrinterPagesModel(self, parent = self)
|
||||
self._whats_new_pages_model = WhatsNewPagesModel(self, parent = self)
|
||||
|
|
@ -520,7 +517,8 @@ class CuraApplication(QtApplication):
|
|||
with self._container_registry.lockFile():
|
||||
self._container_registry.loadAllMetadata()
|
||||
|
||||
# set the setting version for Preferences
|
||||
self.showSplashMessage(self._i18n_catalog.i18nc("@info:progress", "Setting up preferences..."))
|
||||
# Set the setting version for Preferences
|
||||
preferences = self.getPreferences()
|
||||
preferences.addPreference("metadata/setting_version", 0)
|
||||
preferences.setValue("metadata/setting_version", self.SettingVersion) #Don't make it equal to the default so that the setting version always gets written to the file.
|
||||
|
|
@ -722,6 +720,8 @@ class CuraApplication(QtApplication):
|
|||
## Handle loading of all plugin types (and the backend explicitly)
|
||||
# \sa PluginRegistry
|
||||
def _loadPlugins(self) -> None:
|
||||
self._plugin_registry.setCheckIfTrusted(ApplicationMetadata.IsEnterpriseVersion)
|
||||
|
||||
self._plugin_registry.addType("profile_reader", self._addProfileReader)
|
||||
self._plugin_registry.addType("profile_writer", self._addProfileWriter)
|
||||
|
||||
|
|
@ -880,6 +880,10 @@ class CuraApplication(QtApplication):
|
|||
|
||||
@pyqtSlot(result = QObject)
|
||||
def getFirstStartMachineActionsModel(self, *args) -> "FirstStartMachineActionsModel":
|
||||
if self._first_start_machine_actions_model is None:
|
||||
self._first_start_machine_actions_model = FirstStartMachineActionsModel(self, parent = self)
|
||||
if self.started:
|
||||
self._first_start_machine_actions_model.initialize()
|
||||
return self._first_start_machine_actions_model
|
||||
|
||||
@pyqtSlot(result = QObject)
|
||||
|
|
@ -924,20 +928,6 @@ class CuraApplication(QtApplication):
|
|||
self._extruder_manager = ExtruderManager()
|
||||
return self._extruder_manager
|
||||
|
||||
@deprecated("Use the ContainerTree structure instead.", since = "4.3")
|
||||
def getVariantManager(self, *args) -> VariantManager:
|
||||
return VariantManager.getInstance()
|
||||
|
||||
# Can't deprecate this function since the deprecation marker collides with pyqtSlot!
|
||||
@pyqtSlot(result = QObject)
|
||||
def getMaterialManager(self, *args) -> cura.Machines.MaterialManager.MaterialManager:
|
||||
return cura.Machines.MaterialManager.MaterialManager.getInstance()
|
||||
|
||||
# Can't deprecate this function since the deprecation marker collides with pyqtSlot!
|
||||
@pyqtSlot(result = QObject)
|
||||
def getQualityManager(self, *args) -> cura.Machines.QualityManager.QualityManager:
|
||||
return cura.Machines.QualityManager.QualityManager.getInstance()
|
||||
|
||||
def getIntentManager(self, *args) -> IntentManager:
|
||||
return IntentManager.getInstance()
|
||||
|
||||
|
|
@ -1380,16 +1370,19 @@ class CuraApplication(QtApplication):
|
|||
|
||||
for node in nodes:
|
||||
mesh_data = node.getMeshData()
|
||||
if mesh_data and mesh_data.getFileName():
|
||||
job = ReadMeshJob(mesh_data.getFileName())
|
||||
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.")
|
||||
if mesh_data:
|
||||
file_name = mesh_data.getFileName()
|
||||
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")
|
||||
def setExpandedCategories(self, categories: List[str]) -> None:
|
||||
|
|
@ -1808,7 +1801,7 @@ class CuraApplication(QtApplication):
|
|||
try:
|
||||
result = workspace_reader.preRead(file_path, show_dialog=False)
|
||||
return result == WorkspaceReader.PreReadResult.accepted
|
||||
except Exception as e:
|
||||
except Exception:
|
||||
Logger.logException("e", "Could not check file %s", file_url)
|
||||
return False
|
||||
|
||||
|
|
@ -1904,3 +1897,7 @@ class CuraApplication(QtApplication):
|
|||
op.push()
|
||||
from UM.Scene.Selection import Selection
|
||||
Selection.clear()
|
||||
|
||||
@classmethod
|
||||
def getInstance(cls, *args, **kwargs) -> "CuraApplication":
|
||||
return cast(CuraApplication, super().getInstance(**kwargs))
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
# Copyright (c) 2019 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
from UM.Application import Application
|
||||
from UM.Qt.QtApplication import QtApplication
|
||||
from typing import Any, Optional
|
||||
import numpy
|
||||
|
||||
|
|
@ -232,7 +232,7 @@ class LayerPolygon:
|
|||
@classmethod
|
||||
def getColorMap(cls):
|
||||
if cls.__color_map is None:
|
||||
theme = Application.getInstance().getTheme()
|
||||
theme = QtApplication.getInstance().getTheme()
|
||||
cls.__color_map = numpy.array([
|
||||
theme.getColor("layerview_none").getRgbF(), # NoneType
|
||||
theme.getColor("layerview_inset_0").getRgbF(), # Inset0Type
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ from UM.ConfigurationErrorMessage import ConfigurationErrorMessage
|
|||
from UM.Settings.ContainerRegistry import ContainerRegistry
|
||||
from UM.Logger import Logger
|
||||
from UM.Settings.InstanceContainer import InstanceContainer
|
||||
from UM.Decorators import deprecated
|
||||
|
||||
|
||||
## A node in the container tree. It represents one container.
|
||||
#
|
||||
|
|
@ -43,18 +43,6 @@ class ContainerNode:
|
|||
return default
|
||||
return container_metadata[0].get(entry, default)
|
||||
|
||||
## Get the child with the specified container ID.
|
||||
# \param child_id The container ID to get from among the children.
|
||||
# \return The child node, or ``None`` if no child is present with the
|
||||
# specified ID.
|
||||
@deprecated("Iterate over the children instead of requesting them one by one.", "4.3")
|
||||
def getChildNode(self, child_id: str) -> Optional["ContainerNode"]:
|
||||
return self.children_map.get(child_id)
|
||||
|
||||
@deprecated("Use `.container` instead.", "4.3")
|
||||
def getContainer(self) -> Optional[InstanceContainer]:
|
||||
return self.container
|
||||
|
||||
## The container that this node's container ID refers to.
|
||||
#
|
||||
# This can be used to finally instantiate the container in order to put it
|
||||
|
|
|
|||
|
|
@ -1,21 +1,22 @@
|
|||
# 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.Settings.Interfaces import ContainerInterface
|
||||
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, TYPE_CHECKING
|
||||
from typing import Dict, List, Optional, TYPE_CHECKING
|
||||
import time
|
||||
import UM.FlameProfiler
|
||||
|
||||
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
|
||||
|
|
@ -38,12 +39,9 @@ class ContainerTree:
|
|||
return cls.__instance
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.machines = {} # type: Dict[str, MachineNode] # Mapping from definition ID to machine nodes.
|
||||
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.
|
||||
|
||||
container_registry = ContainerRegistry.getInstance()
|
||||
container_registry.containerAdded.connect(self._machineAdded)
|
||||
self._loadAll()
|
||||
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.
|
||||
#
|
||||
|
|
@ -76,54 +74,85 @@ class ContainerTree:
|
|||
extruder_enabled = [extruder.isEnabled for extruder in global_stack.extruderList]
|
||||
return self.machines[global_stack.definition.getId()].getQualityChangesGroups(variant_names, material_bases, extruder_enabled)
|
||||
|
||||
# Add a machine node by the id of it's definition.
|
||||
# This is automatically called by the _machineAdded function, but it's sometimes needed to add a machine node
|
||||
# faster than would have been done when waiting on any signals (for instance; when creating an entirely new machine)
|
||||
@UM.FlameProfiler.profile
|
||||
def addMachineNodeByDefinitionId(self, definition_id: str) -> None:
|
||||
if definition_id in self.machines:
|
||||
return # Already have this definition ID.
|
||||
## 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))
|
||||
|
||||
start_time = time.time()
|
||||
self.machines[definition_id] = MachineNode(definition_id)
|
||||
self.machines[definition_id].materialsChanged.connect(self.materialsChanged)
|
||||
Logger.log("d", "Adding container tree for {definition_id} took {duration} seconds.".format(definition_id = definition_id, duration = time.time() - start_time))
|
||||
## 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]
|
||||
|
||||
## Builds the initial container tree.
|
||||
def _loadAll(self):
|
||||
Logger.log("i", "Building container tree.")
|
||||
start_time = time.time()
|
||||
all_stacks = ContainerRegistry.getInstance().findContainerStacks()
|
||||
for stack in all_stacks:
|
||||
if not isinstance(stack, GlobalStack):
|
||||
continue # Only want to load global stacks. We don't need to create a tree for extruder definitions.
|
||||
definition_id = stack.definition.getId()
|
||||
if definition_id not in self.machines:
|
||||
definition_start_time = time.time()
|
||||
self.machines[definition_id] = MachineNode(definition_id)
|
||||
self.machines[definition_id].materialsChanged.connect(self.materialsChanged)
|
||||
Logger.log("d", "Adding container tree for {definition_id} took {duration} seconds.".format(definition_id = definition_id, duration = time.time() - definition_start_time))
|
||||
## 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
|
||||
|
||||
Logger.log("d", "Building the container tree took %s seconds", time.time() - start_time)
|
||||
## 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]
|
||||
|
||||
## When a printer gets added, we need to build up the tree for that container.
|
||||
def _machineAdded(self, container_stack: ContainerInterface) -> None:
|
||||
if not isinstance(container_stack, GlobalStack):
|
||||
return # Not our concern.
|
||||
self.addMachineNodeByDefinitionId(container_stack.definition.getId())
|
||||
## 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]
|
||||
|
||||
## For debugging purposes, visualise the entire container tree as it stands
|
||||
# now.
|
||||
def _visualise_tree(self) -> str:
|
||||
lines = ["% CONTAINER TREE"] # Start with array and then combine into string, for performance.
|
||||
for machine in self.machines.values():
|
||||
lines.append(" # " + machine.container_id)
|
||||
for variant in machine.variants.values():
|
||||
lines.append(" * " + variant.container_id)
|
||||
for material in variant.materials.values():
|
||||
lines.append(" + " + material.container_id)
|
||||
for quality in material.qualities.values():
|
||||
lines.append(" - " + quality.container_id)
|
||||
for intent in quality.intents.values():
|
||||
lines.append(" . " + intent.container_id)
|
||||
return "\n".join(lines)
|
||||
## 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]
|
||||
|
|
|
|||
|
|
@ -140,7 +140,7 @@ class MachineNode(ContainerNode):
|
|||
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 "position" in quality_changes: # An extruder profile.
|
||||
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
|
||||
|
|
@ -177,5 +177,7 @@ class MachineNode(ContainerNode):
|
|||
global_qualities = container_registry.findInstanceContainersMetadata(type = "quality", definition = self.quality_definition, global_quality = "True") # First try specific to this printer.
|
||||
if len(global_qualities) == 0: # This printer doesn't override the global qualities.
|
||||
global_qualities = container_registry.findInstanceContainersMetadata(type = "quality", definition = "fdmprinter", global_quality = "True") # Otherwise pick the global global qualities.
|
||||
if len(global_qualities) == 0: # There are no global qualities either?! Something went very wrong, but we'll not crash and properly fill the tree.
|
||||
global_qualities = [cura.CuraApplication.CuraApplication.getInstance().empty_quality_container.getMetaData()]
|
||||
for global_quality in global_qualities:
|
||||
self.global_qualities[global_quality["quality_type"]] = QualityNode(global_quality["id"], parent = self)
|
||||
|
|
|
|||
|
|
@ -1,349 +0,0 @@
|
|||
# Copyright (c) 2019 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
from collections import defaultdict
|
||||
import copy
|
||||
import uuid
|
||||
from typing import Dict, Optional, TYPE_CHECKING, Any, List, cast
|
||||
|
||||
from PyQt5.Qt import QTimer, QObject, pyqtSignal, pyqtSlot
|
||||
|
||||
from UM.Decorators import deprecated
|
||||
from UM.Logger import Logger
|
||||
from UM.Settings.ContainerRegistry import ContainerRegistry
|
||||
from UM.Util import parseBool
|
||||
import cura.CuraApplication # Imported like this to prevent circular imports.
|
||||
from cura.Machines.ContainerTree import ContainerTree
|
||||
from cura.Settings.CuraContainerRegistry import CuraContainerRegistry
|
||||
|
||||
from .MaterialNode import MaterialNode
|
||||
from .MaterialGroup import MaterialGroup
|
||||
|
||||
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):
|
||||
__instance = None
|
||||
|
||||
@classmethod
|
||||
@deprecated("Use the ContainerTree structure instead.", since = "4.3")
|
||||
def getInstance(cls) -> "MaterialManager":
|
||||
if cls.__instance is None:
|
||||
cls.__instance = MaterialManager()
|
||||
return cls.__instance
|
||||
|
||||
materialsUpdated = pyqtSignal() # Emitted whenever the material lookup tables are updated.
|
||||
favoritesUpdated = pyqtSignal() # Emitted whenever the favorites are changed
|
||||
|
||||
def __init__(self, parent = None):
|
||||
super().__init__(parent)
|
||||
# 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]
|
||||
|
||||
# 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]]
|
||||
|
||||
self._favorites = set(cura.CuraApplication.CuraApplication.getInstance().getPreferences().getValue("cura/favorite_materials").split(";"))
|
||||
self.materialsUpdated.emit()
|
||||
|
||||
self._update_timer = QTimer(self)
|
||||
self._update_timer.setInterval(300)
|
||||
|
||||
self._update_timer.setSingleShot(True)
|
||||
self._update_timer.timeout.connect(self.materialsUpdated)
|
||||
|
||||
container_registry = ContainerRegistry.getInstance()
|
||||
container_registry.containerMetaDataChanged.connect(self._onContainerMetadataChanged)
|
||||
container_registry.containerAdded.connect(self._onContainerMetadataChanged)
|
||||
container_registry.containerRemoved.connect(self._onContainerMetadataChanged)
|
||||
|
||||
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:
|
||||
original_material = CuraContainerRegistry.getInstance().findInstanceContainersMetadata(id=root_material_id)[0]
|
||||
if original_material["approximate_diameter"] == approximate_diameter:
|
||||
return root_material_id
|
||||
|
||||
matching_materials = CuraContainerRegistry.getInstance().findInstanceContainersMetadata(type = "material", brand = original_material["brand"], definition = original_material["definition"], material = original_material["material"], color_name = original_material["color_name"])
|
||||
for material in matching_materials:
|
||||
if material["approximate_diameter"] == approximate_diameter:
|
||||
return material["id"]
|
||||
return 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
|
||||
|
||||
## Gives a dictionary of all root material IDs and their associated
|
||||
# MaterialNodes from the ContainerTree that are available for the given
|
||||
# printer and variant.
|
||||
def getAvailableMaterials(self, definition_id: str, nozzle_name: Optional[str]) -> Dict[str, MaterialNode]:
|
||||
return ContainerTree.getInstance().machines[definition_id].variants[nozzle_name].materials
|
||||
|
||||
#
|
||||
# A convenience function to get available materials for the given machine with the extruder position.
|
||||
#
|
||||
def getAvailableMaterialsForMachineExtruder(self, machine: "GlobalStack",
|
||||
extruder_stack: "ExtruderStack") -> Dict[str, MaterialNode]:
|
||||
nozzle_name = None
|
||||
if extruder_stack.variant.getId() != "empty_variant":
|
||||
nozzle_name = extruder_stack.variant.getName()
|
||||
|
||||
# Fetch the available materials (ContainerNode) for the current active machine and extruder setup.
|
||||
materials = self.getAvailableMaterials(machine.definition.getId(), nozzle_name)
|
||||
compatible_material_diameter = extruder_stack.getApproximateMaterialDiameter()
|
||||
result = {key: material for key, material in materials.items() if material.container and float(material.container.getMetaDataEntry("approximate_diameter")) == compatible_material_diameter}
|
||||
return result
|
||||
|
||||
#
|
||||
# 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"]:
|
||||
container_tree = ContainerTree.getInstance()
|
||||
machine_node = container_tree.machines.get(machine_definition_id)
|
||||
if machine_node is None:
|
||||
Logger.log("w", "Could not find machine with definition %s in the container tree", machine_definition_id)
|
||||
return None
|
||||
|
||||
variant_node = machine_node.variants.get(nozzle_name)
|
||||
if variant_node is None:
|
||||
Logger.log("w", "Could not find variant %s for machine with definition %s in the container tree", nozzle_name, machine_definition_id )
|
||||
return None
|
||||
|
||||
material_node = variant_node.materials.get(root_material_id)
|
||||
|
||||
if material_node is None:
|
||||
Logger.log("w", "Could not find material %s for machine with definition %s and variant %s in the container tree", root_material_id, machine_definition_id, nozzle_name)
|
||||
return None
|
||||
|
||||
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"]:
|
||||
machine_definition = global_stack.definition
|
||||
extruder = global_stack.extruderList[int(position)]
|
||||
variant_name = extruder.variant.getName()
|
||||
approximate_diameter = extruder.getApproximateMaterialDiameter()
|
||||
|
||||
return self.getMaterialNode(machine_definition.getId(), variant_name, buildplate_name, approximate_diameter, material_guid)
|
||||
|
||||
# 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
|
||||
|
||||
#
|
||||
# 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) -> "MaterialNode":
|
||||
definition_id = global_stack.definition.getId()
|
||||
machine_node = ContainerTree.getInstance().machines[definition_id]
|
||||
if nozzle_name in machine_node.variants:
|
||||
nozzle_node = machine_node.variants[nozzle_name]
|
||||
else:
|
||||
Logger.log("w", "Could not find variant {nozzle_name} for machine with definition {definition_id} in the container tree".format(nozzle_name = nozzle_name, definition_id = definition_id))
|
||||
nozzle_node = next(iter(machine_node.variants))
|
||||
|
||||
if not parseBool(global_stack.getMetaDataEntry("has_materials", False)):
|
||||
return next(iter(nozzle_node.materials))
|
||||
|
||||
if extruder_definition is not None:
|
||||
material_diameter = extruder_definition.getProperty("material_diameter", "value")
|
||||
else:
|
||||
material_diameter = global_stack.extruders[position].getCompatibleMaterialDiameter()
|
||||
approximate_material_diameter = round(material_diameter)
|
||||
|
||||
return nozzle_node.preferredMaterial(approximate_material_diameter)
|
||||
|
||||
def removeMaterialByRootId(self, root_material_id: str):
|
||||
container_registry = CuraContainerRegistry.getInstance()
|
||||
results = container_registry.findContainers(id = root_material_id)
|
||||
if not results:
|
||||
container_registry.addWrongContainerId(root_material_id)
|
||||
|
||||
for result in results:
|
||||
container_registry.removeContainer(result.getMetaDataEntry("id", ""))
|
||||
|
||||
@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.base_file
|
||||
ids_to_remove = {metadata.get("id", "") for metadata in CuraContainerRegistry.getInstance().findInstanceContainersMetadata(base_file = root_material_id)}
|
||||
|
||||
for extruder_stack in CuraContainerRegistry.getInstance().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:
|
||||
return cura.CuraApplication.CuraApplication.getMaterialManagementModel().setMaterialName(material_node, 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:
|
||||
return cura.CuraApplication.CuraApplication.getMaterialManagementModel().setMaterialName(material_node)
|
||||
|
||||
def duplicateMaterialByRootId(self, root_material_id: str, new_base_id: Optional[str] = None, new_metadata: Optional[Dict[str, Any]] = None) -> Optional[str]:
|
||||
result = cura.CuraApplication.CuraApplication.getInstance().getMaterialManagementModel().duplicateMaterialByBaseFile(root_material_id, new_base_id, new_metadata)
|
||||
if result is None:
|
||||
return "ERROR"
|
||||
return result
|
||||
|
||||
## 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) -> str:
|
||||
result = cura.CuraApplication.CuraApplication.getInstance().getMaterialManagementModel().duplicateMaterial(material_node, new_base_id, new_metadata)
|
||||
if result is None:
|
||||
return "ERROR"
|
||||
return result
|
||||
|
||||
## 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:
|
||||
return cura.CuraApplication.CuraApplication.getMaterialManagementModel().createMaterial()
|
||||
|
||||
@pyqtSlot(str)
|
||||
def addFavorite(self, root_material_id: str) -> None:
|
||||
self._favorites.add(root_material_id)
|
||||
self.materialsUpdated.emit()
|
||||
|
||||
# Ensure all settings are saved.
|
||||
cura.CuraApplication.CuraApplication.getInstance().getPreferences().setValue("cura/favorite_materials", ";".join(list(self._favorites)))
|
||||
cura.CuraApplication.CuraApplication.getInstance().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.
|
||||
cura.CuraApplication.CuraApplication.getInstance().getPreferences().setValue("cura/favorite_materials", ";".join(list(self._favorites)))
|
||||
cura.CuraApplication.CuraApplication.getInstance().saveSettings()
|
||||
|
||||
@pyqtSlot()
|
||||
def getFavorites(self):
|
||||
return self._favorites
|
||||
|
|
@ -1,11 +1,12 @@
|
|||
# Copyright (c) 2019 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
from typing import Optional, Dict, Set
|
||||
from typing import Dict, Set
|
||||
|
||||
from PyQt5.QtCore import Qt, pyqtSignal, pyqtProperty
|
||||
from PyQt5.QtCore import Qt, QTimer, pyqtSignal, pyqtProperty
|
||||
|
||||
from UM.Qt.ListModel import ListModel
|
||||
from UM.Logger import Logger
|
||||
|
||||
import cura.CuraApplication # Imported like this to prevent a circular reference.
|
||||
from cura.Machines.ContainerTree import ContainerTree
|
||||
|
|
@ -38,14 +39,25 @@ class BaseMaterialsModel(ListModel):
|
|||
self._extruder_stack = None
|
||||
self._enabled = True
|
||||
|
||||
# CURA-6904
|
||||
# Updating the material model requires information from material nodes and containers. We use a timer here to
|
||||
# make sure that an update function call will not be directly invoked by an event. Because the triggered event
|
||||
# can be caused in the middle of a XMLMaterial loading, and the material container we try to find may not be
|
||||
# in the system yet. This will cause an infinite recursion of (1) trying to load a material, (2) trying to
|
||||
# update the material model, (3) cannot find the material container, load it, (4) repeat #1.
|
||||
self._update_timer = QTimer()
|
||||
self._update_timer.setInterval(100)
|
||||
self._update_timer.setSingleShot(True)
|
||||
self._update_timer.timeout.connect(self._update)
|
||||
|
||||
# Update the stack and the model data when the machine changes
|
||||
self._machine_manager.globalContainerChanged.connect(self._updateExtruderStack)
|
||||
self._updateExtruderStack()
|
||||
|
||||
# Update this model when switching machines, when adding materials or changing their metadata.
|
||||
self._machine_manager.activeStackChanged.connect(self._update)
|
||||
# Update this model when switching machines or tabs, when adding materials or changing their metadata.
|
||||
self._machine_manager.activeStackChanged.connect(self._onChanged)
|
||||
ContainerTree.getInstance().materialsChanged.connect(self._materialsListChanged)
|
||||
self._application.getMaterialManagementModel().favoritesChanged.connect(self._update)
|
||||
self._application.getMaterialManagementModel().favoritesChanged.connect(self._onChanged)
|
||||
|
||||
self.addRoleName(Qt.UserRole + 1, "root_material_id")
|
||||
self.addRoleName(Qt.UserRole + 2, "id")
|
||||
|
|
@ -64,14 +76,17 @@ class BaseMaterialsModel(ListModel):
|
|||
self.addRoleName(Qt.UserRole + 15, "container_node")
|
||||
self.addRoleName(Qt.UserRole + 16, "is_favorite")
|
||||
|
||||
def _onChanged(self) -> None:
|
||||
self._update_timer.start()
|
||||
|
||||
def _updateExtruderStack(self):
|
||||
global_stack = self._machine_manager.activeMachine
|
||||
if global_stack is None:
|
||||
return
|
||||
|
||||
if self._extruder_stack is not None:
|
||||
self._extruder_stack.pyqtContainersChanged.disconnect(self._update)
|
||||
self._extruder_stack.approximateMaterialDiameterChanged.disconnect(self._update)
|
||||
self._extruder_stack.pyqtContainersChanged.disconnect(self._onChanged)
|
||||
self._extruder_stack.approximateMaterialDiameterChanged.disconnect(self._onChanged)
|
||||
|
||||
try:
|
||||
self._extruder_stack = global_stack.extruderList[self._extruder_position]
|
||||
|
|
@ -79,10 +94,10 @@ class BaseMaterialsModel(ListModel):
|
|||
self._extruder_stack = None
|
||||
|
||||
if self._extruder_stack is not None:
|
||||
self._extruder_stack.pyqtContainersChanged.connect(self._update)
|
||||
self._extruder_stack.approximateMaterialDiameterChanged.connect(self._update)
|
||||
self._extruder_stack.pyqtContainersChanged.connect(self._onChanged)
|
||||
self._extruder_stack.approximateMaterialDiameterChanged.connect(self._onChanged)
|
||||
# Force update the model when the extruder stack changes
|
||||
self._update()
|
||||
self._onChanged()
|
||||
|
||||
def setExtruderPosition(self, position: int):
|
||||
if self._extruder_stack is None or self._extruder_position != position:
|
||||
|
|
@ -99,7 +114,7 @@ class BaseMaterialsModel(ListModel):
|
|||
self._enabled = enabled
|
||||
if self._enabled:
|
||||
# ensure the data is there again.
|
||||
self._update()
|
||||
self._onChanged()
|
||||
self.enabledChanged.emit()
|
||||
|
||||
@pyqtProperty(bool, fset = setEnabled, notify = enabledChanged)
|
||||
|
|
@ -119,15 +134,15 @@ class BaseMaterialsModel(ListModel):
|
|||
return
|
||||
if material.variant.machine.container_id != global_stack.definition.getId():
|
||||
return
|
||||
self._update()
|
||||
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._update()
|
||||
self._onChanged()
|
||||
|
||||
## This is an abstract method that needs to be implemented by the specific
|
||||
# models themselves.
|
||||
## This is an abstract method that needs to be implemented by the specific
|
||||
# models themselves.
|
||||
def _update(self):
|
||||
self._favorite_ids = set(cura.CuraApplication.CuraApplication.getInstance().getPreferences().getValue("cura/favorite_materials").split(";"))
|
||||
|
||||
|
|
@ -139,9 +154,14 @@ class BaseMaterialsModel(ListModel):
|
|||
if not extruder_stack:
|
||||
return
|
||||
nozzle_name = extruder_stack.variant.getName()
|
||||
materials = ContainerTree.getInstance().machines[global_stack.definition.getId()].variants[nozzle_name].materials
|
||||
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.container.getMetaDataEntry("approximate_diameter")) == approximate_material_diameter}
|
||||
self._available_materials = {key: material for key, material in materials.items() if float(material.getMetaDataEntry("approximate_diameter", -1)) == approximate_material_diameter}
|
||||
|
||||
## This method is used by all material models in the beginning of the
|
||||
# _update() method in order to prevent errors. It's the same in all models
|
||||
|
|
|
|||
|
|
@ -2,13 +2,8 @@
|
|||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
from PyQt5.QtCore import Qt
|
||||
|
||||
from UM.Application import Application
|
||||
from UM.Logger import Logger
|
||||
from UM.Qt.ListModel import ListModel
|
||||
from UM.Util import parseBool
|
||||
|
||||
from cura.Machines.VariantType import VariantType
|
||||
|
||||
|
||||
class BuildPlateModel(ListModel):
|
||||
|
|
@ -26,4 +21,4 @@ class BuildPlateModel(ListModel):
|
|||
def _update(self):
|
||||
Logger.log("d", "Updating {model_class_name}.".format(model_class_name = self.__class__.__name__))
|
||||
self.setItems([])
|
||||
return
|
||||
return
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@ from UM.Util import parseBool
|
|||
from UM.OutputDevice.OutputDeviceManager import ManualDeviceAdditionAttempt
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from PyQt5.QtCore import QObject
|
||||
from UM.OutputDevice.OutputDevicePlugin import OutputDevicePlugin
|
||||
from cura.CuraApplication import CuraApplication
|
||||
from cura.PrinterOutput.NetworkedPrinterOutputDevice import NetworkedPrinterOutputDevice
|
||||
|
|
|
|||
|
|
@ -9,14 +9,14 @@ class FavoriteMaterialsModel(BaseMaterialsModel):
|
|||
def __init__(self, parent = None):
|
||||
super().__init__(parent)
|
||||
cura.CuraApplication.CuraApplication.getInstance().getPreferences().preferenceChanged.connect(self._onFavoritesChanged)
|
||||
self._update()
|
||||
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._update()
|
||||
self._onChanged()
|
||||
|
||||
def _update(self):
|
||||
if not self._canUpdate():
|
||||
|
|
@ -27,7 +27,7 @@ class FavoriteMaterialsModel(BaseMaterialsModel):
|
|||
|
||||
for root_material_id, container_node in self._available_materials.items():
|
||||
# Do not include the materials from a to-be-removed package
|
||||
if bool(container_node.container.getMetaDataEntry("removed", False)):
|
||||
if bool(container_node.getMetaDataEntry("removed", False)):
|
||||
continue
|
||||
|
||||
# Only add results for favorite materials
|
||||
|
|
|
|||
|
|
@ -33,11 +33,11 @@ class FirstStartMachineActionsModel(ListModel):
|
|||
self._current_action_index = 0
|
||||
|
||||
self._application = application
|
||||
self._application.initializationFinished.connect(self._initialize)
|
||||
self._application.initializationFinished.connect(self.initialize)
|
||||
|
||||
self._previous_global_stack = None
|
||||
|
||||
def _initialize(self) -> None:
|
||||
def initialize(self) -> None:
|
||||
self._application.getMachineManager().globalContainerChanged.connect(self._update)
|
||||
self._update()
|
||||
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ class GenericMaterialsModel(BaseMaterialsModel):
|
|||
|
||||
def __init__(self, parent = None):
|
||||
super().__init__(parent)
|
||||
self._update()
|
||||
self._onChanged()
|
||||
|
||||
def _update(self):
|
||||
if not self._canUpdate():
|
||||
|
|
@ -18,11 +18,11 @@ class GenericMaterialsModel(BaseMaterialsModel):
|
|||
|
||||
for root_material_id, container_node in self._available_materials.items():
|
||||
# Do not include the materials from a to-be-removed package
|
||||
if bool(container_node.container.getMetaDataEntry("removed", False)):
|
||||
if bool(container_node.getMetaDataEntry("removed", False)):
|
||||
continue
|
||||
|
||||
# Only add results for generic materials
|
||||
if container_node.container.getMetaDataEntry("brand", "unknown").lower() != "generic":
|
||||
if container_node.getMetaDataEntry("brand", "unknown").lower() != "generic":
|
||||
continue
|
||||
|
||||
item = self._createMaterialItem(root_material_id, container_node)
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
#Copyright (c) 2019 Ultimaker B.V.
|
||||
#Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
from PyQt5.QtCore import Qt
|
||||
import collections
|
||||
from typing import TYPE_CHECKING
|
||||
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
|
||||
|
|
@ -25,16 +26,34 @@ class IntentCategoryModel(ListModel):
|
|||
IntentCategoryRole = Qt.UserRole + 2
|
||||
WeightRole = Qt.UserRole + 3
|
||||
QualitiesRole = Qt.UserRole + 4
|
||||
|
||||
#Translations to user-visible string. Ordered by weight.
|
||||
#TODO: Create a solution for this name and weight to be used dynamically.
|
||||
name_translation = collections.OrderedDict() #type: "collections.OrderedDict[str,str]"
|
||||
name_translation["default"] = catalog.i18nc("@label", "Default")
|
||||
name_translation["engineering"] = catalog.i18nc("@label", "Engineering")
|
||||
name_translation["smooth"] = catalog.i18nc("@label", "Smooth")
|
||||
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:
|
||||
|
|
@ -45,21 +64,25 @@ class IntentCategoryModel(ListModel):
|
|||
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 = application.getMachineManager()
|
||||
machine_manager.globalContainerChanged.connect(self.update)
|
||||
machine_manager.activeQualityGroupChanged.connect(self.update)
|
||||
machine_manager.activeStackChanged.connect(self.update)
|
||||
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.
|
||||
|
|
@ -67,18 +90,29 @@ class IntentCategoryModel(ListModel):
|
|||
if container.getMetaDataEntry("type") == "intent":
|
||||
self.update()
|
||||
|
||||
def update(self):
|
||||
self._update_timer.start()
|
||||
|
||||
## Updates the list of intents.
|
||||
def update(self) -> None:
|
||||
def _update(self) -> None:
|
||||
available_categories = IntentManager.getInstance().currentAvailableIntentCategories()
|
||||
result = []
|
||||
for category in available_categories:
|
||||
qualities = IntentModel()
|
||||
qualities.setIntentCategory(category)
|
||||
result.append({
|
||||
"name": self.name_translation.get(category, catalog.i18nc("@label", "Unknown")),
|
||||
"name": IntentCategoryModel.translation(category, "name", catalog.i18nc("@label", "Unknown")),
|
||||
"description": IntentCategoryModel.translation(category, "description", None),
|
||||
"intent_category": category,
|
||||
"weight": list(self.name_translation.keys()).index(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)
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ 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
|
||||
|
|
@ -101,6 +102,9 @@ class IntentModel(ListModel):
|
|||
|
||||
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)
|
||||
|
|
|
|||
24
cura/Machines/Models/IntentTranslations.py
Normal file
24
cura/Machines/Models/IntentTranslations.py
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
import collections
|
||||
from typing import Dict, Optional
|
||||
|
||||
from UM.i18n import i18nCatalog
|
||||
from typing import Dict, Optional
|
||||
catalog = i18nCatalog("cura")
|
||||
|
||||
|
||||
intent_translations = collections.OrderedDict() # type: collections.OrderedDict[str, Dict[str, Optional[str]]]
|
||||
intent_translations["default"] = {
|
||||
"name": catalog.i18nc("@label", "Default")
|
||||
}
|
||||
intent_translations["visual"] = {
|
||||
"name": catalog.i18nc("@label", "Visual"),
|
||||
"description": catalog.i18nc("@text", "The visual profile is designed to print visual prototypes and models with the intent of high visual and surface quality.")
|
||||
}
|
||||
intent_translations["engineering"] = {
|
||||
"name": catalog.i18nc("@label", "Engineering"),
|
||||
"description": catalog.i18nc("@text", "The engineering profile is designed to print functional prototypes and end-use parts with the intent of better accuracy and for closer tolerances.")
|
||||
}
|
||||
intent_translations["quick"] = {
|
||||
"name": catalog.i18nc("@label", "Draft"),
|
||||
"description": catalog.i18nc("@text", "The draft profile is designed to print initial prototypes and concept validation with the intent of significant print time reduction.")
|
||||
}
|
||||
|
|
@ -37,18 +37,18 @@ class MaterialBrandsModel(BaseMaterialsModel):
|
|||
# Part 1: Generate the entire tree of brands -> material types -> spcific materials
|
||||
for root_material_id, container_node in self._available_materials.items():
|
||||
# Do not include the materials from a to-be-removed package
|
||||
if bool(container_node.container.getMetaDataEntry("removed", False)):
|
||||
if bool(container_node.getMetaDataEntry("removed", False)):
|
||||
continue
|
||||
|
||||
# Add brands we haven't seen yet to the dict, skipping generics
|
||||
brand = container_node.container.getMetaDataEntry("brand", "")
|
||||
brand = container_node.getMetaDataEntry("brand", "")
|
||||
if brand.lower() == "generic":
|
||||
continue
|
||||
if brand not in brand_group_dict:
|
||||
brand_group_dict[brand] = {}
|
||||
|
||||
# Add material types we haven't seen yet to the dict
|
||||
material_type = container_node.container.getMetaDataEntry("material", "")
|
||||
material_type = container_node.getMetaDataEntry("material", "")
|
||||
if material_type not in brand_group_dict[brand]:
|
||||
brand_group_dict[brand][material_type] = []
|
||||
|
||||
|
|
|
|||
|
|
@ -3,9 +3,9 @@
|
|||
|
||||
from PyQt5.QtCore import Qt
|
||||
|
||||
from UM.Application import Application
|
||||
from UM.Logger import Logger
|
||||
from UM.Qt.ListModel import ListModel
|
||||
import cura.CuraApplication # Imported like this to prevent circular dependencies.
|
||||
from cura.Machines.ContainerTree import ContainerTree
|
||||
|
||||
|
||||
|
|
@ -21,16 +21,13 @@ class NozzleModel(ListModel):
|
|||
self.addRoleName(self.HotendNameRole, "hotend_name")
|
||||
self.addRoleName(self.ContainerNodeRole, "container_node")
|
||||
|
||||
self._application = Application.getInstance()
|
||||
self._machine_manager = self._application.getMachineManager()
|
||||
|
||||
self._machine_manager.globalContainerChanged.connect(self._update)
|
||||
cura.CuraApplication.CuraApplication.getInstance().getMachineManager().globalContainerChanged.connect(self._update)
|
||||
self._update()
|
||||
|
||||
def _update(self):
|
||||
Logger.log("d", "Updating {model_class_name}.".format(model_class_name = self.__class__.__name__))
|
||||
|
||||
global_stack = self._machine_manager.activeMachine
|
||||
global_stack = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack()
|
||||
if global_stack is None:
|
||||
self.setItems([])
|
||||
return
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ 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")
|
||||
|
|
@ -336,20 +337,23 @@ class QualityManagementModel(ListModel):
|
|||
"quality_type": quality_type,
|
||||
"quality_changes_group": None,
|
||||
"intent_category": intent_category,
|
||||
"section_name": catalog.i18nc("@label", intent_category.capitalize()),
|
||||
"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: (x["intent_category"], x["quality_type"]))
|
||||
|
||||
result = sorted(result, key = lambda x: (list(intent_translations).index(x["intent_category"]), x["quality_type"]))
|
||||
item_list += result
|
||||
|
||||
# Create quality_changes group items
|
||||
quality_changes_item_list = []
|
||||
for quality_changes_group in quality_changes_group_list:
|
||||
# CURA-6913 Note that custom qualities can be based on "not supported", so the quality group can be None.
|
||||
quality_group = quality_group_dict.get(quality_changes_group.quality_type)
|
||||
quality_type = quality_changes_group.quality_type
|
||||
item = {"name": quality_changes_group.name,
|
||||
"is_read_only": False,
|
||||
"quality_group": quality_group,
|
||||
"quality_type": quality_group.quality_type,
|
||||
"quality_type": quality_type,
|
||||
"quality_changes_group": quality_changes_group,
|
||||
"intent_category": quality_changes_group.intent_category,
|
||||
"section_name": catalog.i18nc("@label", "Custom profiles"),
|
||||
|
|
|
|||
|
|
@ -40,7 +40,8 @@ class QualityProfilesDropDownMenuModel(ListModel):
|
|||
|
||||
application.globalContainerStackChanged.connect(self._onChange)
|
||||
machine_manager.activeQualityGroupChanged.connect(self._onChange)
|
||||
machine_manager.activeStackChanged.connect(self._onChange)
|
||||
machine_manager.activeMaterialChanged.connect(self._onChange)
|
||||
machine_manager.activeVariantChanged.connect(self._onChange)
|
||||
machine_manager.extruderChanged.connect(self._onChange)
|
||||
|
||||
extruder_manager = application.getExtruderManager()
|
||||
|
|
|
|||
|
|
@ -68,7 +68,7 @@ class QualityGroup:
|
|||
if not node.container:
|
||||
Logger.log("w", "Node {0} doesn't have a container.".format(node.container_id))
|
||||
return
|
||||
is_experimental = parseBool(node.container.getMetaDataEntry("is_experimental", False))
|
||||
is_experimental = parseBool(node.getMetaDataEntry("is_experimental", False))
|
||||
self.is_experimental |= is_experimental
|
||||
|
||||
def setExtruderNode(self, position: int, node: "ContainerNode") -> None:
|
||||
|
|
@ -78,5 +78,5 @@ class QualityGroup:
|
|||
if not node.container:
|
||||
Logger.log("w", "Node {0} doesn't have a container.".format(node.container_id))
|
||||
return
|
||||
is_experimental = parseBool(node.container.getMetaDataEntry("is_experimental", False))
|
||||
is_experimental = parseBool(node.getMetaDataEntry("is_experimental", False))
|
||||
self.is_experimental |= is_experimental
|
||||
|
|
|
|||
|
|
@ -1,215 +0,0 @@
|
|||
# Copyright (c) 2019 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
from typing import Any, Dict, List, Optional, TYPE_CHECKING
|
||||
|
||||
from PyQt5.QtCore import QObject, pyqtSignal, pyqtSlot
|
||||
|
||||
from UM.Util import parseBool
|
||||
from UM.Settings.InstanceContainer import InstanceContainer
|
||||
from UM.Decorators import deprecated
|
||||
|
||||
import cura.CuraApplication
|
||||
from cura.Settings.ExtruderStack import ExtruderStack
|
||||
|
||||
from cura.Machines.ContainerTree import ContainerTree # The implementation that replaces this manager, to keep the deprecated interface working.
|
||||
from .QualityChangesGroup import QualityChangesGroup
|
||||
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
|
||||
|
||||
|
||||
#
|
||||
# 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):
|
||||
__instance = None
|
||||
|
||||
@classmethod
|
||||
@deprecated("Use the ContainerTree structure instead.", since = "4.3")
|
||||
def getInstance(cls) -> "QualityManager":
|
||||
if cls.__instance is None:
|
||||
cls.__instance = QualityManager()
|
||||
return cls.__instance
|
||||
|
||||
qualitiesUpdated = pyqtSignal()
|
||||
|
||||
def __init__(self, parent = None) -> None:
|
||||
super().__init__(parent)
|
||||
application = cura.CuraApplication.CuraApplication.getInstance()
|
||||
self._container_registry = application.getContainerRegistry()
|
||||
|
||||
self._empty_quality_container = application.empty_quality_container
|
||||
self._empty_quality_changes_container = 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)
|
||||
|
||||
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
|
||||
|
||||
# Returns a dict of "custom profile name" -> QualityChangesGroup
|
||||
def getQualityChangesGroups(self, machine: "GlobalStack") -> List[QualityChangesGroup]:
|
||||
variant_names = [extruder.variant.getName() for extruder in machine.extruderList]
|
||||
material_bases = [extruder.material.getMetaDataEntry("base_file") for extruder in machine.extruderList]
|
||||
extruder_enabled = [extruder.isEnabled for extruder in machine.extruderList]
|
||||
machine_node = ContainerTree.getInstance().machines[machine.definition.getId()]
|
||||
return machine_node.getQualityChangesGroups(variant_names, material_bases, extruder_enabled)
|
||||
|
||||
## Gets the quality groups for the current printer.
|
||||
#
|
||||
# Both available and unavailable quality groups will be included. Whether
|
||||
# a quality group is available can be known via the field
|
||||
# ``QualityGroup.is_available``. For more details, see QualityGroup.
|
||||
# \return A dictionary with quality types as keys and the quality groups
|
||||
# for those types as values.
|
||||
def getQualityGroups(self, global_stack: "GlobalStack") -> Dict[str, QualityGroup]:
|
||||
# Gather up the variant names and material base files for each extruder.
|
||||
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]
|
||||
definition_id = global_stack.definition.getId()
|
||||
return ContainerTree.getInstance().machines[definition_id].getQualityGroups(variant_names, material_bases, extruder_enabled)
|
||||
|
||||
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:
|
||||
quality_group = QualityGroup(node.getMetaDataEntry("name", ""), node.quality_type)
|
||||
quality_group.setGlobalNode(node)
|
||||
quality_group_dict[node.quality_type] = quality_group
|
||||
|
||||
return quality_group_dict
|
||||
|
||||
## Get the quality group for the preferred quality type for a certain
|
||||
# global stack.
|
||||
#
|
||||
# If the preferred quality type is not available, ``None`` will be
|
||||
# returned.
|
||||
# \param machine The global stack of the machine to get the preferred
|
||||
# quality group for.
|
||||
# \return The preferred quality group, or ``None`` if that is not
|
||||
# available.
|
||||
def getDefaultQualityType(self, machine: "GlobalStack") -> Optional[QualityGroup]:
|
||||
machine_node = ContainerTree.getInstance().machines[machine.definition.getId()]
|
||||
quality_groups = self.getQualityGroups(machine)
|
||||
result = quality_groups.get(machine_node.preferred_quality_type)
|
||||
if result is not None and result.is_available:
|
||||
return result
|
||||
return None # If preferred quality type is not available, leave it up for the caller.
|
||||
|
||||
|
||||
#
|
||||
# Methods for GUI
|
||||
#
|
||||
|
||||
## 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:
|
||||
return cura.CuraApplication.CuraApplication.getInstance().getQualityManagementModel().removeQualityChangesGroup(quality_changes_group)
|
||||
|
||||
## 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:
|
||||
return cura.CuraApplication.CuraApplication.getInstance().getQualityManagementModel().renameQualityChangesGroup(quality_changes_group, 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, quality_changes_name: str, quality_model_item: Dict[str, Any]) -> None:
|
||||
return cura.CuraApplication.CuraApplication.getInstance().getQualityManagementModel().duplicateQualityChanges(quality_changes_name, quality_model_item)
|
||||
|
||||
## 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:
|
||||
return cura.CuraApplication.CuraApplication.getInstance().getQualityManagementModel().createQualityChanges(base_name)
|
||||
|
||||
## Create a quality changes container with the given set-up.
|
||||
# \param quality_type The quality type 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, new_name: str, machine: "GlobalStack", extruder_stack: Optional["ExtruderStack"]) -> "InstanceContainer":
|
||||
return cura.CuraApplication.CuraApplication.getInstance().getQualityManagementModel()._createQualityChanges(quality_type, new_name, machine, extruder_stack)
|
||||
|
||||
#
|
||||
# Gets the machine definition ID that can be used to search for Quality containers that are suitable for the given
|
||||
# machine. The rule is as follows:
|
||||
# 1. By default, the machine definition ID for quality container search will be "fdmprinter", which is the generic
|
||||
# machine.
|
||||
# 2. If a machine has its own machine quality (with "has_machine_quality = True"), we should use the given machine's
|
||||
# own machine definition ID for quality search.
|
||||
# Example: for an Ultimaker 3, the definition ID should be "ultimaker3".
|
||||
# 3. When condition (2) is met, AND the machine has "quality_definition" defined in its definition file, then the
|
||||
# definition ID specified in "quality_definition" should be used.
|
||||
# Example: for an Ultimaker 3 Extended, it has "quality_definition = ultimaker3". This means Ultimaker 3 Extended
|
||||
# shares the same set of qualities profiles as Ultimaker 3.
|
||||
#
|
||||
def getMachineDefinitionIDForQualitySearch(machine_definition: "DefinitionContainerInterface",
|
||||
default_definition_id: str = "fdmprinter") -> str:
|
||||
machine_definition_id = default_definition_id
|
||||
if parseBool(machine_definition.getMetaDataEntry("has_machine_quality", False)):
|
||||
# Only use the machine's own quality definition ID if this machine has machine quality.
|
||||
machine_definition_id = machine_definition.getMetaDataEntry("quality_definition")
|
||||
if machine_definition_id is None:
|
||||
machine_definition_id = machine_definition.getId()
|
||||
|
||||
return machine_definition_id
|
||||
|
|
@ -1,98 +0,0 @@
|
|||
# Copyright (c) 2019 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.Decorators import deprecated
|
||||
from UM.Logger import Logger
|
||||
from UM.Util import parseBool
|
||||
|
||||
from cura.Machines.ContainerNode import ContainerNode
|
||||
from cura.Machines.VariantType import VariantType, ALL_VARIANT_TYPES
|
||||
from cura.Settings.CuraContainerRegistry import CuraContainerRegistry
|
||||
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:
|
||||
__instance = None
|
||||
|
||||
@classmethod
|
||||
@deprecated("Use the ContainerTree structure instead.", since = "4.3")
|
||||
def getInstance(cls) -> "VariantManager":
|
||||
if cls.__instance is None:
|
||||
cls.__instance = VariantManager()
|
||||
return cls.__instance
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._machine_to_variant_dict_map = dict() # type: Dict[str, Dict["VariantType", Dict[str, ContainerNode]]]
|
||||
|
||||
self._exclude_variant_id_list = ["empty_variant"]
|
||||
|
||||
#
|
||||
# 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.get("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
|
||||
|
|
@ -1,13 +1,12 @@
|
|||
# Copyright (c) 2019 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
from typing import Optional, TYPE_CHECKING
|
||||
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.Settings.cura_empty_instance_containers import empty_variant_container
|
||||
from cura.Machines.ContainerNode import ContainerNode
|
||||
from cura.Machines.MaterialNode import MaterialNode
|
||||
|
||||
|
|
@ -83,12 +82,22 @@ class VariantNode(ContainerNode):
|
|||
# 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 in base_material and approximate_diameter == int(material_node.getMetaDataEntry("approximate_diameter")):
|
||||
if self.machine.preferred_material == base_material and approximate_diameter == int(material_node.getMetaDataEntry("approximate_diameter")):
|
||||
return material_node
|
||||
# First fallback: Choose any material with matching diameter.
|
||||
|
||||
# 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,
|
||||
|
|
@ -122,8 +131,8 @@ class VariantNode(ContainerNode):
|
|||
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", empty_variant_container.getName())
|
||||
if material_variant != self.variant_name:
|
||||
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")
|
||||
|
|
|
|||
|
|
@ -99,7 +99,7 @@ class AuthorizationHelpers:
|
|||
})
|
||||
except requests.exceptions.ConnectionError:
|
||||
# Connection was suddenly dropped. Nothing we can do about that.
|
||||
Logger.log("w", "Something failed while attempting to parse the JWT token")
|
||||
Logger.logException("w", "Something failed while attempting to parse the JWT token")
|
||||
return None
|
||||
if token_request.status_code not in (200, 201):
|
||||
Logger.log("w", "Could not retrieve token data from auth server: %s", token_request.text)
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@
|
|||
|
||||
import json
|
||||
from datetime import datetime, timedelta
|
||||
import os
|
||||
from typing import Optional, TYPE_CHECKING
|
||||
from urllib.parse import urlencode
|
||||
|
||||
|
|
@ -14,7 +13,6 @@ from PyQt5.QtGui import QDesktopServices
|
|||
|
||||
from UM.Logger import Logger
|
||||
from UM.Message import Message
|
||||
from UM.Platform import Platform
|
||||
from UM.Signal import Signal
|
||||
|
||||
from cura.OAuth2.LocalAuthorizationServer import LocalAuthorizationServer
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ from cura.Scene import ZOffsetDecorator
|
|||
|
||||
import random # used for list shuffling
|
||||
|
||||
|
||||
class PlatformPhysics:
|
||||
def __init__(self, controller, volume):
|
||||
super().__init__()
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ class FirmwareUpdater(QObject):
|
|||
|
||||
self._output_device = output_device
|
||||
|
||||
self._update_firmware_thread = Thread(target=self._updateFirmware, daemon=True)
|
||||
self._update_firmware_thread = Thread(target=self._updateFirmware, daemon=True, name = "FirmwareUpdateThread")
|
||||
|
||||
self._firmware_file = ""
|
||||
self._firmware_progress = 0
|
||||
|
|
@ -43,7 +43,7 @@ class FirmwareUpdater(QObject):
|
|||
## Cleanup after a succesful update
|
||||
def _cleanupAfterUpdate(self) -> None:
|
||||
# Clean up for next attempt.
|
||||
self._update_firmware_thread = Thread(target=self._updateFirmware, daemon=True)
|
||||
self._update_firmware_thread = Thread(target=self._updateFirmware, daemon=True, name = "FirmwareUpdateThread")
|
||||
self._firmware_file = ""
|
||||
self._onFirmwareProgress(100)
|
||||
self._setFirmwareUpdateState(FirmwareUpdateState.completed)
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ class PrinterOutputModel(QObject):
|
|||
self._target_bed_temperature = 0 # type: float
|
||||
self._name = ""
|
||||
self._key = "" # Unique identifier
|
||||
self._unique_name = "" # Unique name (used in Connect)
|
||||
self._controller = output_controller
|
||||
self._controller.canUpdateFirmwareChanged.connect(self._onControllerCanUpdateFirmwareChanged)
|
||||
self._extruders = [ExtruderOutputModel(printer = self, position = i) for i in range(number_of_extruders)]
|
||||
|
|
@ -190,6 +191,15 @@ class PrinterOutputModel(QObject):
|
|||
self._name = name
|
||||
self.nameChanged.emit()
|
||||
|
||||
@pyqtProperty(str, notify = nameChanged)
|
||||
def uniqueName(self) -> str:
|
||||
return self._unique_name
|
||||
|
||||
def updateUniqueName(self, unique_name: str) -> None:
|
||||
if self._unique_name != unique_name:
|
||||
self._unique_name = unique_name
|
||||
self.nameChanged.emit()
|
||||
|
||||
## Update the bed temperature. This only changes it locally.
|
||||
def updateBedTemperature(self, temperature: float) -> None:
|
||||
if self._bed_temperature != temperature:
|
||||
|
|
|
|||
|
|
@ -154,7 +154,7 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice):
|
|||
part = QHttpPart()
|
||||
|
||||
if not content_header.startswith("form-data;"):
|
||||
content_header = "form_data; " + content_header
|
||||
content_header = "form-data; " + content_header
|
||||
part.setHeader(QNetworkRequest.ContentDispositionHeader, content_header)
|
||||
|
||||
if content_type is not None:
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ from UM.Logger import Logger
|
|||
from UM.Signal import signalemitter
|
||||
from UM.Qt.QtApplication import QtApplication
|
||||
from UM.FlameProfiler import pyqtSlot
|
||||
from UM.Decorators import deprecated
|
||||
from UM.i18n import i18nCatalog
|
||||
from UM.OutputDevice.OutputDevice import OutputDevice
|
||||
|
||||
|
|
@ -203,10 +202,6 @@ class PrinterOutputDevice(QObject, OutputDevice):
|
|||
def acceptsCommands(self) -> bool:
|
||||
return self._accepts_commands
|
||||
|
||||
@deprecated("Please use the protected function instead", "3.2")
|
||||
def setAcceptsCommands(self, accepts_commands: bool) -> None:
|
||||
self._setAcceptsCommands(accepts_commands)
|
||||
|
||||
## Set a flag to signal the UI that the printer is not (yet) ready to receive commands
|
||||
def _setAcceptsCommands(self, accepts_commands: bool) -> None:
|
||||
if self._accepts_commands != accepts_commands:
|
||||
|
|
@ -225,6 +220,9 @@ class PrinterOutputDevice(QObject, OutputDevice):
|
|||
if printer.printerConfiguration is not None and printer.printerConfiguration.hasAnyMaterialLoaded():
|
||||
all_configurations.add(printer.printerConfiguration)
|
||||
all_configurations.update(printer.availableConfigurations)
|
||||
if None in all_configurations: # Shouldn't happen, but it does. I don't see how it could ever happen. Skip adding that configuration. List could end up empty!
|
||||
Logger.log("e", "Found a broken configuration in the synced list!")
|
||||
all_configurations.remove(None)
|
||||
new_configurations = sorted(all_configurations, key = lambda config: config.printerType or "")
|
||||
if new_configurations != self._unique_configurations:
|
||||
self._unique_configurations = new_configurations
|
||||
|
|
|
|||
|
|
@ -4,12 +4,12 @@ from cura.Scene.CuraSceneNode import CuraSceneNode
|
|||
|
||||
## Make a SceneNode build plate aware CuraSceneNode objects all have this decorator.
|
||||
class BuildPlateDecorator(SceneNodeDecorator):
|
||||
def __init__(self, build_plate_number = -1):
|
||||
def __init__(self, build_plate_number: int = -1) -> None:
|
||||
super().__init__()
|
||||
self._build_plate_number = None
|
||||
self._build_plate_number = build_plate_number
|
||||
self.setBuildPlateNumber(build_plate_number)
|
||||
|
||||
def setBuildPlateNumber(self, nr):
|
||||
def setBuildPlateNumber(self, nr: int) -> None:
|
||||
# Make sure that groups are set correctly
|
||||
# setBuildPlateForSelection in CuraActions makes sure that no single childs are set.
|
||||
self._build_plate_number = nr
|
||||
|
|
@ -19,7 +19,7 @@ class BuildPlateDecorator(SceneNodeDecorator):
|
|||
for child in self._node.getChildren():
|
||||
child.callDecoration("setBuildPlateNumber", nr)
|
||||
|
||||
def getBuildPlateNumber(self):
|
||||
def getBuildPlateNumber(self) -> int:
|
||||
return self._build_plate_number
|
||||
|
||||
def __deepcopy__(self, memo):
|
||||
|
|
|
|||
|
|
@ -88,27 +88,34 @@ class ConvexHullDecorator(SceneNodeDecorator):
|
|||
|
||||
return self._add2DAdhesionMargin(hull)
|
||||
|
||||
## Get the unmodified 2D projected convex hull with 2D adhesion area 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]:
|
||||
if self._node is None:
|
||||
return None
|
||||
if self._node.callDecoration("isNonPrintingMesh"):
|
||||
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.
|
||||
if self._global_stack.getProperty("print_sequence", "value") == "one_at_a_time" and not self.hasGroupAsParent(self._node):
|
||||
hull = hull.getMinkowskiHull(Polygon(numpy.array(self._global_stack.getProperty("machine_head_polygon", "value"), numpy.float32)))
|
||||
hull = self._add2DAdhesionMargin(hull)
|
||||
return hull
|
||||
# Parent can be None if node is just loaded.
|
||||
if self._isSingularOneAtATimeNode():
|
||||
hull = self.getConvexHullHeadFull()
|
||||
if hull is None:
|
||||
return None
|
||||
hull = self._add2DAdhesionMargin(hull)
|
||||
return hull
|
||||
|
||||
## Get the convex hull of the node with the full head size
|
||||
return self._compute2DConvexHull()
|
||||
|
||||
## For one at the time this is the convex hull of the node with the full head size
|
||||
# In case of printing all at once this is None.
|
||||
def getConvexHullHeadFull(self) -> Optional[Polygon]:
|
||||
if self._node is None:
|
||||
return None
|
||||
|
||||
return self._compute2DConvexHeadFull()
|
||||
if self._isSingularOneAtATimeNode():
|
||||
return self._compute2DConvexHeadFull()
|
||||
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def hasGroupAsParent(node: "SceneNode") -> bool:
|
||||
|
|
@ -118,38 +125,47 @@ class ConvexHullDecorator(SceneNodeDecorator):
|
|||
return bool(parent.callDecoration("isGroup"))
|
||||
|
||||
## Get convex hull of the object + head size
|
||||
# In case of printing all at once this is the same as the convex hull.
|
||||
# In case of printing all at once this is None.
|
||||
# For one at the time this is area with intersection of mirrored head
|
||||
def getConvexHullHead(self) -> Optional[Polygon]:
|
||||
if self._node is None:
|
||||
return None
|
||||
if self._node.callDecoration("isNonPrintingMesh"):
|
||||
return None
|
||||
if self._global_stack:
|
||||
if self._global_stack.getProperty("print_sequence", "value") == "one_at_a_time" and not self.hasGroupAsParent(self._node):
|
||||
head_with_fans = self._compute2DConvexHeadMin()
|
||||
if head_with_fans is None:
|
||||
return None
|
||||
head_with_fans_with_adhesion_margin = self._add2DAdhesionMargin(head_with_fans)
|
||||
return head_with_fans_with_adhesion_margin
|
||||
if self._isSingularOneAtATimeNode():
|
||||
head_with_fans = self._compute2DConvexHeadMin()
|
||||
if head_with_fans is None:
|
||||
return None
|
||||
head_with_fans_with_adhesion_margin = self._add2DAdhesionMargin(head_with_fans)
|
||||
return head_with_fans_with_adhesion_margin
|
||||
return None
|
||||
|
||||
## Get convex hull of the node
|
||||
# In case of printing all at once this is the same as the convex hull.
|
||||
# In case of printing all at once this None??
|
||||
# For one at the time this is the area without the head.
|
||||
def getConvexHullBoundary(self) -> Optional[Polygon]:
|
||||
if self._node is None:
|
||||
return None
|
||||
|
||||
|
||||
if self._node.callDecoration("isNonPrintingMesh"):
|
||||
return None
|
||||
|
||||
if self._global_stack:
|
||||
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
|
||||
return self._compute2DConvexHull()
|
||||
if self._isSingularOneAtATimeNode():
|
||||
# Printing one at a time and it's not an object in a group
|
||||
return self._compute2DConvexHull()
|
||||
return None
|
||||
|
||||
## Get the buildplate polygon where will be printed
|
||||
# In case of printing all at once this is the same as convex hull (no individual adhesion)
|
||||
# For one at the time this includes the adhesion area
|
||||
def getPrintingArea(self) -> Optional[Polygon]:
|
||||
if self._isSingularOneAtATimeNode():
|
||||
# In one-at-a-time mode, every printed object gets it's own adhesion
|
||||
printing_area = self.getAdhesionArea()
|
||||
else:
|
||||
printing_area = self.getConvexHull()
|
||||
return printing_area
|
||||
|
||||
## The same as recomputeConvexHull, but using a timer if it was set.
|
||||
def recomputeConvexHullDelayed(self) -> None:
|
||||
if self._recompute_convex_hull_timer is not None:
|
||||
|
|
@ -172,10 +188,9 @@ class ConvexHullDecorator(SceneNodeDecorator):
|
|||
self._convex_hull_node = None
|
||||
return
|
||||
|
||||
convex_hull = self.getConvexHull()
|
||||
if self._convex_hull_node:
|
||||
self._convex_hull_node.setParent(None)
|
||||
hull_node = ConvexHullNode.ConvexHullNode(self._node, convex_hull, self._raft_thickness, root)
|
||||
hull_node = ConvexHullNode.ConvexHullNode(self._node, self.getPrintingArea(), self._raft_thickness, root)
|
||||
self._convex_hull_node = hull_node
|
||||
|
||||
def _onSettingValueChanged(self, key: str, property_name: str) -> None:
|
||||
|
|
@ -416,6 +431,14 @@ class ConvexHullDecorator(SceneNodeDecorator):
|
|||
return True
|
||||
return self.__isDescendant(root, node.getParent())
|
||||
|
||||
## True if print_sequence is one_at_a_time and _node is not part of a group
|
||||
def _isSingularOneAtATimeNode(self) -> bool:
|
||||
if self._node is None:
|
||||
return False
|
||||
return self._global_stack is not None \
|
||||
and self._global_stack.getProperty("print_sequence", "value") == "one_at_a_time" \
|
||||
and not self.hasGroupAsParent(self._node)
|
||||
|
||||
_affected_settings = [
|
||||
"adhesion_type", "raft_margin", "print_sequence",
|
||||
"skirt_gap", "skirt_line_count", "skirt_brim_line_width", "skirt_distance", "brim_line_count"]
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# Copyright (c) 2015 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
from typing import Optional
|
||||
from typing import Optional, TYPE_CHECKING
|
||||
|
||||
from UM.Application import Application
|
||||
from UM.Math.Polygon import Polygon
|
||||
|
|
@ -11,6 +11,9 @@ from UM.Math.Color import Color
|
|||
from UM.Mesh.MeshBuilder import MeshBuilder # To create a mesh to display the convex hull with.
|
||||
from UM.View.GL.OpenGL import OpenGL
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from UM.Mesh.MeshData import MeshData
|
||||
|
||||
|
||||
class ConvexHullNode(SceneNode):
|
||||
shader = None # To prevent the shader from being re-built over and over again, only load it once.
|
||||
|
|
@ -43,7 +46,8 @@ class ConvexHullNode(SceneNode):
|
|||
|
||||
# The node this mesh is "watching"
|
||||
self._node = node
|
||||
self._convex_hull_head_mesh = None
|
||||
# Area of the head + fans for display as a shadow on the buildplate
|
||||
self._convex_hull_head_mesh = None # type: Optional[MeshData]
|
||||
|
||||
self._node.decoratorsChanged.connect(self._onNodeDecoratorsChanged)
|
||||
self._onNodeDecoratorsChanged(self._node)
|
||||
|
|
@ -76,14 +80,17 @@ class ConvexHullNode(SceneNode):
|
|||
|
||||
if self.getParent():
|
||||
if self.getMeshData() and isinstance(self._node, SceneNode) and self._node.callDecoration("getBuildPlateNumber") == Application.getInstance().getMultiBuildPlateModel().activeBuildPlate:
|
||||
# The object itself (+ adhesion in one-at-a-time mode)
|
||||
renderer.queueNode(self, transparent = True, shader = ConvexHullNode.shader, backface_cull = True, sort = -8)
|
||||
if self._convex_hull_head_mesh:
|
||||
# The full head. Rendered as a hint to the user: If this area overlaps another object A; this object
|
||||
# cannot be printed after A, because the head would hit A while printing the current object
|
||||
renderer.queueNode(self, shader = ConvexHullNode.shader, transparent = True, mesh = self._convex_hull_head_mesh, backface_cull = True, sort = -8)
|
||||
|
||||
return True
|
||||
|
||||
def _onNodeDecoratorsChanged(self, node: SceneNode) -> None:
|
||||
convex_hull_head = self._node.callDecoration("getConvexHullHead")
|
||||
convex_hull_head = self._node.callDecoration("getConvexHullHeadFull")
|
||||
if convex_hull_head:
|
||||
convex_hull_head_builder = MeshBuilder()
|
||||
convex_hull_head_builder.addConvexPolygon(convex_hull_head.getPoints(), self._mesh_height - self._thickness)
|
||||
|
|
|
|||
|
|
@ -88,7 +88,7 @@ class CuraSceneNode(SceneNode):
|
|||
|
||||
## Return if any area collides with the convex hull of this scene node
|
||||
def collidesWithAreas(self, areas: List[Polygon]) -> bool:
|
||||
convex_hull = self.callDecoration("getConvexHull")
|
||||
convex_hull = self.callDecoration("getPrintingArea")
|
||||
if convex_hull:
|
||||
if not convex_hull.isValid():
|
||||
return False
|
||||
|
|
|
|||
|
|
@ -85,7 +85,7 @@ class ContainerManager(QObject):
|
|||
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.container.getMetaDataEntry("base_file", "")
|
||||
root_material_id = container_node.getMetaDataEntry("base_file", "")
|
||||
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)
|
||||
|
|
@ -247,7 +247,7 @@ class ContainerManager(QObject):
|
|||
|
||||
try:
|
||||
with open(file_url, "rt", encoding = "utf-8") as f:
|
||||
container.deserialize(f.read())
|
||||
container.deserialize(f.read(), file_url)
|
||||
except PermissionError:
|
||||
return {"status": "error", "message": "Permission denied when trying to read the file."}
|
||||
except ContainerFormatError:
|
||||
|
|
@ -339,19 +339,18 @@ class ContainerManager(QObject):
|
|||
# \return A list of names of materials with the same GUID.
|
||||
@pyqtSlot("QVariant", bool, result = "QStringList")
|
||||
def getLinkedMaterials(self, material_node: "MaterialNode", exclude_self: bool = False) -> List[str]:
|
||||
same_guid = ContainerRegistry.getInstance().findInstanceContainersMetadata(guid = material_node.guid)
|
||||
same_guid = ContainerRegistry.getInstance().findInstanceContainersMetadata(GUID = material_node.guid)
|
||||
if exclude_self:
|
||||
return [metadata["name"] for metadata in same_guid if metadata["base_file"] != material_node.base_file]
|
||||
return list({meta["name"] for meta in same_guid if meta["base_file"] != material_node.base_file})
|
||||
else:
|
||||
return [metadata["name"] for metadata in same_guid]
|
||||
return list({meta["name"] for meta in same_guid})
|
||||
|
||||
## Unlink a material from all other materials by creating a new GUID
|
||||
# \param material_id \type{str} the id of the material to create a new GUID for.
|
||||
@pyqtSlot("QVariant")
|
||||
def unlinkMaterial(self, material_node: "MaterialNode") -> None:
|
||||
# Get the material group
|
||||
if material_node.container is None:
|
||||
Logger.log("w", "Material node {0} doesn't have a container.".format(material_node.container_id))
|
||||
if material_node.container is None: # Failed to lazy-load this container.
|
||||
return
|
||||
root_material_query = cura.CuraApplication.CuraApplication.getInstance().getContainerRegistry().findInstanceContainers(id = material_node.getMetaDataEntry("base_file", ""))
|
||||
if not root_material_query:
|
||||
|
|
|
|||
|
|
@ -37,10 +37,6 @@ class CuraStackBuilder:
|
|||
return None
|
||||
|
||||
machine_definition = definitions[0]
|
||||
# The container tree listens to the containerAdded signal to add the definition and build the tree,
|
||||
# but that signal is emitted with a delay which might not have passed yet.
|
||||
# Therefore we must make sure that it's manually added here.
|
||||
container_tree.addMachineNodeByDefinitionId(machine_definition.getId())
|
||||
machine_node = container_tree.machines[machine_definition.getId()]
|
||||
|
||||
generated_name = registry.createUniqueName("machine", "", name, machine_definition.getName())
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
# Copyright (c) 2019 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
from 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.Iterator.BreadthFirstIterator import BreadthFirstIterator
|
||||
from UM.Settings.ContainerRegistry import ContainerRegistry # Finding containers by ID.
|
||||
from UM.Decorators import deprecated
|
||||
|
||||
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.
|
||||
self._selected_object_extruders = [] # type: List[Union[str, "ExtruderStack"]]
|
||||
|
||||
self._addCurrentMachineExtruders()
|
||||
|
||||
Selection.selectionChanged.connect(self.resetSelectedObjectExtruders)
|
||||
|
||||
## Signal to notify other components when the list of extruders for a machine definition changes.
|
||||
|
|
@ -91,17 +88,6 @@ class ExtruderManager(QObject):
|
|||
def activeExtruderIndex(self) -> int:
|
||||
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)
|
||||
@deprecated("Use Cura.MachineManager.activeMachine.extruders[index].name instead", "4.3")
|
||||
def getExtruderName(self, index: int) -> str:
|
||||
try:
|
||||
return self.getActiveExtruderStacks()[index].getName()
|
||||
except IndexError:
|
||||
return ""
|
||||
|
||||
## Emitted whenever the selectedObjectExtruders property changes.
|
||||
selectedObjectExtrudersChanged = pyqtSignal()
|
||||
|
||||
|
|
@ -322,38 +308,33 @@ class ExtruderManager(QObject):
|
|||
|
||||
self.resetSelectedObjectExtruders()
|
||||
|
||||
## Adds the extruders of the currently active machine.
|
||||
def _addCurrentMachineExtruders(self) -> None:
|
||||
global_stack = self._application.getGlobalContainerStack()
|
||||
## Adds the extruders to the selected machine.
|
||||
def addMachineExtruders(self, global_stack: GlobalStack) -> None:
|
||||
extruders_changed = False
|
||||
container_registry = ContainerRegistry.getInstance()
|
||||
global_stack_id = global_stack.getId()
|
||||
|
||||
if global_stack:
|
||||
container_registry = ContainerRegistry.getInstance()
|
||||
global_stack_id = global_stack.getId()
|
||||
# Gets the extruder trains that we just created as well as any that still existed.
|
||||
extruder_trains = container_registry.findContainerStacks(type = "extruder_train", machine = global_stack_id)
|
||||
|
||||
# Gets the extruder trains that we just created as well as any that still existed.
|
||||
extruder_trains = container_registry.findContainerStacks(type = "extruder_train", machine = global_stack_id)
|
||||
# Make sure the extruder trains for the new machine can be placed in the set of sets
|
||||
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
|
||||
if global_stack_id not in self._extruder_trains:
|
||||
self._extruder_trains[global_stack_id] = {}
|
||||
extruders_changed = True
|
||||
# Register the extruder trains by position
|
||||
for extruder_train in extruder_trains:
|
||||
extruder_position = extruder_train.getMetaDataEntry("position")
|
||||
self._extruder_trains[global_stack_id][extruder_position] = extruder_train
|
||||
|
||||
# Register the extruder trains by position
|
||||
for extruder_train in extruder_trains:
|
||||
extruder_position = extruder_train.getMetaDataEntry("position")
|
||||
self._extruder_trains[global_stack_id][extruder_position] = extruder_train
|
||||
# regardless of what the next stack is, we have to set it again, because of signal routing. ???
|
||||
extruder_train.setParent(global_stack)
|
||||
extruder_train.setNextStack(global_stack)
|
||||
extruders_changed = True
|
||||
|
||||
# regardless of what the next stack is, we have to set it again, because of signal routing. ???
|
||||
extruder_train.setParent(global_stack)
|
||||
extruder_train.setNextStack(global_stack)
|
||||
extruders_changed = True
|
||||
|
||||
self.fixSingleExtrusionMachineExtruderDefinition(global_stack)
|
||||
if extruders_changed:
|
||||
self.extrudersChanged.emit(global_stack_id)
|
||||
self.setActiveExtruderIndex(0)
|
||||
self.activeExtruderChanged.emit()
|
||||
self.fixSingleExtrusionMachineExtruderDefinition(global_stack)
|
||||
if extruders_changed:
|
||||
self.extrudersChanged.emit(global_stack_id)
|
||||
|
||||
# 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.
|
||||
|
|
@ -387,7 +368,7 @@ class ExtruderManager(QObject):
|
|||
printer = global_stack.getId(), expected = expected_extruder_definition_0_id, got = extruder_stack_0.definition.getId()))
|
||||
try:
|
||||
extruder_definition = container_registry.findDefinitionContainers(id = expected_extruder_definition_0_id)[0]
|
||||
except IndexError as e:
|
||||
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)
|
||||
|
|
|
|||
|
|
@ -51,6 +51,10 @@ class ExtruderStack(CuraContainerStack):
|
|||
def getNextStack(self) -> Optional["GlobalStack"]:
|
||||
return super().getNextStack()
|
||||
|
||||
@pyqtProperty(int, constant = True)
|
||||
def position(self) -> int:
|
||||
return int(self.getMetaDataEntry("position"))
|
||||
|
||||
def setEnabled(self, enabled: bool) -> None:
|
||||
if self.getMetaDataEntry("enabled", True) == enabled: # No change.
|
||||
return # Don't emit a signal then.
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ from UM.Platform import Platform
|
|||
from UM.Util import parseBool
|
||||
|
||||
import cura.CuraApplication
|
||||
from cura.PrinterOutput.PrinterOutputDevice import ConnectionType
|
||||
|
||||
from . import Exceptions
|
||||
from .CuraContainerStack import CuraContainerStack
|
||||
|
|
@ -108,6 +109,19 @@ class GlobalStack(CuraContainerStack):
|
|||
pass
|
||||
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
|
||||
def addConfiguredConnectionType(self, connection_type: int) -> None:
|
||||
configured_connection_types = self.configuredConnectionTypes
|
||||
|
|
@ -273,15 +287,15 @@ class GlobalStack(CuraContainerStack):
|
|||
def getHeadAndFansCoordinates(self):
|
||||
return self.getProperty("machine_head_with_fans_polygon", "value")
|
||||
|
||||
@pyqtProperty(int, constant=True)
|
||||
def hasMaterials(self):
|
||||
@pyqtProperty(bool, constant = True)
|
||||
def hasMaterials(self) -> bool:
|
||||
return parseBool(self.getMetaDataEntry("has_materials", False))
|
||||
|
||||
@pyqtProperty(int, constant=True)
|
||||
def hasVariants(self):
|
||||
@pyqtProperty(bool, constant = True)
|
||||
def hasVariants(self) -> bool:
|
||||
return parseBool(self.getMetaDataEntry("has_variants", False))
|
||||
|
||||
@pyqtProperty(int, constant=True)
|
||||
@pyqtProperty(bool, constant = True)
|
||||
def hasVariantBuildplates(self) -> bool:
|
||||
return parseBool(self.getMetaDataEntry("has_variant_buildplates", False))
|
||||
|
||||
|
|
|
|||
|
|
@ -1,13 +1,15 @@
|
|||
#Copyright (c) 2019 Ultimaker B.V.
|
||||
#Cura is released under the terms of the LGPLv3 or higher.
|
||||
# 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
|
||||
import cura.CuraApplication
|
||||
|
||||
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
|
||||
from UM.Settings.InstanceContainer import InstanceContainer
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from UM.Settings.InstanceContainer import InstanceContainer
|
||||
|
|
@ -36,8 +38,16 @@ class IntentManager(QObject):
|
|||
# \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]]:
|
||||
material_node = ContainerTree.getInstance().machines[definition_id].variants[nozzle_name].materials[material_base_file]
|
||||
intent_metadatas = []
|
||||
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())
|
||||
|
|
@ -116,7 +126,7 @@ class IntentManager(QObject):
|
|||
## 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:
|
||||
def getDefaultIntent(self) -> "InstanceContainer":
|
||||
return empty_intent_container
|
||||
|
||||
@pyqtProperty(str, notify = intentCategoryChanged)
|
||||
|
|
|
|||
|
|
@ -139,8 +139,8 @@ class MachineManager(QObject):
|
|||
activeVariantChanged = pyqtSignal()
|
||||
activeQualityChanged = pyqtSignal()
|
||||
activeIntentChanged = pyqtSignal()
|
||||
activeStackChanged = pyqtSignal() # Emitted whenever the active stack is changed (ie: when changing between extruders, changing a profile, but not when changing a value)
|
||||
extruderChanged = pyqtSignal()
|
||||
activeStackChanged = pyqtSignal() # Emitted whenever the active extruder stack is changed (ie: when switching the active extruder tab or changing between printers)
|
||||
extruderChanged = pyqtSignal() # Emitted whenever an extruder is activated or deactivated or the default extruder changes.
|
||||
|
||||
activeStackValueChanged = pyqtSignal() # Emitted whenever a value inside the active stack is changed.
|
||||
activeStackValidationChanged = pyqtSignal() # Emitted whenever a validation inside active container is changed
|
||||
|
|
@ -217,6 +217,7 @@ class MachineManager(QObject):
|
|||
return 0
|
||||
return len(general_definition_containers[0].getAllKeys())
|
||||
|
||||
## Triggered when the global container stack is changed in CuraApplication.
|
||||
def _onGlobalContainerChanged(self) -> None:
|
||||
if self._global_container_stack:
|
||||
try:
|
||||
|
|
@ -268,11 +269,7 @@ class MachineManager(QObject):
|
|||
|
||||
def _onActiveExtruderStackChanged(self) -> None:
|
||||
self.blurSettings.emit() # Ensure no-one has focus.
|
||||
if self._active_container_stack is not None:
|
||||
self._active_container_stack.pyqtContainersChanged.disconnect(self.activeStackChanged) # Unplug from the old one.
|
||||
self._active_container_stack = ExtruderManager.getInstance().getActiveExtruderStack()
|
||||
if self._active_container_stack is not None:
|
||||
self._active_container_stack.pyqtContainersChanged.connect(self.activeStackChanged) # Plug into the new one.
|
||||
|
||||
def __emitChangedSignals(self) -> None:
|
||||
self.activeQualityChanged.emit()
|
||||
|
|
@ -296,7 +293,6 @@ class MachineManager(QObject):
|
|||
self.blurSettings.emit() # Ensure no-one has focus.
|
||||
|
||||
container_registry = CuraContainerRegistry.getInstance()
|
||||
|
||||
containers = container_registry.findContainerStacks(id = stack_id)
|
||||
if not containers:
|
||||
return
|
||||
|
|
@ -306,21 +302,25 @@ class MachineManager(QObject):
|
|||
# Make sure that the default machine actions for this machine have been added
|
||||
self._application.getMachineActionManager().addDefaultMachineActions(global_stack)
|
||||
|
||||
ExtruderManager.getInstance().fixSingleExtrusionMachineExtruderDefinition(global_stack)
|
||||
extruder_manager = ExtruderManager.getInstance()
|
||||
extruder_manager.fixSingleExtrusionMachineExtruderDefinition(global_stack)
|
||||
if not global_stack.isValid():
|
||||
# Mark global stack as invalid
|
||||
ConfigurationErrorMessage.getInstance().addFaultyContainers(global_stack.getId())
|
||||
return # We're done here
|
||||
|
||||
self._global_container_stack = global_stack
|
||||
extruder_manager.addMachineExtruders(global_stack)
|
||||
self._application.setGlobalContainerStack(global_stack)
|
||||
ExtruderManager.getInstance()._globalContainerStackChanged()
|
||||
self._onGlobalContainerChanged()
|
||||
|
||||
# Switch to the first enabled extruder
|
||||
self.updateDefaultExtruder()
|
||||
default_extruder_position = int(self.defaultExtruderPosition)
|
||||
ExtruderManager.getInstance().setActiveExtruderIndex(default_extruder_position)
|
||||
old_active_extruder_index = extruder_manager.activeExtruderIndex
|
||||
extruder_manager.setActiveExtruderIndex(default_extruder_position)
|
||||
if old_active_extruder_index == default_extruder_position:
|
||||
# This signal might not have been emitted yet (if it didn't change) but we still want the models to update that depend on it because we changed the contents of the containers too.
|
||||
extruder_manager.activeExtruderChanged.emit()
|
||||
|
||||
self.__emitChangedSignals()
|
||||
|
||||
|
|
@ -447,27 +447,6 @@ class MachineManager(QObject):
|
|||
def stacksHaveErrors(self) -> bool:
|
||||
return bool(self._stacks_have_errors)
|
||||
|
||||
@pyqtProperty(str, notify = globalContainerChanged)
|
||||
@deprecated("use Cura.MachineManager.activeMachine.definition.name instead", "4.1")
|
||||
def activeMachineDefinitionName(self) -> str:
|
||||
if self._global_container_stack:
|
||||
return self._global_container_stack.definition.getName()
|
||||
return ""
|
||||
|
||||
@pyqtProperty(str, notify = globalContainerChanged)
|
||||
@deprecated("use Cura.MachineManager.activeMachine.name instead", "4.1")
|
||||
def activeMachineName(self) -> str:
|
||||
if self._global_container_stack:
|
||||
return self._global_container_stack.getMetaDataEntry("group_name", self._global_container_stack.getName())
|
||||
return ""
|
||||
|
||||
@pyqtProperty(str, notify = globalContainerChanged)
|
||||
@deprecated("use Cura.MachineManager.activeMachine.id instead", "4.1")
|
||||
def activeMachineId(self) -> str:
|
||||
if self._global_container_stack:
|
||||
return self._global_container_stack.getId()
|
||||
return ""
|
||||
|
||||
@pyqtProperty(str, notify = globalContainerChanged)
|
||||
def activeMachineFirmwareVersion(self) -> str:
|
||||
if not self._printer_output_devices:
|
||||
|
|
@ -484,25 +463,6 @@ class MachineManager(QObject):
|
|||
def printerConnected(self) -> bool:
|
||||
return bool(self._printer_output_devices)
|
||||
|
||||
@pyqtProperty(bool, notify = printerConnectedStatusChanged)
|
||||
@deprecated("use Cura.MachineManager.activeMachine.configuredConnectionTypes instead", "4.2")
|
||||
def activeMachineHasRemoteConnection(self) -> bool:
|
||||
if self._global_container_stack:
|
||||
has_remote_connection = False
|
||||
|
||||
for connection_type in self._global_container_stack.configuredConnectionTypes:
|
||||
has_remote_connection |= connection_type in [ConnectionType.NetworkConnection.value,
|
||||
ConnectionType.CloudConnection.value]
|
||||
return has_remote_connection
|
||||
return False
|
||||
|
||||
@pyqtProperty("QVariantList", notify=globalContainerChanged)
|
||||
@deprecated("use Cura.MachineManager.activeMachine.configuredConnectionTypes instead", "4.1")
|
||||
def activeMachineConfiguredConnectionTypes(self):
|
||||
if self._global_container_stack:
|
||||
return self._global_container_stack.configuredConnectionTypes
|
||||
return []
|
||||
|
||||
@pyqtProperty(bool, notify = printerConnectedStatusChanged)
|
||||
def activeMachineIsGroup(self) -> bool:
|
||||
return bool(self._printer_output_devices) and len(self._printer_output_devices[0].printers) > 1
|
||||
|
|
@ -554,24 +514,6 @@ class MachineManager(QObject):
|
|||
return material.getId()
|
||||
return ""
|
||||
|
||||
## Gets a dict with the active materials ids set in all extruder stacks and the global stack
|
||||
# (when there is one extruder, the material is set in the global stack)
|
||||
#
|
||||
# \return The material ids in all stacks
|
||||
@pyqtProperty("QVariantMap", notify = activeMaterialChanged)
|
||||
@deprecated("use Cura.MachineManager.activeStack.extruders instead.", "4.3")
|
||||
def allActiveMaterialIds(self) -> Dict[str, str]:
|
||||
result = {}
|
||||
|
||||
active_stacks = ExtruderManager.getInstance().getActiveExtruderStacks()
|
||||
for stack in active_stacks:
|
||||
material_container = stack.material
|
||||
if not material_container:
|
||||
continue
|
||||
result[stack.getId()] = material_container.getId()
|
||||
|
||||
return result
|
||||
|
||||
## Gets the layer height of the currently active quality profile.
|
||||
#
|
||||
# This is indicated together with the name of the active quality profile.
|
||||
|
|
@ -693,44 +635,6 @@ class MachineManager(QObject):
|
|||
# Check if the value has to be replaced
|
||||
extruder_stack.userChanges.setProperty(key, "value", new_value)
|
||||
|
||||
@pyqtProperty(str, notify = activeVariantChanged)
|
||||
@deprecated("use Cura.MachineManager.activeStack.variant.name instead", "4.1")
|
||||
def activeVariantName(self) -> str:
|
||||
if self._active_container_stack:
|
||||
variant = self._active_container_stack.variant
|
||||
if variant:
|
||||
return variant.getName()
|
||||
|
||||
return ""
|
||||
|
||||
@pyqtProperty(str, notify = activeVariantChanged)
|
||||
@deprecated("use Cura.MachineManager.activeStack.variant.id instead", "4.1")
|
||||
def activeVariantId(self) -> str:
|
||||
if self._active_container_stack:
|
||||
variant = self._active_container_stack.variant
|
||||
if variant:
|
||||
return variant.getId()
|
||||
|
||||
return ""
|
||||
|
||||
@pyqtProperty(str, notify = activeVariantChanged)
|
||||
@deprecated("use Cura.MachineManager.activeMachine.variant.name instead", "4.1")
|
||||
def activeVariantBuildplateName(self) -> str:
|
||||
if self._global_container_stack:
|
||||
variant = self._global_container_stack.variant
|
||||
if variant:
|
||||
return variant.getName()
|
||||
|
||||
return ""
|
||||
|
||||
@pyqtProperty(str, notify = globalContainerChanged)
|
||||
@deprecated("use Cura.MachineManager.activeMachine.definition.id instead", "4.1")
|
||||
def activeDefinitionId(self) -> str:
|
||||
if self._global_container_stack:
|
||||
return self._global_container_stack.definition.id
|
||||
|
||||
return ""
|
||||
|
||||
## Get the Definition ID to use to select quality profiles for the currently active machine
|
||||
# \returns DefinitionID (string) if found, empty string otherwise
|
||||
@pyqtProperty(str, notify = globalContainerChanged)
|
||||
|
|
@ -788,27 +692,6 @@ class MachineManager(QObject):
|
|||
# This reuses the method and remove all printers recursively
|
||||
self.removeMachine(hidden_containers[0].getId())
|
||||
|
||||
@pyqtProperty(bool, notify = globalContainerChanged)
|
||||
@deprecated("use Cura.MachineManager.activeMachine.hasMaterials instead", "4.2")
|
||||
def hasMaterials(self) -> bool:
|
||||
if self._global_container_stack:
|
||||
return self._global_container_stack.hasMaterials
|
||||
return False
|
||||
|
||||
@pyqtProperty(bool, notify = globalContainerChanged)
|
||||
@deprecated("use Cura.MachineManager.activeMachine.hasVariants instead", "4.2")
|
||||
def hasVariants(self) -> bool:
|
||||
if self._global_container_stack:
|
||||
return self._global_container_stack.hasVariants
|
||||
return False
|
||||
|
||||
@pyqtProperty(bool, notify = globalContainerChanged)
|
||||
@deprecated("use Cura.MachineManager.activeMachine.hasVariantBuildplates instead", "4.2")
|
||||
def hasVariantBuildplates(self) -> bool:
|
||||
if self._global_container_stack:
|
||||
return self._global_container_stack.hasVariantBuildplates
|
||||
return False
|
||||
|
||||
## The selected buildplate is compatible if it is compatible with all the materials in all the extruders
|
||||
@pyqtProperty(bool, notify = activeMaterialChanged)
|
||||
def variantBuildplateCompatible(self) -> bool:
|
||||
|
|
@ -823,7 +706,8 @@ class MachineManager(QObject):
|
|||
if material_container == empty_material_container:
|
||||
continue
|
||||
if material_container.getMetaDataEntry("buildplate_compatible"):
|
||||
buildplate_compatible = buildplate_compatible and material_container.getMetaDataEntry("buildplate_compatible")[self.activeVariantBuildplateName]
|
||||
active_buildplate_name = self.activeMachine.variant.name
|
||||
buildplate_compatible = buildplate_compatible and material_container.getMetaDataEntry("buildplate_compatible")[active_buildplate_name]
|
||||
|
||||
return buildplate_compatible
|
||||
|
||||
|
|
@ -946,7 +830,7 @@ class MachineManager(QObject):
|
|||
if settable_per_extruder:
|
||||
limit_to_extruder = int(self._global_container_stack.getProperty(setting_key, "limit_to_extruder"))
|
||||
extruder_position = max(0, limit_to_extruder)
|
||||
extruder_stack = self.getExtruder(extruder_position)
|
||||
extruder_stack = self._global_container_stack.extruderList[extruder_position]
|
||||
if extruder_stack:
|
||||
extruder_stack.userChanges.setProperty(setting_key, "value", global_user_container.getProperty(setting_key, "value"))
|
||||
else:
|
||||
|
|
@ -957,20 +841,6 @@ class MachineManager(QObject):
|
|||
self._application.globalContainerStackChanged.emit()
|
||||
self.forceUpdateAllSettings()
|
||||
|
||||
@pyqtSlot(int, result = QObject)
|
||||
def getExtruder(self, position: int) -> Optional[ExtruderStack]:
|
||||
return self._getExtruder(position)
|
||||
|
||||
# This is a workaround for the deprecated decorator and the pyqtSlot not playing well together.
|
||||
@deprecated("use Cura.MachineManager.activeMachine.extruders instead", "4.2")
|
||||
def _getExtruder(self, position) -> Optional[ExtruderStack]:
|
||||
if self._global_container_stack:
|
||||
try:
|
||||
return self._global_container_stack.extruderList[int(position)]
|
||||
except IndexError:
|
||||
return None
|
||||
return None
|
||||
|
||||
def updateDefaultExtruder(self) -> None:
|
||||
if self._global_container_stack is None:
|
||||
return
|
||||
|
|
@ -1002,7 +872,10 @@ class MachineManager(QObject):
|
|||
def numberExtrudersEnabled(self) -> int:
|
||||
if self._global_container_stack is None:
|
||||
return 1
|
||||
return self._global_container_stack.definitionChanges.getProperty("extruders_enabled_count", "value")
|
||||
extruders_enabled_count = self._global_container_stack.definitionChanges.getProperty("extruders_enabled_count", "value")
|
||||
if extruders_enabled_count is None:
|
||||
extruders_enabled_count = len(self._global_container_stack.extruderList)
|
||||
return extruders_enabled_count
|
||||
|
||||
@pyqtProperty(str, notify = extruderChanged)
|
||||
def defaultExtruderPosition(self) -> str:
|
||||
|
|
@ -1021,10 +894,10 @@ class MachineManager(QObject):
|
|||
|
||||
@pyqtSlot(int, bool)
|
||||
def setExtruderEnabled(self, position: int, enabled: bool) -> None:
|
||||
extruder = self.getExtruder(position)
|
||||
if not extruder or self._global_container_stack is None:
|
||||
if self._global_container_stack is None:
|
||||
Logger.log("w", "Could not find extruder on position %s", position)
|
||||
return
|
||||
extruder = self._global_container_stack.extruderList[position]
|
||||
|
||||
extruder.setEnabled(enabled)
|
||||
self.updateDefaultExtruder()
|
||||
|
|
@ -1078,13 +951,6 @@ class MachineManager(QObject):
|
|||
container = extruder.userChanges
|
||||
container.removeInstance(setting_name)
|
||||
|
||||
@pyqtProperty("QVariantList", notify = globalContainerChanged)
|
||||
@deprecated("use Cura.MachineManager.activeMachine.extruders instead", "4.2")
|
||||
def currentExtruderPositions(self) -> List[str]:
|
||||
if self._global_container_stack is None:
|
||||
return []
|
||||
return sorted(list(self._global_container_stack.extruders.keys()))
|
||||
|
||||
## Update _current_root_material_id when the current root material was changed.
|
||||
def _onRootMaterialChanged(self) -> None:
|
||||
self._current_root_material_id = {}
|
||||
|
|
@ -1363,7 +1229,7 @@ class MachineManager(QObject):
|
|||
@pyqtSlot(str)
|
||||
def switchPrinterType(self, machine_name: str) -> None:
|
||||
# Don't switch if the user tries to change to the same type of printer
|
||||
if self._global_container_stack is None or self.activeMachineDefinitionName == machine_name:
|
||||
if self._global_container_stack is None or self._global_container_stack.definition.name == machine_name:
|
||||
return
|
||||
Logger.log("i", "Attempting to switch the printer type to [%s]", machine_name)
|
||||
# Get the definition id corresponding to this machine name
|
||||
|
|
@ -1381,6 +1247,8 @@ class MachineManager(QObject):
|
|||
if metadata_key in new_machine.getMetaData():
|
||||
continue # Don't copy the already preset stuff.
|
||||
new_machine.setMetaDataEntry(metadata_key, self._global_container_stack.getMetaDataEntry(metadata_key))
|
||||
# Special case, group_id should be overwritten!
|
||||
new_machine.setMetaDataEntry("group_id", self._global_container_stack.getMetaDataEntry("group_id"))
|
||||
else:
|
||||
Logger.log("i", "Found a %s with the key %s. Let's use it!", machine_name, self.activeMachineNetworkKey())
|
||||
|
||||
|
|
@ -1586,8 +1454,9 @@ class MachineManager(QObject):
|
|||
|
||||
intent_category = self.activeIntentCategory
|
||||
if intent_category != "default":
|
||||
intent_display_name = IntentCategoryModel.name_translation.get(intent_category,
|
||||
catalog.i18nc("@label", "Unknown"))
|
||||
intent_display_name = IntentCategoryModel.translation(intent_category,
|
||||
"name",
|
||||
catalog.i18nc("@label", "Unknown"))
|
||||
display_name = "{intent_name} - {the_rest}".format(intent_name = intent_display_name,
|
||||
the_rest = display_name)
|
||||
|
||||
|
|
|
|||
|
|
@ -28,20 +28,21 @@ if TYPE_CHECKING:
|
|||
class SettingInheritanceManager(QObject):
|
||||
def __init__(self, parent = None) -> None:
|
||||
super().__init__(parent)
|
||||
Application.getInstance().globalContainerStackChanged.connect(self._onGlobalContainerChanged)
|
||||
|
||||
self._global_container_stack = None # type: Optional[ContainerStack]
|
||||
self._settings_with_inheritance_warning = [] # type: List[str]
|
||||
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.setInterval(500)
|
||||
self._update_timer.setSingleShot(True)
|
||||
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()
|
||||
|
||||
## Get the keys of all children settings with an override.
|
||||
|
|
@ -88,8 +89,8 @@ class SettingInheritanceManager(QObject):
|
|||
self.settingsWithIntheritanceChanged.emit()
|
||||
|
||||
@pyqtSlot()
|
||||
def forceUpdate(self) -> None:
|
||||
self._update()
|
||||
def scheduleUpdate(self) -> None:
|
||||
self._update_timer.start()
|
||||
|
||||
def _onActiveExtruderChanged(self) -> None:
|
||||
new_active_stack = ExtruderManager.getInstance().getActiveExtruderStack()
|
||||
|
|
@ -106,7 +107,7 @@ class SettingInheritanceManager(QObject):
|
|||
if self._active_container_stack is not None:
|
||||
self._active_container_stack.propertyChanged.connect(self._onPropertyChanged)
|
||||
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:
|
||||
if (property_name == "value" or property_name == "enabled") and self._global_container_stack:
|
||||
|
|
|
|||
|
|
@ -56,11 +56,11 @@ class CuraSplashScreen(QSplashScreen):
|
|||
if buildtype:
|
||||
version[0] += " (%s)" % buildtype
|
||||
|
||||
# draw version text
|
||||
# Draw version text
|
||||
font = QFont() # Using system-default font here
|
||||
font.setPixelSize(37)
|
||||
painter.setFont(font)
|
||||
painter.drawText(215, 66, 330 * self._scale, 230 * self._scale, Qt.AlignLeft | Qt.AlignTop, version[0])
|
||||
painter.drawText(60, 66, 330 * self._scale, 230 * self._scale, Qt.AlignLeft | Qt.AlignTop, version[0])
|
||||
if len(version) > 1:
|
||||
font.setPixelSize(16)
|
||||
painter.setFont(font)
|
||||
|
|
@ -68,14 +68,14 @@ class CuraSplashScreen(QSplashScreen):
|
|||
painter.drawText(247, 105, 330 * self._scale, 255 * self._scale, Qt.AlignLeft | Qt.AlignTop, version[1])
|
||||
painter.setPen(QColor(255, 255, 255, 255))
|
||||
|
||||
# draw the loading image
|
||||
# Draw the loading image
|
||||
pen = QPen()
|
||||
pen.setWidth(6 * self._scale)
|
||||
pen.setColor(QColor(32, 166, 219, 255))
|
||||
painter.setPen(pen)
|
||||
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:
|
||||
font = QFont() # Using system-default font here
|
||||
font.setPixelSize(13)
|
||||
|
|
|
|||
30
cura/Utils/Decorators.py
Normal file
30
cura/Utils/Decorators.py
Normal 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
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
#!/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.
|
||||
|
||||
import argparse
|
||||
|
|
@ -131,7 +131,10 @@ def exceptHook(hook_type, value, traceback):
|
|||
# Set exception hook to use the crash dialog handler
|
||||
sys.excepthook = exceptHook
|
||||
# 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
|
||||
# is a race condition between Arcus and PyQt. Importing Arcus
|
||||
|
|
|
|||
|
|
@ -13,8 +13,8 @@ UM.Dialog
|
|||
id: base
|
||||
title: catalog.i18nc("@title:window", "Open Project")
|
||||
|
||||
minimumWidth: 500 * screenScaleFactor
|
||||
minimumHeight: 450 * screenScaleFactor
|
||||
minimumWidth: UM.Theme.getSize("popup_dialog").width
|
||||
minimumHeight: UM.Theme.getSize("popup_dialog").height
|
||||
width: minimumWidth
|
||||
height: minimumHeight
|
||||
|
||||
|
|
|
|||
|
|
@ -12,7 +12,6 @@ except ImportError:
|
|||
from . import ThreeMFWorkspaceReader
|
||||
|
||||
from UM.i18n import i18nCatalog
|
||||
from UM.Platform import Platform
|
||||
|
||||
catalog = i18nCatalog("cura")
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,6 @@
|
|||
"author": "Ultimaker B.V.",
|
||||
"version": "1.0.1",
|
||||
"description": "Provides support for reading 3MF files.",
|
||||
"api": "6.0",
|
||||
"api": "7.0",
|
||||
"i18n-catalog": "cura"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,6 @@
|
|||
"author": "Ultimaker B.V.",
|
||||
"version": "1.0.1",
|
||||
"description": "Provides support for writing 3MF files.",
|
||||
"api": "6.0",
|
||||
"api": "7.0",
|
||||
"i18n-catalog": "cura"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,5 +3,5 @@
|
|||
"author": "fieldOfView",
|
||||
"version": "1.0.0",
|
||||
"description": "Provides support for reading AMF files.",
|
||||
"api": "6.0.0"
|
||||
"api": "7.0.0"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,6 @@
|
|||
"author": "Ultimaker B.V.",
|
||||
"description": "Backup and restore your configuration.",
|
||||
"version": "1.2.0",
|
||||
"api": 6,
|
||||
"api": "7.0",
|
||||
"i18n-catalog": "cura"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ Window
|
|||
minimumHeight: Math.round(UM.Theme.getSize("modal_window_minimum").height)
|
||||
maximumWidth: Math.round(minimumWidth * 1.2)
|
||||
maximumHeight: Math.round(minimumHeight * 1.2)
|
||||
modality: Qt.ApplicationModal
|
||||
width: minimumWidth
|
||||
height: minimumHeight
|
||||
color: UM.Theme.getColor("main_background")
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
import numpy
|
||||
|
|
@ -72,7 +72,7 @@ class GcodeStartEndFormatter(Formatter):
|
|||
value = default_value_str
|
||||
# "-1" is global stack, and if the setting value exists in the global stack, use it as the fallback value.
|
||||
if key in kwargs["-1"]:
|
||||
value = kwargs["-1"]
|
||||
value = kwargs["-1"][key]
|
||||
if str(extruder_nr) in kwargs and key in kwargs[str(extruder_nr)]:
|
||||
value = kwargs[str(extruder_nr)][key]
|
||||
|
||||
|
|
@ -106,6 +106,11 @@ class StartSliceJob(Job):
|
|||
if stack is None:
|
||||
return False
|
||||
|
||||
# if there are no per-object settings we don't need to check the other settings here
|
||||
stack_top = stack.getTop()
|
||||
if stack_top is None or not stack_top.getAllKeys():
|
||||
return False
|
||||
|
||||
for key in stack.getAllKeys():
|
||||
validation_state = stack.getProperty(key, "validationState")
|
||||
if validation_state in (ValidatorState.Exception, ValidatorState.MaximumError, ValidatorState.MinimumError, ValidatorState.Invalid):
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
"name": "CuraEngine Backend",
|
||||
"author": "Ultimaker B.V.",
|
||||
"description": "Provides the link to the CuraEngine slicing backend.",
|
||||
"api": "6.0",
|
||||
"api": "7.0",
|
||||
"version": "1.0.1",
|
||||
"i18n-catalog": "cura"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,6 @@
|
|||
"author": "Ultimaker B.V.",
|
||||
"version": "1.0.1",
|
||||
"description": "Provides support for importing Cura profiles.",
|
||||
"api": "6.0",
|
||||
"api": "7.0",
|
||||
"i18n-catalog": "cura"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,6 @@
|
|||
"author": "Ultimaker B.V.",
|
||||
"version": "1.0.1",
|
||||
"description": "Provides support for exporting Cura profiles.",
|
||||
"api": "6.0",
|
||||
"api": "7.0",
|
||||
"i18n-catalog":"cura"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,6 @@
|
|||
"author": "Ultimaker B.V.",
|
||||
"version": "1.0.1",
|
||||
"description": "Checks for firmware updates.",
|
||||
"api": "6.0",
|
||||
"api": "7.0",
|
||||
"i18n-catalog": "cura"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,6 @@
|
|||
"author": "Ultimaker B.V.",
|
||||
"version": "1.0.1",
|
||||
"description": "Provides a machine actions for updating firmware.",
|
||||
"api": "6.0",
|
||||
"api": "7.0",
|
||||
"i18n-catalog": "cura"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,6 @@
|
|||
"author": "Ultimaker B.V.",
|
||||
"version": "1.0.1",
|
||||
"description": "Reads g-code from a compressed archive.",
|
||||
"api": "6.0",
|
||||
"api": "7.0",
|
||||
"i18n-catalog": "cura"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,6 @@
|
|||
"author": "Ultimaker B.V.",
|
||||
"version": "1.0.1",
|
||||
"description": "Writes g-code to a compressed archive.",
|
||||
"api": "6.0",
|
||||
"api": "7.0",
|
||||
"i18n-catalog": "cura"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,6 @@
|
|||
"author": "Ultimaker B.V.",
|
||||
"version": "1.0.1",
|
||||
"description": "Provides support for importing profiles from g-code files.",
|
||||
"api": "6.0",
|
||||
"api": "7.0",
|
||||
"i18n-catalog": "cura"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,6 @@
|
|||
"author": "Victor Larchenko, Ultimaker",
|
||||
"version": "1.0.1",
|
||||
"description": "Allows loading and displaying G-code files.",
|
||||
"api": "6.0",
|
||||
"api": "7.0",
|
||||
"i18n-catalog": "cura"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,6 @@
|
|||
"author": "Ultimaker B.V.",
|
||||
"version": "1.0.1",
|
||||
"description": "Writes g-code to a file.",
|
||||
"api": "6.0",
|
||||
"api": "7.0",
|
||||
"i18n-catalog": "cura"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -143,6 +143,52 @@ UM.Dialog
|
|||
}
|
||||
}
|
||||
|
||||
UM.TooltipArea {
|
||||
Layout.fillWidth:true
|
||||
height: childrenRect.height
|
||||
text: catalog.i18nc("@info:tooltip","For lithophanes a simple logarithmic model for translucency is available. For height maps the pixel values correspond to heights linearly.")
|
||||
Row {
|
||||
width: parent.width
|
||||
|
||||
Label {
|
||||
text: "Color Model"
|
||||
width: 150 * screenScaleFactor
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
ComboBox {
|
||||
id: color_model
|
||||
objectName: "ColorModel"
|
||||
model: [ catalog.i18nc("@item:inlistbox","Linear"), catalog.i18nc("@item:inlistbox","Translucency") ]
|
||||
width: 180 * screenScaleFactor
|
||||
onCurrentIndexChanged: { manager.onColorModelChanged(currentIndex) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
UM.TooltipArea {
|
||||
Layout.fillWidth:true
|
||||
height: childrenRect.height
|
||||
text: catalog.i18nc("@info:tooltip","The percentage of light penetrating a print with a thickness of 1 millimeter. Lowering this value increases the contrast in dark regions and decreases the contrast in light regions of the image.")
|
||||
visible: color_model.currentText == catalog.i18nc("@item:inlistbox","Translucency")
|
||||
Row {
|
||||
width: parent.width
|
||||
|
||||
Label {
|
||||
text: catalog.i18nc("@action:label", "1mm Transmittance (%)")
|
||||
width: 150 * screenScaleFactor
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
TextField {
|
||||
id: transmittance
|
||||
objectName: "Transmittance"
|
||||
focus: true
|
||||
validator: RegExpValidator {regExp: /^[1-9]\d{0,2}([\,|\.]\d*)?$/}
|
||||
width: 180 * screenScaleFactor
|
||||
onTextChanged: { manager.onTransmittanceChanged(text) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
UM.TooltipArea {
|
||||
Layout.fillWidth:true
|
||||
height: childrenRect.height
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@
|
|||
|
||||
import numpy
|
||||
|
||||
import math
|
||||
|
||||
from PyQt5.QtGui import QImage, qRed, qGreen, qBlue
|
||||
from PyQt5.QtCore import Qt
|
||||
|
||||
|
|
@ -46,9 +48,9 @@ class ImageReader(MeshReader):
|
|||
|
||||
def _read(self, file_name):
|
||||
size = max(self._ui.getWidth(), self._ui.getDepth())
|
||||
return self._generateSceneNode(file_name, size, self._ui.peak_height, self._ui.base_height, self._ui.smoothing, 512, self._ui.lighter_is_higher)
|
||||
return self._generateSceneNode(file_name, size, self._ui.peak_height, self._ui.base_height, self._ui.smoothing, 512, self._ui.lighter_is_higher, self._ui.use_transparency_model, self._ui.transmittance_1mm)
|
||||
|
||||
def _generateSceneNode(self, file_name, xz_size, peak_height, base_height, blur_iterations, max_size, lighter_is_higher):
|
||||
def _generateSceneNode(self, file_name, xz_size, peak_height, base_height, blur_iterations, max_size, lighter_is_higher, use_transparency_model, transmittance_1mm):
|
||||
scene_node = SceneNode()
|
||||
|
||||
mesh = MeshBuilder()
|
||||
|
|
@ -99,12 +101,14 @@ class ImageReader(MeshReader):
|
|||
for x in range(0, width):
|
||||
for y in range(0, height):
|
||||
qrgb = img.pixel(x, y)
|
||||
avg = float(qRed(qrgb) + qGreen(qrgb) + qBlue(qrgb)) / (3 * 255)
|
||||
height_data[y, x] = avg
|
||||
if use_transparency_model:
|
||||
height_data[y, x] = (0.299 * math.pow(qRed(qrgb) / 255.0, 2.2) + 0.587 * math.pow(qGreen(qrgb) / 255.0, 2.2) + 0.114 * math.pow(qBlue(qrgb) / 255.0, 2.2))
|
||||
else:
|
||||
height_data[y, x] = (0.212655 * qRed(qrgb) + 0.715158 * qGreen(qrgb) + 0.072187 * qBlue(qrgb)) / 255 # fast computation ignoring gamma and degamma
|
||||
|
||||
Job.yieldThread()
|
||||
|
||||
if not lighter_is_higher:
|
||||
if lighter_is_higher == use_transparency_model:
|
||||
height_data = 1 - height_data
|
||||
|
||||
for _ in range(0, blur_iterations):
|
||||
|
|
@ -124,8 +128,15 @@ class ImageReader(MeshReader):
|
|||
|
||||
Job.yieldThread()
|
||||
|
||||
height_data *= scale_vector.y
|
||||
height_data += base_height
|
||||
if use_transparency_model:
|
||||
divisor = 1.0 / math.log(transmittance_1mm / 100.0) # log-base doesn't matter here. Precompute this value for faster computation of each pixel.
|
||||
min_luminance = (transmittance_1mm / 100.0) ** (peak_height - base_height)
|
||||
for (y, x) in numpy.ndindex(height_data.shape):
|
||||
mapped_luminance = min_luminance + (1.0 - min_luminance) * height_data[y, x]
|
||||
height_data[y, x] = base_height + divisor * math.log(mapped_luminance) # use same base as a couple lines above this
|
||||
else:
|
||||
height_data *= scale_vector.y
|
||||
height_data += base_height
|
||||
|
||||
heightmap_face_count = 2 * height_minus_one * width_minus_one
|
||||
total_face_count = heightmap_face_count + (width_minus_one * 2) * (height_minus_one * 2) + 2
|
||||
|
|
|
|||
|
|
@ -34,6 +34,8 @@ class ImageReaderUI(QObject):
|
|||
self.peak_height = 2.5
|
||||
self.smoothing = 1
|
||||
self.lighter_is_higher = False;
|
||||
self.use_transparency_model = True;
|
||||
self.transmittance_1mm = 50.0; # based on pearl PLA
|
||||
|
||||
self._ui_lock = threading.Lock()
|
||||
self._cancelled = False
|
||||
|
|
@ -75,6 +77,7 @@ class ImageReaderUI(QObject):
|
|||
|
||||
self._ui_view.findChild(QObject, "Base_Height").setProperty("text", str(self.base_height))
|
||||
self._ui_view.findChild(QObject, "Peak_Height").setProperty("text", str(self.peak_height))
|
||||
self._ui_view.findChild(QObject, "Transmittance").setProperty("text", str(self.transmittance_1mm))
|
||||
self._ui_view.findChild(QObject, "Smoothing").setProperty("value", self.smoothing)
|
||||
|
||||
def _createConfigUI(self):
|
||||
|
|
@ -144,3 +147,11 @@ class ImageReaderUI(QObject):
|
|||
@pyqtSlot(int)
|
||||
def onImageColorInvertChanged(self, value):
|
||||
self.lighter_is_higher = (value == 1)
|
||||
|
||||
@pyqtSlot(int)
|
||||
def onColorModelChanged(self, value):
|
||||
self.use_transparency_model = (value == 0)
|
||||
|
||||
@pyqtSlot(int)
|
||||
def onTransmittanceChanged(self, value):
|
||||
self.transmittance_1mm = value
|
||||
|
|
|
|||
|
|
@ -3,6 +3,6 @@
|
|||
"author": "Ultimaker B.V.",
|
||||
"version": "1.0.1",
|
||||
"description": "Enables ability to generate printable geometry from 2D image files.",
|
||||
"api": "6.0",
|
||||
"api": "7.0",
|
||||
"i18n-catalog": "cura"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -157,7 +157,7 @@ class LegacyProfileReader(ProfileReader):
|
|||
data = stream.getvalue()
|
||||
|
||||
profile = InstanceContainer(profile_id)
|
||||
profile.deserialize(data) # Also performs the version upgrade.
|
||||
profile.deserialize(data, file_name) # Also performs the version upgrade.
|
||||
profile.setDirty(True)
|
||||
|
||||
#We need to return one extruder stack and one global stack.
|
||||
|
|
|
|||
|
|
@ -3,6 +3,6 @@
|
|||
"author": "Ultimaker B.V.",
|
||||
"version": "1.0.1",
|
||||
"description": "Provides support for importing profiles from legacy Cura versions.",
|
||||
"api": "6.0",
|
||||
"api": "7.0",
|
||||
"i18n-catalog": "cura"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ import configparser # An input for some functions we're testing.
|
|||
import os.path # To find the integration test .ini files.
|
||||
import pytest # To register tests with.
|
||||
import unittest.mock # To mock the application, plug-in and container registry out.
|
||||
import os.path
|
||||
import sys
|
||||
sys.path.append(os.path.join(os.path.dirname(os.path.abspath(__file__)), ".."))
|
||||
|
||||
|
|
@ -16,6 +15,7 @@ import UM.Settings.InstanceContainer # To intercept the serialised data from the
|
|||
|
||||
import LegacyProfileReader as LegacyProfileReaderModule # To get the directory of the module.
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def legacy_profile_reader():
|
||||
try:
|
||||
|
|
@ -162,7 +162,7 @@ def test_read(legacy_profile_reader, file_name):
|
|||
plugin_registry.getPluginPath = unittest.mock.MagicMock(return_value = os.path.dirname(LegacyProfileReaderModule.__file__))
|
||||
|
||||
# Mock out the resulting InstanceContainer so that we can intercept the data before it's passed through the version upgrader.
|
||||
def deserialize(self, data): # Intercepts the serialised data that we'd perform the version upgrade from when deserializing.
|
||||
def deserialize(self, data, filename): # Intercepts the serialised data that we'd perform the version upgrade from when deserializing.
|
||||
global intercepted_data
|
||||
intercepted_data = data
|
||||
|
||||
|
|
@ -192,4 +192,4 @@ def test_read(legacy_profile_reader, file_name):
|
|||
assert parser["metadata"]["type"] == "quality_changes"
|
||||
assert parser["metadata"]["quality_type"] == "normal"
|
||||
assert parser["metadata"]["position"] == "0"
|
||||
assert parser["metadata"]["setting_version"] == "5" # Yes, before we upgraded.
|
||||
assert parser["metadata"]["setting_version"] == "5" # Yes, before we upgraded.
|
||||
|
|
|
|||
|
|
@ -87,9 +87,25 @@ Cura.MachineAction
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
Label
|
||||
{
|
||||
id: machineNameLabel
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: UM.Theme.getSize("default_margin").width
|
||||
text: Cura.MachineManager.activeMachine.name
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
font: UM.Theme.getFont("large_bold")
|
||||
color: UM.Theme.getColor("text")
|
||||
renderType: Text.NativeRendering
|
||||
}
|
||||
|
||||
UM.TabRow
|
||||
{
|
||||
id: tabBar
|
||||
anchors.top: machineNameLabel.bottom
|
||||
anchors.topMargin: UM.Theme.getSize("default_margin").height
|
||||
width: parent.width
|
||||
Repeater
|
||||
{
|
||||
|
|
|
|||
|
|
@ -68,7 +68,7 @@ Item
|
|||
Cura.NumericTextFieldWithUnit // "Nozzle size"
|
||||
{
|
||||
id: extruderNozzleSizeField
|
||||
visible: !Cura.MachineManager.hasVariants
|
||||
visible: !Cura.MachineManager.activeMachine.hasVariants
|
||||
containerStackId: base.extruderStackId
|
||||
settingKey: "machine_nozzle_size"
|
||||
settingStoreIndex: propertyStoreIndex
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ Item
|
|||
property int controlWidth: (columnWidth / 3) | 0
|
||||
property var labelFont: UM.Theme.getFont("default")
|
||||
|
||||
property string machineStackId: Cura.MachineManager.activeMachineId
|
||||
property string machineStackId: Cura.MachineManager.activeMachine.id
|
||||
|
||||
property var forceUpdateFunction: manager.forceUpdate
|
||||
|
||||
|
|
@ -331,6 +331,18 @@ Item
|
|||
onGlobalContainerChanged: extruderCountModel.update()
|
||||
}
|
||||
}
|
||||
|
||||
Cura.SimpleCheckBox // "Shared Heater"
|
||||
{
|
||||
id: sharedHeaterCheckBox
|
||||
containerStackId: machineStackId
|
||||
settingKey: "machine_extruders_share_heater"
|
||||
settingStoreIndex: propertyStoreIndex
|
||||
labelText: catalog.i18nc("@label", "Shared Heater")
|
||||
labelFont: base.labelFont
|
||||
labelWidth: base.labelWidth
|
||||
forceUpdateOnChangeFunction: forceUpdateFunction
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,6 @@
|
|||
"author": "fieldOfView",
|
||||
"version": "1.0.1",
|
||||
"description": "Provides a way to change machine settings (such as build volume, nozzle size, etc.).",
|
||||
"api": "6.0",
|
||||
"api": "7.0",
|
||||
"i18n-catalog": "cura"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
"name": "Model Checker",
|
||||
"author": "Ultimaker B.V.",
|
||||
"version": "1.0.1",
|
||||
"api": "6.0",
|
||||
"api": "7.0",
|
||||
"description": "Checks models and print configuration for possible printing issues and give suggestions.",
|
||||
"i18n-catalog": "cura"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ Rectangle
|
|||
{
|
||||
// Readability:
|
||||
var connectedTypes = [2, 3];
|
||||
var types = Cura.MachineManager.activeMachineConfiguredConnectionTypes
|
||||
var types = Cura.MachineManager.activeMachine.configuredConnectionTypes
|
||||
|
||||
// Check if configured connection types includes either 2 or 3 (LAN or cloud)
|
||||
for (var i = 0; i < types.length; i++)
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
{
|
||||
"name": "Monitor Stage",
|
||||
"author": "Ultimaker B.V.",
|
||||
"version": "1.0.1",
|
||||
"description": "Provides a monitor stage in Cura.",
|
||||
"api": "6.0",
|
||||
"i18n-catalog": "cura"
|
||||
{
|
||||
"name": "Monitor Stage",
|
||||
"author": "Ultimaker B.V.",
|
||||
"version": "1.0.1",
|
||||
"description": "Provides a monitor stage in Cura.",
|
||||
"api": "7.0",
|
||||
"i18n-catalog": "cura"
|
||||
}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
# Copyright (c) 2016 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
from PyQt5.QtCore import QObject, pyqtProperty, pyqtSignal
|
||||
from PyQt5.QtCore import pyqtProperty
|
||||
from UM.FlameProfiler import pyqtSlot
|
||||
|
||||
from UM.Application import Application
|
||||
|
|
@ -13,6 +13,7 @@ import UM.Settings.Models.SettingVisibilityHandler
|
|||
from cura.Settings.ExtruderManager import ExtruderManager #To get global-inherits-stack setting values from different extruders.
|
||||
from cura.Settings.SettingOverrideDecorator import SettingOverrideDecorator
|
||||
|
||||
|
||||
## The per object setting visibility handler ensures that only setting
|
||||
# definitions that have a matching instance Container are returned as visible.
|
||||
class PerObjectSettingVisibilityHandler(UM.Settings.Models.SettingVisibilityHandler.SettingVisibilityHandler):
|
||||
|
|
|
|||
|
|
@ -3,6 +3,6 @@
|
|||
"author": "Ultimaker B.V.",
|
||||
"version": "1.0.1",
|
||||
"description": "Provides the Per Model Settings.",
|
||||
"api": "6.0",
|
||||
"api": "7.0",
|
||||
"i18n-catalog": "cura"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
"name": "Post Processing",
|
||||
"author": "Ultimaker",
|
||||
"version": "2.2.1",
|
||||
"api": "6.0",
|
||||
"api": "7.0",
|
||||
"description": "Extension that allows for user created scripts for post processing",
|
||||
"catalog": "cura"
|
||||
}
|
||||
|
|
@ -0,0 +1,94 @@
|
|||
# Cura PostProcessingPlugin
|
||||
# Author: Mathias Lyngklip Kjeldgaard
|
||||
# Date: July 31, 2019
|
||||
# Modified: November 26, 2019
|
||||
|
||||
# Description: This plugin displayes the remaining time on the LCD of the printer
|
||||
# using the estimated print-time generated by Cura.
|
||||
|
||||
|
||||
|
||||
|
||||
from ..Script import Script
|
||||
|
||||
import re
|
||||
import datetime
|
||||
|
||||
|
||||
class DisplayRemainingTimeOnLCD(Script):
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
|
||||
def getSettingDataString(self):
|
||||
return """{
|
||||
"name":"Display Remaining Time on LCD",
|
||||
"key":"DisplayRemainingTimeOnLCD",
|
||||
"metadata": {},
|
||||
"version": 2,
|
||||
"settings":
|
||||
{
|
||||
"TurnOn":
|
||||
{
|
||||
"label": "Enable",
|
||||
"description": "When enabled, It will write Time Left: HHMMSS on the display. This is updated every layer.",
|
||||
"type": "bool",
|
||||
"default_value": false
|
||||
}
|
||||
}
|
||||
}"""
|
||||
|
||||
def execute(self, data):
|
||||
if self.getSettingValueByKey("TurnOn"):
|
||||
total_time = 0
|
||||
total_time_string = ""
|
||||
for layer in data:
|
||||
layer_index = data.index(layer)
|
||||
lines = layer.split("\n")
|
||||
for line in lines:
|
||||
if line.startswith(";TIME:"):
|
||||
# At this point, we have found a line in the GCODE with ";TIME:"
|
||||
# which is the indication of total_time. Looks like: ";TIME:1337", where
|
||||
# 1337 is the total print time in seconds.
|
||||
line_index = lines.index(line) # We take a hold of that line
|
||||
split_string = re.split(":", line) # Then we split it, so we can get the number
|
||||
|
||||
string_with_numbers = "{}".format(split_string[1]) # Here we insert that number from the
|
||||
# list into a string.
|
||||
total_time = int(string_with_numbers) # Only to contert it to a int.
|
||||
|
||||
m, s = divmod(total_time, 60) # Math to calculate
|
||||
h, m = divmod(m, 60) # hours, minutes and seconds.
|
||||
total_time_string = "{:d}h{:02d}m{:02d}s".format(h, m, s) # Now we put it into the string
|
||||
lines[line_index] = "M117 Time Left {}".format(total_time_string) # And print that string instead of the original one
|
||||
|
||||
|
||||
|
||||
|
||||
elif line.startswith(";TIME_ELAPSED:"):
|
||||
|
||||
# As we didnt find the total time (";TIME:"), we have found a elapsed time mark
|
||||
# This time represents the time the printer have printed. So with some math;
|
||||
# totalTime - printTime = RemainingTime.
|
||||
line_index = lines.index(line) # We get a hold of the line
|
||||
list_split = re.split(":", line) # Again, we split at ":" so we can get the number
|
||||
string_with_numbers = "{}".format(list_split[1]) # Then we put that number from the list, into a string
|
||||
|
||||
current_time = float(string_with_numbers) # This time we convert to a float, as the line looks something like:
|
||||
# ;TIME_ELAPSED:1234.6789
|
||||
# which is total time in seconds
|
||||
|
||||
time_left = total_time - current_time # Here we calculate remaining time
|
||||
m1, s1 = divmod(time_left, 60) # And some math to get the total time in seconds into
|
||||
h1, m1 = divmod(m1, 60) # the right format. (HH,MM,SS)
|
||||
current_time_string = "{:d}h{:2d}m{:2d}s".format(int(h1), int(m1), int(s1)) # Here we create the string holding our time
|
||||
lines[line_index] = "M117 Time Left {}".format(current_time_string) # And now insert that into the GCODE
|
||||
|
||||
|
||||
# Here we are OUT of the second for-loop
|
||||
# Which means we have found and replaces all the occurences.
|
||||
# Which also means we are ready to join the lines for that section of the GCODE file.
|
||||
final_lines = "\n".join(lines)
|
||||
data[layer_index] = final_lines
|
||||
return data
|
||||
|
|
@ -219,7 +219,7 @@ class PauseAtHeight(Script):
|
|||
|
||||
current_height = current_z - layer_0_z
|
||||
if current_height < pause_height:
|
||||
break # Try the next layer.
|
||||
continue # Scan the enitre layer, z-changes are not always on the same/first line.
|
||||
|
||||
# Pause at layer
|
||||
else:
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
{
|
||||
"name": "Prepare Stage",
|
||||
"author": "Ultimaker B.V.",
|
||||
"version": "1.0.1",
|
||||
"description": "Provides a prepare stage in Cura.",
|
||||
"api": "6.0",
|
||||
"i18n-catalog": "cura"
|
||||
{
|
||||
"name": "Prepare Stage",
|
||||
"author": "Ultimaker B.V.",
|
||||
"version": "1.0.1",
|
||||
"description": "Provides a prepare stage in Cura.",
|
||||
"api": "7.0",
|
||||
"i18n-catalog": "cura"
|
||||
}
|
||||
|
|
@ -11,12 +11,34 @@ import Cura 1.0 as Cura
|
|||
|
||||
Item
|
||||
{
|
||||
|
||||
// An Item whose bounds are guaranteed to be safe for overlays to be placed.
|
||||
// Defaults to parent, ie. the entire available area
|
||||
property var safeArea: parent
|
||||
|
||||
// Subtract the actionPanel from the safe area. This way the view won't draw interface elements under/over it
|
||||
Item
|
||||
{
|
||||
id: childSafeArea
|
||||
x: safeArea.x - parent.x
|
||||
y: safeArea.y - parent.y
|
||||
width: actionPanelWidget.x - x
|
||||
height: actionPanelWidget.y - y
|
||||
}
|
||||
|
||||
Loader
|
||||
{
|
||||
id: previewMain
|
||||
anchors.fill: parent
|
||||
|
||||
source: UM.Controller.activeView != null && UM.Controller.activeView.mainComponent != null ? UM.Controller.activeView.mainComponent : ""
|
||||
|
||||
onLoaded:
|
||||
{
|
||||
if (previewMain.item.safeArea !== undefined){
|
||||
previewMain.item.safeArea = Qt.binding(function() { return childSafeArea });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Cura.ActionPanelWidget
|
||||
|
|
|
|||
|
|
@ -3,6 +3,6 @@
|
|||
"author": "Ultimaker B.V.",
|
||||
"version": "1.0.1",
|
||||
"description": "Provides a preview stage in Cura.",
|
||||
"api": "6.0",
|
||||
"api": "7.0",
|
||||
"i18n-catalog": "cura"
|
||||
}
|
||||
|
|
@ -48,9 +48,13 @@ class WindowsRemovableDrivePlugin(RemovableDrivePlugin.RemovableDrivePlugin):
|
|||
drives = {}
|
||||
|
||||
bitmask = ctypes.windll.kernel32.GetLogicalDrives()
|
||||
# Check possible drive letters, from A to Z
|
||||
# Check possible drive letters, from C to Z
|
||||
# Note: using ascii_uppercase because we do not want this to change with locale!
|
||||
for letter in string.ascii_uppercase:
|
||||
# Skip A and B, since those drives are typically reserved for floppy disks.
|
||||
# Those drives can theoretically be reassigned but it's safer to not check them for removable drives.
|
||||
# Windows will also behave weirdly even with some of its internal functions if you do this (e.g. search indexing doesn't search it).
|
||||
# Users that have removable drives in A or B will just have to save to file and select the drive there.
|
||||
for letter in string.ascii_uppercase[2:]:
|
||||
drive = "{0}:/".format(letter)
|
||||
|
||||
# Do we really want to skip A and B?
|
||||
|
|
|
|||
|
|
@ -3,6 +3,6 @@
|
|||
"author": "Ultimaker B.V.",
|
||||
"description": "Provides removable drive hotplugging and writing support.",
|
||||
"version": "1.0.1",
|
||||
"api": "6.0",
|
||||
"api": "7.0",
|
||||
"i18n-catalog": "cura"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -155,25 +155,19 @@ Item
|
|||
}
|
||||
|
||||
onPositionChanged: parent.onHandleDragged()
|
||||
onPressed: sliderRoot.setActiveHandle(rangeHandle)
|
||||
onPressed:
|
||||
{
|
||||
sliderRoot.setActiveHandle(rangeHandle)
|
||||
sliderRoot.forceActiveFocus()
|
||||
}
|
||||
}
|
||||
|
||||
SimulationSliderLabel
|
||||
{
|
||||
id: rangleHandleLabel
|
||||
}
|
||||
|
||||
height: sliderRoot.handleSize + UM.Theme.getSize("default_margin").height
|
||||
x: parent.x - width - UM.Theme.getSize("default_margin").width
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
target: Qt.point(sliderRoot.width, y + height / 2)
|
||||
visible: sliderRoot.activeHandle == parent
|
||||
|
||||
// custom properties
|
||||
maximumValue: sliderRoot.maximumValue
|
||||
value: sliderRoot.upperValue
|
||||
busy: UM.SimulationView.busy
|
||||
setValue: rangeHandle.setValueManually // connect callback functions
|
||||
}
|
||||
onHeightChanged : {
|
||||
// After a height change, the pixel-position of the handles is out of sync with the property value
|
||||
setLowerValue(lowerValue)
|
||||
setUpperValue(upperValue)
|
||||
}
|
||||
|
||||
// Upper handle
|
||||
|
|
@ -270,11 +264,12 @@ Item
|
|||
{
|
||||
id: upperHandleLabel
|
||||
|
||||
height: sliderRoot.handleSize + UM.Theme.getSize("default_margin").height
|
||||
x: parent.x - parent.width - width
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
target: Qt.point(sliderRoot.width, y + height / 2)
|
||||
visible: sliderRoot.activeHandle == parent
|
||||
height: sliderRoot.handleSize
|
||||
anchors.bottom: parent.top
|
||||
anchors.bottomMargin: UM.Theme.getSize("narrow_margin").height
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
target: Qt.point(parent.width / 2, parent.top)
|
||||
visible: sliderRoot.activeHandle == parent || sliderRoot.activeHandle == rangeHandle
|
||||
|
||||
// custom properties
|
||||
maximumValue: sliderRoot.maximumValue
|
||||
|
|
@ -333,7 +328,6 @@ Item
|
|||
// set the slider position based on the lower value
|
||||
function setValue(value)
|
||||
{
|
||||
|
||||
// Normalize values between range, since using arrow keys will create out-of-the-range values
|
||||
value = sliderRoot.normalizeValue(value)
|
||||
|
||||
|
|
@ -380,11 +374,12 @@ Item
|
|||
{
|
||||
id: lowerHandleLabel
|
||||
|
||||
height: sliderRoot.handleSize + UM.Theme.getSize("default_margin").height
|
||||
x: parent.x - parent.width - width
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
target: Qt.point(sliderRoot.width + width, y + height / 2)
|
||||
visible: sliderRoot.activeHandle == parent
|
||||
height: sliderRoot.handleSize
|
||||
anchors.top: parent.bottom
|
||||
anchors.topMargin: UM.Theme.getSize("narrow_margin").height
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
target: Qt.point(parent.width / 2, parent.bottom)
|
||||
visible: sliderRoot.activeHandle == parent || sliderRoot.activeHandle == rangeHandle
|
||||
|
||||
// custom properties
|
||||
maximumValue: sliderRoot.maximumValue
|
||||
|
|
@ -393,4 +388,4 @@ Item
|
|||
setValue: lowerHandle.setValueManually // connect callback functions
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@
|
|||
|
||||
from UM.Application import Application
|
||||
from UM.Math.Color import Color
|
||||
from UM.Math.Vector import Vector
|
||||
from UM.PluginRegistry import PluginRegistry
|
||||
from UM.Scene.SceneNode import SceneNode
|
||||
from UM.View.GL.OpenGL import OpenGL
|
||||
|
|
|
|||
|
|
@ -56,6 +56,11 @@ Item
|
|||
return Math.min(Math.max(value, sliderRoot.minimumValue), sliderRoot.maximumValue)
|
||||
}
|
||||
|
||||
onWidthChanged : {
|
||||
// After a width change, the pixel-position of the handle is out of sync with the property value
|
||||
setHandleValue(handleValue)
|
||||
}
|
||||
|
||||
// slider track
|
||||
Rectangle
|
||||
{
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue