mirror of
https://github.com/Ultimaker/Cura.git
synced 2025-12-11 16:00:47 -07:00
Merge remote-tracking branch 'Ultimaker/master' into Felix_Profile
This commit is contained in:
commit
17c08eacf4
1203 changed files with 305148 additions and 137799 deletions
21
.github/ISSUE_TEMPLATE.md
vendored
21
.github/ISSUE_TEMPLATE.md
vendored
|
|
@ -3,7 +3,7 @@ The following template is useful for filing new issues. Processing an issue will
|
||||||
|
|
||||||
Before filing, PLEASE check if the issue already exists (either open or closed) by using the search bar on the issues page. If it does, comment there. Even if it's closed, we can reopen it based on your comment.
|
Before filing, PLEASE check if the issue already exists (either open or closed) by using the search bar on the issues page. If it does, comment there. Even if it's closed, we can reopen it based on your comment.
|
||||||
|
|
||||||
Also, please note the application version in the title of the issue. For example: "[3.2.1] Cannot connect to 3rd-party printer". Please do not write thigns like "Request:" or "[BUG]" in the title; this is what labels are for.
|
Also, please note the application version in the title of the issue. For example: "[3.2.1] Cannot connect to 3rd-party printer". Please do not write things like "Request:" or "[BUG]" in the title; this is what labels are for.
|
||||||
|
|
||||||
It is also helpful to attach a project (.3mf or .curaproject) file and Cura log file so we can debug issues quicker.
|
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.
|
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.
|
||||||
|
|
@ -15,28 +15,19 @@ Thank you for using Cura!
|
||||||
<!-- The version of the application this issue occurs with -->
|
<!-- The version of the application this issue occurs with -->
|
||||||
|
|
||||||
**Platform**
|
**Platform**
|
||||||
<!-- Information about the platform the issue occurs on -->
|
<!-- Information about the operating system the issue occurs on -->
|
||||||
|
|
||||||
**Qt**
|
|
||||||
<!-- The version of Qt used (not necessary if you're using the version from Ultimaker's website) -->
|
|
||||||
|
|
||||||
**PyQt**
|
|
||||||
<!-- The version of PyQt used (not necessary if you're using the version from Ultimaker's website) -->
|
|
||||||
|
|
||||||
**Display Driver**
|
|
||||||
<!-- Video driver name and version -->
|
|
||||||
|
|
||||||
**Printer**
|
**Printer**
|
||||||
<!-- Which printer was selected in Cura. Please attach project file as .curaproject.3mf.zip -->
|
<!-- Which printer was selected in Cura. Please attach project file as .curaproject.3mf.zip -->
|
||||||
|
|
||||||
**Steps to Reproduce**
|
**Steps to Reproduce**
|
||||||
<!-- Add the steps needed that lead up to the issue (replace this text) -->
|
<!-- Add the steps needed that lead up to the issue -->
|
||||||
|
|
||||||
**Actual Results**
|
**Actual Results**
|
||||||
<!-- What happens after the above steps have been followed (replace this text) -->
|
<!-- What happens after the above steps have been followed -->
|
||||||
|
|
||||||
**Expected results**
|
**Expected results**
|
||||||
<!-- What should happen after the above steps have been followed (replace this text) -->
|
<!-- What should happen after the above steps have been followed -->
|
||||||
|
|
||||||
**Additional Information**
|
**Additional Information**
|
||||||
<!-- Extra information relevant to the issue, like screenshots (replace this text) -->
|
<!-- Extra information relevant to the issue, like screenshots -->
|
||||||
|
|
|
||||||
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -14,6 +14,7 @@ CuraEngine.exe
|
||||||
LC_MESSAGES
|
LC_MESSAGES
|
||||||
.cache
|
.cache
|
||||||
*.qmlc
|
*.qmlc
|
||||||
|
.mypy_cache
|
||||||
|
|
||||||
#MacOS
|
#MacOS
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|
@ -38,6 +39,8 @@ plugins/cura-god-mode-plugin
|
||||||
plugins/cura-siemensnx-plugin
|
plugins/cura-siemensnx-plugin
|
||||||
plugins/CuraBlenderPlugin
|
plugins/CuraBlenderPlugin
|
||||||
plugins/CuraCloudPlugin
|
plugins/CuraCloudPlugin
|
||||||
|
plugins/CuraDrivePlugin
|
||||||
|
plugins/CuraDrive
|
||||||
plugins/CuraLiveScriptingPlugin
|
plugins/CuraLiveScriptingPlugin
|
||||||
plugins/CuraOpenSCADPlugin
|
plugins/CuraOpenSCADPlugin
|
||||||
plugins/CuraPrintProfileCreator
|
plugins/CuraPrintProfileCreator
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,10 @@ endif()
|
||||||
|
|
||||||
set(CURA_VERSION "master" CACHE STRING "Version name of Cura")
|
set(CURA_VERSION "master" CACHE STRING "Version name of Cura")
|
||||||
set(CURA_BUILDTYPE "" CACHE STRING "Build type of Cura, eg. 'PPA'")
|
set(CURA_BUILDTYPE "" CACHE STRING "Build type of Cura, eg. 'PPA'")
|
||||||
|
set(CURA_SDK_VERSION "" CACHE STRING "SDK version of Cura")
|
||||||
|
set(CURA_CLOUD_API_ROOT "" CACHE STRING "Alternative Cura cloud API root")
|
||||||
|
set(CURA_CLOUD_API_VERSION "" CACHE STRING "Alternative Cura cloud API version")
|
||||||
|
|
||||||
configure_file(${CMAKE_SOURCE_DIR}/cura.desktop.in ${CMAKE_BINARY_DIR}/cura.desktop @ONLY)
|
configure_file(${CMAKE_SOURCE_DIR}/cura.desktop.in ${CMAKE_BINARY_DIR}/cura.desktop @ONLY)
|
||||||
configure_file(cura/CuraVersion.py.in CuraVersion.py @ONLY)
|
configure_file(cura/CuraVersion.py.in CuraVersion.py @ONLY)
|
||||||
|
|
||||||
|
|
|
||||||
1
Jenkinsfile
vendored
1
Jenkinsfile
vendored
|
|
@ -1,5 +1,6 @@
|
||||||
parallel_nodes(['linux && cura', 'windows && cura']) {
|
parallel_nodes(['linux && cura', 'windows && cura']) {
|
||||||
timeout(time: 2, unit: "HOURS") {
|
timeout(time: 2, unit: "HOURS") {
|
||||||
|
|
||||||
// Prepare building
|
// Prepare building
|
||||||
stage('Prepare') {
|
stage('Prepare') {
|
||||||
// Ensure we start with a clean build directory.
|
// Ensure we start with a clean build directory.
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
# Copyright (c) 2017 Ultimaker B.V.
|
# Copyright (c) 2018 Ultimaker B.V.
|
||||||
# Cura is released under the terms of the LGPLv3 or higher.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
enable_testing()
|
enable_testing()
|
||||||
|
|
@ -53,3 +53,9 @@ foreach(_plugin ${_plugins})
|
||||||
cura_add_test(NAME pytest-${_plugin_name} DIRECTORY ${_plugin_directory} PYTHONPATH "${_plugin_directory}|${CMAKE_SOURCE_DIR}|${URANIUM_DIR}")
|
cura_add_test(NAME pytest-${_plugin_name} DIRECTORY ${_plugin_directory} PYTHONPATH "${_plugin_directory}|${CMAKE_SOURCE_DIR}|${URANIUM_DIR}")
|
||||||
endif()
|
endif()
|
||||||
endforeach()
|
endforeach()
|
||||||
|
|
||||||
|
#Add code style test.
|
||||||
|
add_test(
|
||||||
|
NAME "code-style"
|
||||||
|
COMMAND ${PYTHON_EXECUTABLE} run_mypy.py WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
|
||||||
|
)
|
||||||
|
|
@ -26,6 +26,6 @@
|
||||||
<screenshots>
|
<screenshots>
|
||||||
<screenshot type="default" width="1280" height="720">http://software.ultimaker.com/Cura.png</screenshot>
|
<screenshot type="default" width="1280" height="720">http://software.ultimaker.com/Cura.png</screenshot>
|
||||||
</screenshots>
|
</screenshots>
|
||||||
<url type="homepage">https://ultimaker.com/en/products/cura-software?utm_source=cura&utm_medium=software&utm_campaign=resources</url>
|
<url type="homepage">https://ultimaker.com/en/products/cura-software?utm_source=cura&utm_medium=software&utm_campaign=resources</url>
|
||||||
<translation type="gettext">Cura</translation>
|
<translation type="gettext">Cura</translation>
|
||||||
</component>
|
</component>
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,13 @@
|
||||||
[Desktop Entry]
|
[Desktop Entry]
|
||||||
Name=Ultimaker Cura
|
Name=Ultimaker Cura
|
||||||
Name[de]=Ultimaker Cura
|
Name[de]=Ultimaker Cura
|
||||||
|
Name[nl]=Ultimaker Cura
|
||||||
GenericName=3D Printing Software
|
GenericName=3D Printing Software
|
||||||
GenericName[de]=3D-Druck-Software
|
GenericName[de]=3D-Druck-Software
|
||||||
|
GenericName[nl]=3D-printsoftware
|
||||||
Comment=Cura converts 3D models into paths for a 3D printer. It prepares your print for maximum accuracy, minimum printing time and good reliability with many extra features that make your print come out great.
|
Comment=Cura converts 3D models into paths for a 3D printer. It prepares your print for maximum accuracy, minimum printing time and good reliability with many extra features that make your print come out great.
|
||||||
Comment[de]=Cura wandelt 3D-Modelle in Pfade für einen 3D-Drucker um. Es bereitet Ihren Druck für maximale Genauigkeit, minimale Druckzeit und guter Zuverlässigkeit mit vielen zusätzlichen Funktionen vor, damit Ihr Druck großartig wird.
|
Comment[de]=Cura wandelt 3D-Modelle in Pfade für einen 3D-Drucker um. Es bereitet Ihren Druck für maximale Genauigkeit, minimale Druckzeit und guter Zuverlässigkeit mit vielen zusätzlichen Funktionen vor, damit Ihr Druck großartig wird.
|
||||||
|
Comment[nl]=Cura converteert 3D-modellen naar paden voor een 3D printer. Het bereidt je print voor om zeer precies, snel en betrouwbaar te kunnen printen, met veel extra functionaliteit om je print er goed uit te laten komen.
|
||||||
Exec=@CMAKE_INSTALL_FULL_BINDIR@/cura %F
|
Exec=@CMAKE_INSTALL_FULL_BINDIR@/cura %F
|
||||||
TryExec=@CMAKE_INSTALL_FULL_BINDIR@/cura
|
TryExec=@CMAKE_INSTALL_FULL_BINDIR@/cura
|
||||||
Icon=cura-icon
|
Icon=cura-icon
|
||||||
|
|
@ -12,4 +15,4 @@ Terminal=false
|
||||||
Type=Application
|
Type=Application
|
||||||
MimeType=application/sla;application/vnd.ms-3mfdocument;application/prs.wavefront-obj;image/bmp;image/gif;image/jpeg;image/png;model/x3d+xml;
|
MimeType=application/sla;application/vnd.ms-3mfdocument;application/prs.wavefront-obj;image/bmp;image/gif;image/jpeg;image/png;model/x3d+xml;
|
||||||
Categories=Graphics;
|
Categories=Graphics;
|
||||||
Keywords=3D;Printing;
|
Keywords=3D;Printing;Slicer;
|
||||||
|
|
|
||||||
31
cura/API/Backups.py
Normal file
31
cura/API/Backups.py
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
# Copyright (c) 2018 Ultimaker B.V.
|
||||||
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
from typing import Tuple, Optional
|
||||||
|
|
||||||
|
from cura.Backups.BackupsManager import BackupsManager
|
||||||
|
|
||||||
|
|
||||||
|
## The back-ups API provides a version-proof bridge between Cura's
|
||||||
|
# BackupManager and plug-ins that hook into it.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# ``from cura.API import CuraAPI
|
||||||
|
# api = CuraAPI()
|
||||||
|
# api.backups.createBackup()
|
||||||
|
# api.backups.restoreBackup(my_zip_file, {"cura_release": "3.1"})``
|
||||||
|
|
||||||
|
class Backups:
|
||||||
|
manager = BackupsManager() # Re-used instance of the backups manager.
|
||||||
|
|
||||||
|
## Create a new back-up using the BackupsManager.
|
||||||
|
# \return Tuple containing a ZIP file with the back-up data and a dict
|
||||||
|
# with metadata about the back-up.
|
||||||
|
def createBackup(self) -> Tuple[Optional[bytes], Optional[dict]]:
|
||||||
|
return self.manager.createBackup()
|
||||||
|
|
||||||
|
## Restore a back-up using the BackupsManager.
|
||||||
|
# \param zip_file A ZIP file containing the actual back-up data.
|
||||||
|
# \param meta_data Some metadata needed for restoring a back-up, like the
|
||||||
|
# Cura version number.
|
||||||
|
def restoreBackup(self, zip_file: bytes, meta_data: dict) -> None:
|
||||||
|
return self.manager.restoreBackup(zip_file, meta_data)
|
||||||
33
cura/API/Interface/Settings.py
Normal file
33
cura/API/Interface/Settings.py
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
# Copyright (c) 2018 Ultimaker B.V.
|
||||||
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
|
from cura.CuraApplication import CuraApplication
|
||||||
|
|
||||||
|
## The Interface.Settings API provides a version-proof bridge between Cura's
|
||||||
|
# (currently) sidebar UI and plug-ins that hook into it.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# ``from cura.API import CuraAPI
|
||||||
|
# api = CuraAPI()
|
||||||
|
# api.interface.settings.getContextMenuItems()
|
||||||
|
# data = {
|
||||||
|
# "name": "My Plugin Action",
|
||||||
|
# "iconName": "my-plugin-icon",
|
||||||
|
# "actions": my_menu_actions,
|
||||||
|
# "menu_item": MyPluginAction(self)
|
||||||
|
# }
|
||||||
|
# api.interface.settings.addContextMenuItem(data)``
|
||||||
|
|
||||||
|
class Settings:
|
||||||
|
# Re-used instance of Cura:
|
||||||
|
application = CuraApplication.getInstance() # type: CuraApplication
|
||||||
|
|
||||||
|
## Add items to the sidebar context menu.
|
||||||
|
# \param menu_item dict containing the menu item to add.
|
||||||
|
def addContextMenuItem(self, menu_item: dict) -> None:
|
||||||
|
self.application.addSidebarCustomMenuItem(menu_item)
|
||||||
|
|
||||||
|
## Get all custom items currently added to the sidebar context menu.
|
||||||
|
# \return List containing all custom context menu items.
|
||||||
|
def getContextMenuItems(self) -> list:
|
||||||
|
return self.application.getSidebarCustomMenuItems()
|
||||||
24
cura/API/Interface/__init__.py
Normal file
24
cura/API/Interface/__init__.py
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
# Copyright (c) 2018 Ultimaker B.V.
|
||||||
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
|
from UM.PluginRegistry import PluginRegistry
|
||||||
|
from cura.API.Interface.Settings import Settings
|
||||||
|
|
||||||
|
## The Interface class serves as a common root for the specific API
|
||||||
|
# methods for each interface element.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# ``from cura.API import CuraAPI
|
||||||
|
# api = CuraAPI()
|
||||||
|
# api.interface.settings.addContextMenuItem()
|
||||||
|
# api.interface.viewport.addOverlay() # Not implemented, just a hypothetical
|
||||||
|
# api.interface.toolbar.getToolButtonCount() # Not implemented, just a hypothetical
|
||||||
|
# # etc.``
|
||||||
|
|
||||||
|
class Interface:
|
||||||
|
|
||||||
|
# For now we use the same API version to be consistent.
|
||||||
|
VERSION = PluginRegistry.APIVersion
|
||||||
|
|
||||||
|
# API methods specific to the settings portion of the UI
|
||||||
|
settings = Settings()
|
||||||
23
cura/API/__init__.py
Normal file
23
cura/API/__init__.py
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
# Copyright (c) 2018 Ultimaker B.V.
|
||||||
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
from UM.PluginRegistry import PluginRegistry
|
||||||
|
from cura.API.Backups import Backups
|
||||||
|
from cura.API.Interface import Interface
|
||||||
|
|
||||||
|
## The official Cura API that plug-ins can use to interact with Cura.
|
||||||
|
#
|
||||||
|
# Python does not technically prevent talking to other classes as well, but
|
||||||
|
# this API provides a version-safe interface with proper deprecation warnings
|
||||||
|
# etc. Usage of any other methods than the ones provided in this API can cause
|
||||||
|
# plug-ins to be unstable.
|
||||||
|
|
||||||
|
class CuraAPI:
|
||||||
|
|
||||||
|
# For now we use the same API version to be consistent.
|
||||||
|
VERSION = PluginRegistry.APIVersion
|
||||||
|
|
||||||
|
# Backups API
|
||||||
|
backups = Backups()
|
||||||
|
|
||||||
|
# Interface API
|
||||||
|
interface = Interface()
|
||||||
|
|
@ -1,6 +1,12 @@
|
||||||
|
# Copyright (c) 2018 Ultimaker B.V.
|
||||||
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
from typing import List
|
||||||
|
|
||||||
from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
|
from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
|
||||||
from UM.Logger import Logger
|
from UM.Logger import Logger
|
||||||
|
from UM.Math.Polygon import Polygon
|
||||||
from UM.Math.Vector import Vector
|
from UM.Math.Vector import Vector
|
||||||
|
from UM.Scene.SceneNode import SceneNode
|
||||||
from cura.Arranging.ShapeArray import ShapeArray
|
from cura.Arranging.ShapeArray import ShapeArray
|
||||||
from cura.Scene import ZOffsetDecorator
|
from cura.Scene import ZOffsetDecorator
|
||||||
|
|
||||||
|
|
@ -18,17 +24,20 @@ LocationSuggestion = namedtuple("LocationSuggestion", ["x", "y", "penalty_points
|
||||||
# good locations for objects that you try to put on a build place.
|
# good locations for objects that you try to put on a build place.
|
||||||
# Different priority schemes can be defined so it alters the behavior while using
|
# Different priority schemes can be defined so it alters the behavior while using
|
||||||
# the same logic.
|
# the same logic.
|
||||||
|
#
|
||||||
|
# Note: Make sure the scale is the same between ShapeArray objects and the Arrange instance.
|
||||||
class Arrange:
|
class Arrange:
|
||||||
build_volume = None
|
build_volume = None
|
||||||
|
|
||||||
def __init__(self, x, y, offset_x, offset_y, scale= 1.0):
|
def __init__(self, x, y, offset_x, offset_y, scale= 0.5):
|
||||||
self.shape = (y, x)
|
|
||||||
self._priority = numpy.zeros((x, y), dtype=numpy.int32)
|
|
||||||
self._priority_unique_values = []
|
|
||||||
self._occupied = numpy.zeros((x, y), dtype=numpy.int32)
|
|
||||||
self._scale = scale # convert input coordinates to arrange coordinates
|
self._scale = scale # convert input coordinates to arrange coordinates
|
||||||
self._offset_x = offset_x
|
world_x, world_y = int(x * self._scale), int(y * self._scale)
|
||||||
self._offset_y = offset_y
|
self._shape = (world_y, world_x)
|
||||||
|
self._priority = numpy.zeros((world_y, world_x), dtype=numpy.int32) # beware: these are indexed (y, x)
|
||||||
|
self._priority_unique_values = []
|
||||||
|
self._occupied = numpy.zeros((world_y, world_x), dtype=numpy.int32) # beware: these are indexed (y, x)
|
||||||
|
self._offset_x = int(offset_x * self._scale)
|
||||||
|
self._offset_y = int(offset_y * self._scale)
|
||||||
self._last_priority = 0
|
self._last_priority = 0
|
||||||
self._is_empty = True
|
self._is_empty = True
|
||||||
|
|
||||||
|
|
@ -39,7 +48,7 @@ class Arrange:
|
||||||
# \param scene_root Root for finding all scene nodes
|
# \param scene_root Root for finding all scene nodes
|
||||||
# \param fixed_nodes Scene nodes to be placed
|
# \param fixed_nodes Scene nodes to be placed
|
||||||
@classmethod
|
@classmethod
|
||||||
def create(cls, scene_root = None, fixed_nodes = None, scale = 0.5, x = 220, y = 220):
|
def create(cls, scene_root = None, fixed_nodes = None, scale = 0.5, x = 350, y = 250, min_offset = 8):
|
||||||
arranger = Arrange(x, y, x // 2, y // 2, scale = scale)
|
arranger = Arrange(x, y, x // 2, y // 2, scale = scale)
|
||||||
arranger.centerFirst()
|
arranger.centerFirst()
|
||||||
|
|
||||||
|
|
@ -52,59 +61,64 @@ class Arrange:
|
||||||
|
|
||||||
# Place all objects fixed nodes
|
# Place all objects fixed nodes
|
||||||
for fixed_node in fixed_nodes:
|
for fixed_node in fixed_nodes:
|
||||||
vertices = fixed_node.callDecoration("getConvexHull")
|
vertices = fixed_node.callDecoration("getConvexHullHead") or fixed_node.callDecoration("getConvexHull")
|
||||||
if not vertices:
|
if not vertices:
|
||||||
continue
|
continue
|
||||||
|
vertices = vertices.getMinkowskiHull(Polygon.approximatedCircle(min_offset))
|
||||||
points = copy.deepcopy(vertices._points)
|
points = copy.deepcopy(vertices._points)
|
||||||
shape_arr = ShapeArray.fromPolygon(points, scale = scale)
|
shape_arr = ShapeArray.fromPolygon(points, scale = scale)
|
||||||
arranger.place(0, 0, shape_arr)
|
arranger.place(0, 0, shape_arr)
|
||||||
|
|
||||||
# If a build volume was set, add the disallowed areas
|
# If a build volume was set, add the disallowed areas
|
||||||
if Arrange.build_volume:
|
if Arrange.build_volume:
|
||||||
disallowed_areas = Arrange.build_volume.getDisallowedAreas()
|
disallowed_areas = Arrange.build_volume.getDisallowedAreasNoBrim()
|
||||||
for area in disallowed_areas:
|
for area in disallowed_areas:
|
||||||
points = copy.deepcopy(area._points)
|
points = copy.deepcopy(area._points)
|
||||||
shape_arr = ShapeArray.fromPolygon(points, scale = scale)
|
shape_arr = ShapeArray.fromPolygon(points, scale = scale)
|
||||||
arranger.place(0, 0, shape_arr, update_empty = False)
|
arranger.place(0, 0, shape_arr, update_empty = False)
|
||||||
return arranger
|
return arranger
|
||||||
|
|
||||||
|
## This resets the optimization for finding location based on size
|
||||||
|
def resetLastPriority(self):
|
||||||
|
self._last_priority = 0
|
||||||
|
|
||||||
## Find placement for a node (using offset shape) and place it (using hull shape)
|
## Find placement for a node (using offset shape) and place it (using hull shape)
|
||||||
# return the nodes that should be placed
|
# return the nodes that should be placed
|
||||||
# \param node
|
# \param node
|
||||||
# \param offset_shape_arr ShapeArray with offset, used to find location
|
# \param offset_shape_arr ShapeArray with offset, for placing the shape
|
||||||
# \param hull_shape_arr ShapeArray without offset, for placing the shape
|
# \param hull_shape_arr ShapeArray without offset, used to find location
|
||||||
def findNodePlacement(self, node, offset_shape_arr, hull_shape_arr, step = 1):
|
def findNodePlacement(self, node: SceneNode, offset_shape_arr: ShapeArray, hull_shape_arr: ShapeArray, step = 1):
|
||||||
new_node = copy.deepcopy(node)
|
|
||||||
best_spot = self.bestSpot(
|
best_spot = self.bestSpot(
|
||||||
offset_shape_arr, start_prio = self._last_priority, step = step)
|
hull_shape_arr, start_prio = self._last_priority, step = step)
|
||||||
x, y = best_spot.x, best_spot.y
|
x, y = best_spot.x, best_spot.y
|
||||||
|
|
||||||
# Save the last priority.
|
# Save the last priority.
|
||||||
self._last_priority = best_spot.priority
|
self._last_priority = best_spot.priority
|
||||||
|
|
||||||
# Ensure that the object is above the build platform
|
# Ensure that the object is above the build platform
|
||||||
new_node.removeDecorator(ZOffsetDecorator.ZOffsetDecorator)
|
node.removeDecorator(ZOffsetDecorator.ZOffsetDecorator)
|
||||||
if new_node.getBoundingBox():
|
bbox = node.getBoundingBox()
|
||||||
center_y = new_node.getWorldPosition().y - new_node.getBoundingBox().bottom
|
if bbox:
|
||||||
|
center_y = node.getWorldPosition().y - bbox.bottom
|
||||||
else:
|
else:
|
||||||
center_y = 0
|
center_y = 0
|
||||||
|
|
||||||
if x is not None: # We could find a place
|
if x is not None: # We could find a place
|
||||||
new_node.setPosition(Vector(x, center_y, y))
|
node.setPosition(Vector(x, center_y, y))
|
||||||
found_spot = True
|
found_spot = True
|
||||||
self.place(x, y, hull_shape_arr) # place the object in arranger
|
self.place(x, y, offset_shape_arr) # place the object in arranger
|
||||||
else:
|
else:
|
||||||
Logger.log("d", "Could not find spot!"),
|
Logger.log("d", "Could not find spot!"),
|
||||||
found_spot = False
|
found_spot = False
|
||||||
new_node.setPosition(Vector(200, center_y, 100))
|
node.setPosition(Vector(200, center_y, 100))
|
||||||
return new_node, found_spot
|
return found_spot
|
||||||
|
|
||||||
## Fill priority, center is best. Lower value is better
|
## Fill priority, center is best. Lower value is better
|
||||||
# This is a strategy for the arranger.
|
# This is a strategy for the arranger.
|
||||||
def centerFirst(self):
|
def centerFirst(self):
|
||||||
# Square distance: creates a more round shape
|
# Square distance: creates a more round shape
|
||||||
self._priority = numpy.fromfunction(
|
self._priority = numpy.fromfunction(
|
||||||
lambda i, j: (self._offset_x - i) ** 2 + (self._offset_y - j) ** 2, self.shape, dtype=numpy.int32)
|
lambda j, i: (self._offset_x - i) ** 2 + (self._offset_y - j) ** 2, self._shape, dtype=numpy.int32)
|
||||||
self._priority_unique_values = numpy.unique(self._priority)
|
self._priority_unique_values = numpy.unique(self._priority)
|
||||||
self._priority_unique_values.sort()
|
self._priority_unique_values.sort()
|
||||||
|
|
||||||
|
|
@ -112,7 +126,7 @@ class Arrange:
|
||||||
# This is a strategy for the arranger.
|
# This is a strategy for the arranger.
|
||||||
def backFirst(self):
|
def backFirst(self):
|
||||||
self._priority = numpy.fromfunction(
|
self._priority = numpy.fromfunction(
|
||||||
lambda i, j: 10 * j + abs(self._offset_x - i), self.shape, dtype=numpy.int32)
|
lambda j, i: 10 * j + abs(self._offset_x - i), self._shape, dtype=numpy.int32)
|
||||||
self._priority_unique_values = numpy.unique(self._priority)
|
self._priority_unique_values = numpy.unique(self._priority)
|
||||||
self._priority_unique_values.sort()
|
self._priority_unique_values.sort()
|
||||||
|
|
||||||
|
|
@ -126,9 +140,15 @@ class Arrange:
|
||||||
y = int(self._scale * y)
|
y = int(self._scale * y)
|
||||||
offset_x = x + self._offset_x + shape_arr.offset_x
|
offset_x = x + self._offset_x + shape_arr.offset_x
|
||||||
offset_y = y + self._offset_y + shape_arr.offset_y
|
offset_y = y + self._offset_y + shape_arr.offset_y
|
||||||
|
if offset_x < 0 or offset_y < 0:
|
||||||
|
return None # out of bounds in self._occupied
|
||||||
|
occupied_x_max = offset_x + shape_arr.arr.shape[1]
|
||||||
|
occupied_y_max = offset_y + shape_arr.arr.shape[0]
|
||||||
|
if occupied_x_max > self._occupied.shape[1] + 1 or occupied_y_max > self._occupied.shape[0] + 1:
|
||||||
|
return None # out of bounds in self._occupied
|
||||||
occupied_slice = self._occupied[
|
occupied_slice = self._occupied[
|
||||||
offset_y:offset_y + shape_arr.arr.shape[0],
|
offset_y:occupied_y_max,
|
||||||
offset_x:offset_x + shape_arr.arr.shape[1]]
|
offset_x:occupied_x_max]
|
||||||
try:
|
try:
|
||||||
if numpy.any(occupied_slice[numpy.where(shape_arr.arr == 1)]):
|
if numpy.any(occupied_slice[numpy.where(shape_arr.arr == 1)]):
|
||||||
return None
|
return None
|
||||||
|
|
@ -140,7 +160,7 @@ class Arrange:
|
||||||
return numpy.sum(prio_slice[numpy.where(shape_arr.arr == 1)])
|
return numpy.sum(prio_slice[numpy.where(shape_arr.arr == 1)])
|
||||||
|
|
||||||
## Find "best" spot for ShapeArray
|
## Find "best" spot for ShapeArray
|
||||||
# Return namedtuple with properties x, y, penalty_points, priority
|
# Return namedtuple with properties x, y, penalty_points, priority.
|
||||||
# \param shape_arr ShapeArray
|
# \param shape_arr ShapeArray
|
||||||
# \param start_prio Start with this priority value (and skip the ones before)
|
# \param start_prio Start with this priority value (and skip the ones before)
|
||||||
# \param step Slicing value, higher = more skips = faster but less accurate
|
# \param step Slicing value, higher = more skips = faster but less accurate
|
||||||
|
|
@ -153,12 +173,11 @@ class Arrange:
|
||||||
for priority in self._priority_unique_values[start_idx::step]:
|
for priority in self._priority_unique_values[start_idx::step]:
|
||||||
tryout_idx = numpy.where(self._priority == priority)
|
tryout_idx = numpy.where(self._priority == priority)
|
||||||
for idx in range(len(tryout_idx[0])):
|
for idx in range(len(tryout_idx[0])):
|
||||||
x = tryout_idx[0][idx]
|
x = tryout_idx[1][idx]
|
||||||
y = tryout_idx[1][idx]
|
y = tryout_idx[0][idx]
|
||||||
projected_x = x - self._offset_x
|
projected_x = int((x - self._offset_x) / self._scale)
|
||||||
projected_y = y - self._offset_y
|
projected_y = int((y - self._offset_y) / self._scale)
|
||||||
|
|
||||||
# array to "world" coordinates
|
|
||||||
penalty_points = self.checkShape(projected_x, projected_y, shape_arr)
|
penalty_points = self.checkShape(projected_x, projected_y, shape_arr)
|
||||||
if penalty_points is not None:
|
if penalty_points is not None:
|
||||||
return LocationSuggestion(x = projected_x, y = projected_y, penalty_points = penalty_points, priority = priority)
|
return LocationSuggestion(x = projected_x, y = projected_y, penalty_points = penalty_points, priority = priority)
|
||||||
|
|
@ -191,8 +210,12 @@ class Arrange:
|
||||||
|
|
||||||
# Set priority to low (= high number), so it won't get picked at trying out.
|
# Set priority to low (= high number), so it won't get picked at trying out.
|
||||||
prio_slice = self._priority[min_y:max_y, min_x:max_x]
|
prio_slice = self._priority[min_y:max_y, min_x:max_x]
|
||||||
prio_slice[numpy.where(shape_arr.arr[
|
prio_slice[new_occupied] = 999
|
||||||
min_y - offset_y:max_y - offset_y, min_x - offset_x:max_x - offset_x] == 1)] = 999
|
|
||||||
|
# If you want to see how the rasterized arranger build plate looks like, uncomment this code
|
||||||
|
# numpy.set_printoptions(linewidth=500, edgeitems=200)
|
||||||
|
# print(self._occupied.shape)
|
||||||
|
# print(self._occupied)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def isEmpty(self):
|
def isEmpty(self):
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
# Copyright (c) 2017 Ultimaker B.V.
|
# Copyright (c) 2018 Ultimaker B.V.
|
||||||
# Cura is released under the terms of the LGPLv3 or higher.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
|
from UM.Application import Application
|
||||||
from UM.Job import Job
|
from UM.Job import Job
|
||||||
from UM.Scene.SceneNode import SceneNode
|
from UM.Scene.SceneNode import SceneNode
|
||||||
from UM.Math.Vector import Vector
|
from UM.Math.Vector import Vector
|
||||||
|
|
@ -17,15 +18,16 @@ from cura.Arranging.ShapeArray import ShapeArray
|
||||||
from typing import List
|
from typing import List
|
||||||
|
|
||||||
|
|
||||||
|
## Do arrangements on multiple build plates (aka builtiplexer)
|
||||||
class ArrangeArray:
|
class ArrangeArray:
|
||||||
def __init__(self, x: int, y: int, fixed_nodes: List[SceneNode]):
|
def __init__(self, x: int, y: int, fixed_nodes: List[SceneNode]) -> None:
|
||||||
self._x = x
|
self._x = x
|
||||||
self._y = y
|
self._y = y
|
||||||
self._fixed_nodes = fixed_nodes
|
self._fixed_nodes = fixed_nodes
|
||||||
self._count = 0
|
self._count = 0
|
||||||
self._first_empty = None
|
self._first_empty = None
|
||||||
self._has_empty = False
|
self._has_empty = False
|
||||||
self._arrange = []
|
self._arrange = [] # type: List[Arrange]
|
||||||
|
|
||||||
def _update_first_empty(self):
|
def _update_first_empty(self):
|
||||||
for i, a in enumerate(self._arrange):
|
for i, a in enumerate(self._arrange):
|
||||||
|
|
@ -46,16 +48,17 @@ class ArrangeArray:
|
||||||
return self._count
|
return self._count
|
||||||
|
|
||||||
def get(self, index):
|
def get(self, index):
|
||||||
|
print(self._arrange)
|
||||||
return self._arrange[index]
|
return self._arrange[index]
|
||||||
|
|
||||||
def getFirstEmpty(self):
|
def getFirstEmpty(self):
|
||||||
if not self._is_empty:
|
if not self._has_empty:
|
||||||
self.add()
|
self.add()
|
||||||
return self._arrange[self._first_empty]
|
return self._arrange[self._first_empty]
|
||||||
|
|
||||||
|
|
||||||
class ArrangeObjectsAllBuildPlatesJob(Job):
|
class ArrangeObjectsAllBuildPlatesJob(Job):
|
||||||
def __init__(self, nodes: List[SceneNode], min_offset = 8):
|
def __init__(self, nodes: List[SceneNode], min_offset = 8) -> None:
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self._nodes = nodes
|
self._nodes = nodes
|
||||||
self._min_offset = min_offset
|
self._min_offset = min_offset
|
||||||
|
|
@ -79,7 +82,11 @@ class ArrangeObjectsAllBuildPlatesJob(Job):
|
||||||
nodes_arr.sort(key=lambda item: item[0])
|
nodes_arr.sort(key=lambda item: item[0])
|
||||||
nodes_arr.reverse()
|
nodes_arr.reverse()
|
||||||
|
|
||||||
x, y = 200, 200
|
global_container_stack = Application.getInstance().getGlobalContainerStack()
|
||||||
|
machine_width = global_container_stack.getProperty("machine_width", "value")
|
||||||
|
machine_depth = global_container_stack.getProperty("machine_depth", "value")
|
||||||
|
|
||||||
|
x, y = machine_width, machine_depth
|
||||||
|
|
||||||
arrange_array = ArrangeArray(x = x, y = y, fixed_nodes = [])
|
arrange_array = ArrangeArray(x = x, y = y, fixed_nodes = [])
|
||||||
arrange_array.add()
|
arrange_array.add()
|
||||||
|
|
@ -93,27 +100,18 @@ class ArrangeObjectsAllBuildPlatesJob(Job):
|
||||||
for idx, (size, node, offset_shape_arr, hull_shape_arr) in enumerate(nodes_arr):
|
for idx, (size, node, offset_shape_arr, hull_shape_arr) in enumerate(nodes_arr):
|
||||||
# For performance reasons, we assume that when a location does not fit,
|
# For performance reasons, we assume that when a location does not fit,
|
||||||
# it will also not fit for the next object (while what can be untrue).
|
# it will also not fit for the next object (while what can be untrue).
|
||||||
# We also skip possibilities by slicing through the possibilities (step = 10)
|
|
||||||
|
|
||||||
try_placement = True
|
try_placement = True
|
||||||
|
|
||||||
current_build_plate_number = 0 # always start with the first one
|
current_build_plate_number = 0 # always start with the first one
|
||||||
|
|
||||||
# # Only for first build plate
|
|
||||||
# if last_size == size and last_build_plate_number == current_build_plate_number:
|
|
||||||
# # This optimization works if many of the objects have the same size
|
|
||||||
# # Continue with same build plate number
|
|
||||||
# start_priority = last_priority
|
|
||||||
# else:
|
|
||||||
# start_priority = 0
|
|
||||||
|
|
||||||
while try_placement:
|
while try_placement:
|
||||||
# make sure that current_build_plate_number is not going crazy or you'll have a lot of arrange objects
|
# make sure that current_build_plate_number is not going crazy or you'll have a lot of arrange objects
|
||||||
while current_build_plate_number >= arrange_array.count():
|
while current_build_plate_number >= arrange_array.count():
|
||||||
arrange_array.add()
|
arrange_array.add()
|
||||||
arranger = arrange_array.get(current_build_plate_number)
|
arranger = arrange_array.get(current_build_plate_number)
|
||||||
|
|
||||||
best_spot = arranger.bestSpot(offset_shape_arr, start_prio=start_priority, step=10)
|
best_spot = arranger.bestSpot(hull_shape_arr, start_prio=start_priority)
|
||||||
x, y = best_spot.x, best_spot.y
|
x, y = best_spot.x, best_spot.y
|
||||||
node.removeDecorator(ZOffsetDecorator)
|
node.removeDecorator(ZOffsetDecorator)
|
||||||
if node.getBoundingBox():
|
if node.getBoundingBox():
|
||||||
|
|
@ -121,7 +119,7 @@ class ArrangeObjectsAllBuildPlatesJob(Job):
|
||||||
else:
|
else:
|
||||||
center_y = 0
|
center_y = 0
|
||||||
if x is not None: # We could find a place
|
if x is not None: # We could find a place
|
||||||
arranger.place(x, y, hull_shape_arr) # place the object in the arranger
|
arranger.place(x, y, offset_shape_arr) # place the object in the arranger
|
||||||
|
|
||||||
node.callDecoration("setBuildPlateNumber", current_build_plate_number)
|
node.callDecoration("setBuildPlateNumber", current_build_plate_number)
|
||||||
grouped_operation.addOperation(TranslateOperation(node, Vector(x, center_y, y), set_position = True))
|
grouped_operation.addOperation(TranslateOperation(node, Vector(x, center_y, y), set_position = True))
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
# Copyright (c) 2017 Ultimaker B.V.
|
# Copyright (c) 2017 Ultimaker B.V.
|
||||||
# Cura is released under the terms of the LGPLv3 or higher.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
|
from UM.Application import Application
|
||||||
from UM.Job import Job
|
from UM.Job import Job
|
||||||
from UM.Scene.SceneNode import SceneNode
|
from UM.Scene.SceneNode import SceneNode
|
||||||
from UM.Math.Vector import Vector
|
from UM.Math.Vector import Vector
|
||||||
|
|
@ -19,7 +20,7 @@ from typing import List
|
||||||
|
|
||||||
|
|
||||||
class ArrangeObjectsJob(Job):
|
class ArrangeObjectsJob(Job):
|
||||||
def __init__(self, nodes: List[SceneNode], fixed_nodes: List[SceneNode], min_offset = 8):
|
def __init__(self, nodes: List[SceneNode], fixed_nodes: List[SceneNode], min_offset = 8) -> None:
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self._nodes = nodes
|
self._nodes = nodes
|
||||||
self._fixed_nodes = fixed_nodes
|
self._fixed_nodes = fixed_nodes
|
||||||
|
|
@ -32,12 +33,19 @@ class ArrangeObjectsJob(Job):
|
||||||
progress = 0,
|
progress = 0,
|
||||||
title = i18n_catalog.i18nc("@info:title", "Finding Location"))
|
title = i18n_catalog.i18nc("@info:title", "Finding Location"))
|
||||||
status_message.show()
|
status_message.show()
|
||||||
arranger = Arrange.create(fixed_nodes = self._fixed_nodes)
|
global_container_stack = Application.getInstance().getGlobalContainerStack()
|
||||||
|
machine_width = global_container_stack.getProperty("machine_width", "value")
|
||||||
|
machine_depth = global_container_stack.getProperty("machine_depth", "value")
|
||||||
|
|
||||||
|
arranger = Arrange.create(x = machine_width, y = machine_depth, fixed_nodes = self._fixed_nodes, min_offset = self._min_offset)
|
||||||
|
|
||||||
# Collect nodes to be placed
|
# Collect nodes to be placed
|
||||||
nodes_arr = [] # fill with (size, node, offset_shape_arr, hull_shape_arr)
|
nodes_arr = [] # fill with (size, node, offset_shape_arr, hull_shape_arr)
|
||||||
for node in self._nodes:
|
for node in self._nodes:
|
||||||
offset_shape_arr, hull_shape_arr = ShapeArray.fromNode(node, min_offset = self._min_offset)
|
offset_shape_arr, hull_shape_arr = ShapeArray.fromNode(node, min_offset = self._min_offset)
|
||||||
|
if offset_shape_arr is None:
|
||||||
|
Logger.log("w", "Node [%s] could not be converted to an array for arranging...", str(node))
|
||||||
|
continue
|
||||||
nodes_arr.append((offset_shape_arr.arr.shape[0] * offset_shape_arr.arr.shape[1], node, offset_shape_arr, hull_shape_arr))
|
nodes_arr.append((offset_shape_arr.arr.shape[0] * offset_shape_arr.arr.shape[1], node, offset_shape_arr, hull_shape_arr))
|
||||||
|
|
||||||
# Sort the nodes with the biggest area first.
|
# Sort the nodes with the biggest area first.
|
||||||
|
|
@ -50,15 +58,15 @@ class ArrangeObjectsJob(Job):
|
||||||
last_size = None
|
last_size = None
|
||||||
grouped_operation = GroupedOperation()
|
grouped_operation = GroupedOperation()
|
||||||
found_solution_for_all = True
|
found_solution_for_all = True
|
||||||
|
not_fit_count = 0
|
||||||
for idx, (size, node, offset_shape_arr, hull_shape_arr) in enumerate(nodes_arr):
|
for idx, (size, node, offset_shape_arr, hull_shape_arr) in enumerate(nodes_arr):
|
||||||
# For performance reasons, we assume that when a location does not fit,
|
# For performance reasons, we assume that when a location does not fit,
|
||||||
# it will also not fit for the next object (while what can be untrue).
|
# it will also not fit for the next object (while what can be untrue).
|
||||||
# We also skip possibilities by slicing through the possibilities (step = 10)
|
|
||||||
if last_size == size: # This optimization works if many of the objects have the same size
|
if last_size == size: # This optimization works if many of the objects have the same size
|
||||||
start_priority = last_priority
|
start_priority = last_priority
|
||||||
else:
|
else:
|
||||||
start_priority = 0
|
start_priority = 0
|
||||||
best_spot = arranger.bestSpot(offset_shape_arr, start_prio=start_priority, step=10)
|
best_spot = arranger.bestSpot(hull_shape_arr, start_prio = start_priority)
|
||||||
x, y = best_spot.x, best_spot.y
|
x, y = best_spot.x, best_spot.y
|
||||||
node.removeDecorator(ZOffsetDecorator)
|
node.removeDecorator(ZOffsetDecorator)
|
||||||
if node.getBoundingBox():
|
if node.getBoundingBox():
|
||||||
|
|
@ -69,13 +77,13 @@ class ArrangeObjectsJob(Job):
|
||||||
last_size = size
|
last_size = size
|
||||||
last_priority = best_spot.priority
|
last_priority = best_spot.priority
|
||||||
|
|
||||||
arranger.place(x, y, hull_shape_arr) # take place before the next one
|
arranger.place(x, y, offset_shape_arr) # take place before the next one
|
||||||
|
|
||||||
grouped_operation.addOperation(TranslateOperation(node, Vector(x, center_y, y), set_position = True))
|
grouped_operation.addOperation(TranslateOperation(node, Vector(x, center_y, y), set_position = True))
|
||||||
else:
|
else:
|
||||||
Logger.log("d", "Arrange all: could not find spot!")
|
Logger.log("d", "Arrange all: could not find spot!")
|
||||||
found_solution_for_all = False
|
found_solution_for_all = False
|
||||||
grouped_operation.addOperation(TranslateOperation(node, Vector(200, center_y, - idx * 20), set_position = True))
|
grouped_operation.addOperation(TranslateOperation(node, Vector(200, center_y, -not_fit_count * 20), set_position = True))
|
||||||
|
not_fit_count += 1
|
||||||
|
|
||||||
status_message.setProgress((idx + 1) / len(nodes_arr) * 100)
|
status_message.setProgress((idx + 1) / len(nodes_arr) * 100)
|
||||||
Job.yieldThread()
|
Job.yieldThread()
|
||||||
|
|
|
||||||
|
|
@ -74,7 +74,7 @@ class ShapeArray:
|
||||||
# \param vertices
|
# \param vertices
|
||||||
@classmethod
|
@classmethod
|
||||||
def arrayFromPolygon(cls, shape, vertices):
|
def arrayFromPolygon(cls, shape, vertices):
|
||||||
base_array = numpy.zeros(shape, dtype=float) # Initialize your array of zeros
|
base_array = numpy.zeros(shape, dtype = numpy.int32) # Initialize your array of zeros
|
||||||
|
|
||||||
fill = numpy.ones(base_array.shape) * True # Initialize boolean array defining shape fill
|
fill = numpy.ones(base_array.shape) * True # Initialize boolean array defining shape fill
|
||||||
|
|
||||||
|
|
|
||||||
52
cura/AutoSave.py
Normal file
52
cura/AutoSave.py
Normal file
|
|
@ -0,0 +1,52 @@
|
||||||
|
# Copyright (c) 2016 Ultimaker B.V.
|
||||||
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
|
from PyQt5.QtCore import QTimer
|
||||||
|
|
||||||
|
from UM.Logger import Logger
|
||||||
|
|
||||||
|
|
||||||
|
class AutoSave:
|
||||||
|
def __init__(self, application):
|
||||||
|
self._application = application
|
||||||
|
self._application.getPreferences().preferenceChanged.connect(self._triggerTimer)
|
||||||
|
|
||||||
|
self._global_stack = None
|
||||||
|
|
||||||
|
self._application.getPreferences().addPreference("cura/autosave_delay", 1000 * 10)
|
||||||
|
|
||||||
|
self._change_timer = QTimer()
|
||||||
|
self._change_timer.setInterval(self._application.getPreferences().getValue("cura/autosave_delay"))
|
||||||
|
self._change_timer.setSingleShot(True)
|
||||||
|
|
||||||
|
self._saving = False
|
||||||
|
|
||||||
|
def initialize(self):
|
||||||
|
# only initialise if the application is created and has started
|
||||||
|
self._change_timer.timeout.connect(self._onTimeout)
|
||||||
|
self._application.globalContainerStackChanged.connect(self._onGlobalStackChanged)
|
||||||
|
self._onGlobalStackChanged()
|
||||||
|
self._triggerTimer()
|
||||||
|
|
||||||
|
def _triggerTimer(self, *args):
|
||||||
|
if not self._saving:
|
||||||
|
self._change_timer.start()
|
||||||
|
|
||||||
|
def _onGlobalStackChanged(self):
|
||||||
|
if self._global_stack:
|
||||||
|
self._global_stack.propertyChanged.disconnect(self._triggerTimer)
|
||||||
|
self._global_stack.containersChanged.disconnect(self._triggerTimer)
|
||||||
|
|
||||||
|
self._global_stack = self._application.getGlobalContainerStack()
|
||||||
|
|
||||||
|
if self._global_stack:
|
||||||
|
self._global_stack.propertyChanged.connect(self._triggerTimer)
|
||||||
|
self._global_stack.containersChanged.connect(self._triggerTimer)
|
||||||
|
|
||||||
|
def _onTimeout(self):
|
||||||
|
self._saving = True # To prevent the save process from triggering another autosave.
|
||||||
|
Logger.log("d", "Autosaving preferences, instances and profiles")
|
||||||
|
|
||||||
|
self._application.saveSettings()
|
||||||
|
|
||||||
|
self._saving = False
|
||||||
149
cura/Backups/Backup.py
Normal file
149
cura/Backups/Backup.py
Normal file
|
|
@ -0,0 +1,149 @@
|
||||||
|
# Copyright (c) 2018 Ultimaker B.V.
|
||||||
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
|
import io
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
|
||||||
|
import shutil
|
||||||
|
|
||||||
|
from typing import Dict, Optional
|
||||||
|
from zipfile import ZipFile, ZIP_DEFLATED, BadZipfile
|
||||||
|
|
||||||
|
from UM import i18nCatalog
|
||||||
|
from UM.Logger import Logger
|
||||||
|
from UM.Message import Message
|
||||||
|
from UM.Platform import Platform
|
||||||
|
from UM.Resources import Resources
|
||||||
|
from cura.CuraApplication import CuraApplication
|
||||||
|
|
||||||
|
|
||||||
|
## The back-up class holds all data about a back-up.
|
||||||
|
#
|
||||||
|
# It is also responsible for reading and writing the zip file to the user data
|
||||||
|
# folder.
|
||||||
|
class Backup:
|
||||||
|
# These files should be ignored when making a backup.
|
||||||
|
IGNORED_FILES = [r"cura\.log", r"plugins\.json", r"cache", r"__pycache__", r"\.qmlc", r"\.pyc"]
|
||||||
|
|
||||||
|
# Re-use translation catalog.
|
||||||
|
catalog = i18nCatalog("cura")
|
||||||
|
|
||||||
|
def __init__(self, zip_file: bytes = None, meta_data: Dict[str, str] = None) -> None:
|
||||||
|
self.zip_file = zip_file # type: Optional[bytes]
|
||||||
|
self.meta_data = meta_data # type: Optional[Dict[str, str]]
|
||||||
|
|
||||||
|
## Create a back-up from the current user config folder.
|
||||||
|
def makeFromCurrent(self) -> None:
|
||||||
|
cura_release = CuraApplication.getInstance().getVersion()
|
||||||
|
version_data_dir = Resources.getDataStoragePath()
|
||||||
|
|
||||||
|
Logger.log("d", "Creating backup for Cura %s, using folder %s", cura_release, version_data_dir)
|
||||||
|
|
||||||
|
# Ensure all current settings are saved.
|
||||||
|
CuraApplication.getInstance().saveSettings()
|
||||||
|
|
||||||
|
# We copy the preferences file to the user data directory in Linux as it's in a different location there.
|
||||||
|
# When restoring a backup on Linux, we move it back.
|
||||||
|
if Platform.isLinux():
|
||||||
|
preferences_file_name = CuraApplication.getInstance().getApplicationName()
|
||||||
|
preferences_file = Resources.getPath(Resources.Preferences, "{}.cfg".format(preferences_file_name))
|
||||||
|
backup_preferences_file = os.path.join(version_data_dir, "{}.cfg".format(preferences_file_name))
|
||||||
|
Logger.log("d", "Copying preferences file from %s to %s", preferences_file, backup_preferences_file)
|
||||||
|
shutil.copyfile(preferences_file, backup_preferences_file)
|
||||||
|
|
||||||
|
# Create an empty buffer and write the archive to it.
|
||||||
|
buffer = io.BytesIO()
|
||||||
|
archive = self._makeArchive(buffer, version_data_dir)
|
||||||
|
if archive is None:
|
||||||
|
return
|
||||||
|
files = archive.namelist()
|
||||||
|
|
||||||
|
# Count the metadata items. We do this in a rather naive way at the moment.
|
||||||
|
machine_count = len([s for s in files if "machine_instances/" in s]) - 1
|
||||||
|
material_count = len([s for s in files if "materials/" in s]) - 1
|
||||||
|
profile_count = len([s for s in files if "quality_changes/" in s]) - 1
|
||||||
|
plugin_count = len([s for s in files if "plugin.json" in s])
|
||||||
|
|
||||||
|
# Store the archive and metadata so the BackupManager can fetch them when needed.
|
||||||
|
self.zip_file = buffer.getvalue()
|
||||||
|
self.meta_data = {
|
||||||
|
"cura_release": cura_release,
|
||||||
|
"machine_count": str(machine_count),
|
||||||
|
"material_count": str(material_count),
|
||||||
|
"profile_count": str(profile_count),
|
||||||
|
"plugin_count": str(plugin_count)
|
||||||
|
}
|
||||||
|
|
||||||
|
## Make a full archive from the given root path with the given name.
|
||||||
|
# \param root_path The root directory to archive recursively.
|
||||||
|
# \return The archive as bytes.
|
||||||
|
def _makeArchive(self, buffer: "io.BytesIO", root_path: str) -> Optional[ZipFile]:
|
||||||
|
ignore_string = re.compile("|".join(self.IGNORED_FILES))
|
||||||
|
try:
|
||||||
|
archive = ZipFile(buffer, "w", ZIP_DEFLATED)
|
||||||
|
for root, folders, files in os.walk(root_path):
|
||||||
|
for item_name in folders + files:
|
||||||
|
absolute_path = os.path.join(root, item_name)
|
||||||
|
if ignore_string.search(absolute_path):
|
||||||
|
continue
|
||||||
|
archive.write(absolute_path, absolute_path[len(root_path) + len(os.sep):])
|
||||||
|
archive.close()
|
||||||
|
return archive
|
||||||
|
except (IOError, OSError, BadZipfile) as error:
|
||||||
|
Logger.log("e", "Could not create archive from user data directory: %s", error)
|
||||||
|
self._showMessage(
|
||||||
|
self.catalog.i18nc("@info:backup_failed",
|
||||||
|
"Could not create archive from user data directory: {}".format(error)))
|
||||||
|
return None
|
||||||
|
|
||||||
|
## Show a UI message.
|
||||||
|
def _showMessage(self, message: str) -> None:
|
||||||
|
Message(message, title=self.catalog.i18nc("@info:title", "Backup"), lifetime=30).show()
|
||||||
|
|
||||||
|
## Restore this back-up.
|
||||||
|
# \return Whether we had success or not.
|
||||||
|
def restore(self) -> bool:
|
||||||
|
if not self.zip_file or not self.meta_data or not self.meta_data.get("cura_release", None):
|
||||||
|
# We can restore without the minimum required information.
|
||||||
|
Logger.log("w", "Tried to restore a Cura backup without having proper data or meta data.")
|
||||||
|
self._showMessage(
|
||||||
|
self.catalog.i18nc("@info:backup_failed",
|
||||||
|
"Tried to restore a Cura backup without having proper data or meta data."))
|
||||||
|
return False
|
||||||
|
|
||||||
|
current_version = CuraApplication.getInstance().getVersion()
|
||||||
|
version_to_restore = self.meta_data.get("cura_release", "master")
|
||||||
|
if current_version != version_to_restore:
|
||||||
|
# Cannot restore version older or newer than current because settings might have changed.
|
||||||
|
# Restoring this will cause a lot of issues so we don't allow this for now.
|
||||||
|
self._showMessage(
|
||||||
|
self.catalog.i18nc("@info:backup_failed",
|
||||||
|
"Tried to restore a Cura backup that does not match your current version."))
|
||||||
|
return False
|
||||||
|
|
||||||
|
version_data_dir = Resources.getDataStoragePath()
|
||||||
|
archive = ZipFile(io.BytesIO(self.zip_file), "r")
|
||||||
|
extracted = self._extractArchive(archive, version_data_dir)
|
||||||
|
|
||||||
|
# Under Linux, preferences are stored elsewhere, so we copy the file to there.
|
||||||
|
if Platform.isLinux():
|
||||||
|
preferences_file_name = CuraApplication.getInstance().getApplicationName()
|
||||||
|
preferences_file = Resources.getPath(Resources.Preferences, "{}.cfg".format(preferences_file_name))
|
||||||
|
backup_preferences_file = os.path.join(version_data_dir, "{}.cfg".format(preferences_file_name))
|
||||||
|
Logger.log("d", "Moving preferences file from %s to %s", backup_preferences_file, preferences_file)
|
||||||
|
shutil.move(backup_preferences_file, preferences_file)
|
||||||
|
|
||||||
|
return extracted
|
||||||
|
|
||||||
|
## Extract the whole archive to the given target path.
|
||||||
|
# \param archive The archive as ZipFile.
|
||||||
|
# \param target_path The target path.
|
||||||
|
# \return Whether we had success or not.
|
||||||
|
@staticmethod
|
||||||
|
def _extractArchive(archive: "ZipFile", target_path: str) -> bool:
|
||||||
|
Logger.log("d", "Removing current data in location: %s", target_path)
|
||||||
|
Resources.factoryReset()
|
||||||
|
Logger.log("d", "Extracting backup to location: %s", target_path)
|
||||||
|
archive.extractall(target_path)
|
||||||
|
return True
|
||||||
56
cura/Backups/BackupsManager.py
Normal file
56
cura/Backups/BackupsManager.py
Normal file
|
|
@ -0,0 +1,56 @@
|
||||||
|
# Copyright (c) 2018 Ultimaker B.V.
|
||||||
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
|
from typing import Dict, Optional, Tuple
|
||||||
|
|
||||||
|
from UM.Logger import Logger
|
||||||
|
from cura.Backups.Backup import Backup
|
||||||
|
from cura.CuraApplication import CuraApplication
|
||||||
|
|
||||||
|
|
||||||
|
## The BackupsManager is responsible for managing the creating and restoring of
|
||||||
|
# back-ups.
|
||||||
|
#
|
||||||
|
# Back-ups themselves are represented in a different class.
|
||||||
|
class BackupsManager:
|
||||||
|
def __init__(self):
|
||||||
|
self._application = CuraApplication.getInstance()
|
||||||
|
|
||||||
|
## Get a back-up of the current configuration.
|
||||||
|
# \return A tuple containing a ZipFile (the actual back-up) and a dict
|
||||||
|
# containing some metadata (like version).
|
||||||
|
def createBackup(self) -> Tuple[Optional[bytes], Optional[Dict[str, str]]]:
|
||||||
|
self._disableAutoSave()
|
||||||
|
backup = Backup()
|
||||||
|
backup.makeFromCurrent()
|
||||||
|
self._enableAutoSave()
|
||||||
|
# We don't return a Backup here because we want plugins only to interact with our API and not full objects.
|
||||||
|
return backup.zip_file, backup.meta_data
|
||||||
|
|
||||||
|
## Restore a back-up from a given ZipFile.
|
||||||
|
# \param zip_file A bytes object containing the actual back-up.
|
||||||
|
# \param meta_data A dict containing some metadata that is needed to
|
||||||
|
# restore the back-up correctly.
|
||||||
|
def restoreBackup(self, zip_file: bytes, meta_data: Dict[str, str]) -> None:
|
||||||
|
if not meta_data.get("cura_release", None):
|
||||||
|
# If there is no "cura_release" specified in the meta data, we don't execute a backup restore.
|
||||||
|
Logger.log("w", "Tried to restore a backup without specifying a Cura version number.")
|
||||||
|
return
|
||||||
|
|
||||||
|
self._disableAutoSave()
|
||||||
|
|
||||||
|
backup = Backup(zip_file = zip_file, meta_data = meta_data)
|
||||||
|
restored = backup.restore()
|
||||||
|
if restored:
|
||||||
|
# At this point, Cura will need to restart for the changes to take effect.
|
||||||
|
# We don't want to store the data at this point as that would override the just-restored backup.
|
||||||
|
self._application.windowClosed(save_data = False)
|
||||||
|
|
||||||
|
## Here we try to disable the auto-save plug-in as it might interfere with
|
||||||
|
# restoring a back-up.
|
||||||
|
def _disableAutoSave(self) -> None:
|
||||||
|
self._application.setSaveDataEnabled(False)
|
||||||
|
|
||||||
|
## Re-enable auto-save after we're done.
|
||||||
|
def _enableAutoSave(self) -> None:
|
||||||
|
self._application.setSaveDataEnabled(True)
|
||||||
0
cura/Backups/__init__.py
Normal file
0
cura/Backups/__init__.py
Normal file
|
|
@ -3,12 +3,11 @@
|
||||||
|
|
||||||
from cura.Scene.CuraSceneNode import CuraSceneNode
|
from cura.Scene.CuraSceneNode import CuraSceneNode
|
||||||
from cura.Settings.ExtruderManager import ExtruderManager
|
from cura.Settings.ExtruderManager import ExtruderManager
|
||||||
from UM.Settings.ContainerRegistry import ContainerRegistry
|
from UM.Application import Application #To modify the maximum zoom level.
|
||||||
from UM.i18n import i18nCatalog
|
from UM.i18n import i18nCatalog
|
||||||
from UM.Scene.Platform import Platform
|
from UM.Scene.Platform import Platform
|
||||||
from UM.Scene.Iterator.BreadthFirstIterator import BreadthFirstIterator
|
from UM.Scene.Iterator.BreadthFirstIterator import BreadthFirstIterator
|
||||||
from UM.Scene.SceneNode import SceneNode
|
from UM.Scene.SceneNode import SceneNode
|
||||||
from UM.Application import Application
|
|
||||||
from UM.Resources import Resources
|
from UM.Resources import Resources
|
||||||
from UM.Mesh.MeshBuilder import MeshBuilder
|
from UM.Mesh.MeshBuilder import MeshBuilder
|
||||||
from UM.Math.Vector import Vector
|
from UM.Math.Vector import Vector
|
||||||
|
|
@ -25,6 +24,7 @@ catalog = i18nCatalog("cura")
|
||||||
|
|
||||||
import numpy
|
import numpy
|
||||||
import math
|
import math
|
||||||
|
import copy
|
||||||
|
|
||||||
from typing import List, Optional
|
from typing import List, Optional
|
||||||
|
|
||||||
|
|
@ -36,8 +36,10 @@ PRIME_CLEARANCE = 6.5
|
||||||
class BuildVolume(SceneNode):
|
class BuildVolume(SceneNode):
|
||||||
raftThicknessChanged = Signal()
|
raftThicknessChanged = Signal()
|
||||||
|
|
||||||
def __init__(self, parent = None):
|
def __init__(self, application, parent = None):
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
|
self._application = application
|
||||||
|
self._machine_manager = self._application.getMachineManager()
|
||||||
|
|
||||||
self._volume_outline_color = None
|
self._volume_outline_color = None
|
||||||
self._x_axis_color = None
|
self._x_axis_color = None
|
||||||
|
|
@ -46,10 +48,10 @@ class BuildVolume(SceneNode):
|
||||||
self._disallowed_area_color = None
|
self._disallowed_area_color = None
|
||||||
self._error_area_color = None
|
self._error_area_color = None
|
||||||
|
|
||||||
self._width = 0
|
self._width = 0 #type: float
|
||||||
self._height = 0
|
self._height = 0 #type: float
|
||||||
self._depth = 0
|
self._depth = 0 #type: float
|
||||||
self._shape = ""
|
self._shape = "" #type: str
|
||||||
|
|
||||||
self._shader = None
|
self._shader = None
|
||||||
|
|
||||||
|
|
@ -61,6 +63,7 @@ class BuildVolume(SceneNode):
|
||||||
self._grid_shader = None
|
self._grid_shader = None
|
||||||
|
|
||||||
self._disallowed_areas = []
|
self._disallowed_areas = []
|
||||||
|
self._disallowed_areas_no_brim = []
|
||||||
self._disallowed_area_mesh = None
|
self._disallowed_area_mesh = None
|
||||||
|
|
||||||
self._error_areas = []
|
self._error_areas = []
|
||||||
|
|
@ -80,14 +83,14 @@ class BuildVolume(SceneNode):
|
||||||
" with printed models."), title = catalog.i18nc("@info:title", "Build Volume"))
|
" with printed models."), title = catalog.i18nc("@info:title", "Build Volume"))
|
||||||
|
|
||||||
self._global_container_stack = None
|
self._global_container_stack = None
|
||||||
Application.getInstance().globalContainerStackChanged.connect(self._onStackChanged)
|
self._application.globalContainerStackChanged.connect(self._onStackChanged)
|
||||||
self._onStackChanged()
|
self._onStackChanged()
|
||||||
|
|
||||||
self._engine_ready = False
|
self._engine_ready = False
|
||||||
Application.getInstance().engineCreatedSignal.connect(self._onEngineCreated)
|
self._application.engineCreatedSignal.connect(self._onEngineCreated)
|
||||||
|
|
||||||
self._has_errors = False
|
self._has_errors = False
|
||||||
Application.getInstance().getController().getScene().sceneChanged.connect(self._onSceneChanged)
|
self._application.getController().getScene().sceneChanged.connect(self._onSceneChanged)
|
||||||
|
|
||||||
#Objects loaded at the moment. We are connected to the property changed events of these objects.
|
#Objects loaded at the moment. We are connected to the property changed events of these objects.
|
||||||
self._scene_objects = set()
|
self._scene_objects = set()
|
||||||
|
|
@ -105,14 +108,14 @@ class BuildVolume(SceneNode):
|
||||||
# Must be after setting _build_volume_message, apparently that is used in getMachineManager.
|
# Must be after setting _build_volume_message, apparently that is used in getMachineManager.
|
||||||
# activeQualityChanged is always emitted after setActiveVariant, setActiveMaterial and setActiveQuality.
|
# activeQualityChanged is always emitted after setActiveVariant, setActiveMaterial and setActiveQuality.
|
||||||
# Therefore this works.
|
# Therefore this works.
|
||||||
Application.getInstance().getMachineManager().activeQualityChanged.connect(self._onStackChanged)
|
self._machine_manager.activeQualityChanged.connect(self._onStackChanged)
|
||||||
|
|
||||||
# This should also ways work, and it is semantically more correct,
|
# This should also ways work, and it is semantically more correct,
|
||||||
# but it does not update the disallowed areas after material change
|
# but it does not update the disallowed areas after material change
|
||||||
Application.getInstance().getMachineManager().activeStackChanged.connect(self._onStackChanged)
|
self._machine_manager.activeStackChanged.connect(self._onStackChanged)
|
||||||
|
|
||||||
# Enable and disable extruder
|
# Enable and disable extruder
|
||||||
Application.getInstance().getMachineManager().extruderChanged.connect(self.updateNodeBoundaryCheck)
|
self._machine_manager.extruderChanged.connect(self.updateNodeBoundaryCheck)
|
||||||
|
|
||||||
# list of settings which were updated
|
# list of settings which were updated
|
||||||
self._changed_settings_since_last_rebuild = []
|
self._changed_settings_since_last_rebuild = []
|
||||||
|
|
@ -122,7 +125,7 @@ class BuildVolume(SceneNode):
|
||||||
self._scene_change_timer.start()
|
self._scene_change_timer.start()
|
||||||
|
|
||||||
def _onSceneChangeTimerFinished(self):
|
def _onSceneChangeTimerFinished(self):
|
||||||
root = Application.getInstance().getController().getScene().getRoot()
|
root = self._application.getController().getScene().getRoot()
|
||||||
new_scene_objects = set(node for node in BreadthFirstIterator(root) if node.callDecoration("isSliceable"))
|
new_scene_objects = set(node for node in BreadthFirstIterator(root) if node.callDecoration("isSliceable"))
|
||||||
if new_scene_objects != self._scene_objects:
|
if new_scene_objects != self._scene_objects:
|
||||||
for node in new_scene_objects - self._scene_objects: #Nodes that were added to the scene.
|
for node in new_scene_objects - self._scene_objects: #Nodes that were added to the scene.
|
||||||
|
|
@ -152,25 +155,34 @@ class BuildVolume(SceneNode):
|
||||||
if active_extruder_changed is not None:
|
if active_extruder_changed is not None:
|
||||||
active_extruder_changed.connect(self._updateDisallowedAreasAndRebuild)
|
active_extruder_changed.connect(self._updateDisallowedAreasAndRebuild)
|
||||||
|
|
||||||
def setWidth(self, width):
|
def setWidth(self, width: float) -> None:
|
||||||
if width is not None:
|
if width is not None:
|
||||||
self._width = width
|
self._width = width
|
||||||
|
|
||||||
def setHeight(self, height):
|
def setHeight(self, height: float) -> None:
|
||||||
if height is not None:
|
if height is not None:
|
||||||
self._height = height
|
self._height = height
|
||||||
|
|
||||||
def setDepth(self, depth):
|
def setDepth(self, depth: float) -> None:
|
||||||
if depth is not None:
|
if depth is not None:
|
||||||
self._depth = depth
|
self._depth = depth
|
||||||
|
|
||||||
def setShape(self, shape: str):
|
def setShape(self, shape: str) -> None:
|
||||||
if shape:
|
if shape:
|
||||||
self._shape = shape
|
self._shape = shape
|
||||||
|
|
||||||
|
## Get the length of the 3D diagonal through the build volume.
|
||||||
|
#
|
||||||
|
# This gives a sense of the scale of the build volume in general.
|
||||||
|
def getDiagonalSize(self) -> float:
|
||||||
|
return math.sqrt(self._width * self._width + self._height * self._height + self._depth * self._depth)
|
||||||
|
|
||||||
def getDisallowedAreas(self) -> List[Polygon]:
|
def getDisallowedAreas(self) -> List[Polygon]:
|
||||||
return self._disallowed_areas
|
return self._disallowed_areas
|
||||||
|
|
||||||
|
def getDisallowedAreasNoBrim(self) -> List[Polygon]:
|
||||||
|
return self._disallowed_areas_no_brim
|
||||||
|
|
||||||
def setDisallowedAreas(self, areas: List[Polygon]):
|
def setDisallowedAreas(self, areas: List[Polygon]):
|
||||||
self._disallowed_areas = areas
|
self._disallowed_areas = areas
|
||||||
|
|
||||||
|
|
@ -181,13 +193,13 @@ class BuildVolume(SceneNode):
|
||||||
if not self._shader:
|
if not self._shader:
|
||||||
self._shader = OpenGL.getInstance().createShaderProgram(Resources.getPath(Resources.Shaders, "default.shader"))
|
self._shader = OpenGL.getInstance().createShaderProgram(Resources.getPath(Resources.Shaders, "default.shader"))
|
||||||
self._grid_shader = OpenGL.getInstance().createShaderProgram(Resources.getPath(Resources.Shaders, "grid.shader"))
|
self._grid_shader = OpenGL.getInstance().createShaderProgram(Resources.getPath(Resources.Shaders, "grid.shader"))
|
||||||
theme = Application.getInstance().getTheme()
|
theme = self._application.getTheme()
|
||||||
self._grid_shader.setUniformValue("u_plateColor", Color(*theme.getColor("buildplate").getRgb()))
|
self._grid_shader.setUniformValue("u_plateColor", Color(*theme.getColor("buildplate").getRgb()))
|
||||||
self._grid_shader.setUniformValue("u_gridColor0", Color(*theme.getColor("buildplate_grid").getRgb()))
|
self._grid_shader.setUniformValue("u_gridColor0", Color(*theme.getColor("buildplate_grid").getRgb()))
|
||||||
self._grid_shader.setUniformValue("u_gridColor1", Color(*theme.getColor("buildplate_grid_minor").getRgb()))
|
self._grid_shader.setUniformValue("u_gridColor1", Color(*theme.getColor("buildplate_grid_minor").getRgb()))
|
||||||
|
|
||||||
renderer.queueNode(self, mode = RenderBatch.RenderMode.Lines)
|
renderer.queueNode(self, mode = RenderBatch.RenderMode.Lines)
|
||||||
renderer.queueNode(self, mesh = self._origin_mesh)
|
renderer.queueNode(self, mesh = self._origin_mesh, backface_cull = True)
|
||||||
renderer.queueNode(self, mesh = self._grid_mesh, shader = self._grid_shader, backface_cull = True)
|
renderer.queueNode(self, mesh = self._grid_mesh, shader = self._grid_shader, backface_cull = True)
|
||||||
if self._disallowed_area_mesh:
|
if self._disallowed_area_mesh:
|
||||||
renderer.queueNode(self, mesh = self._disallowed_area_mesh, shader = self._shader, transparent = True, backface_cull = True, sort = -9)
|
renderer.queueNode(self, mesh = self._disallowed_area_mesh, shader = self._shader, transparent = True, backface_cull = True, sort = -9)
|
||||||
|
|
@ -201,7 +213,7 @@ class BuildVolume(SceneNode):
|
||||||
## For every sliceable node, update node._outside_buildarea
|
## For every sliceable node, update node._outside_buildarea
|
||||||
#
|
#
|
||||||
def updateNodeBoundaryCheck(self):
|
def updateNodeBoundaryCheck(self):
|
||||||
root = Application.getInstance().getController().getScene().getRoot()
|
root = self._application.getController().getScene().getRoot()
|
||||||
nodes = list(BreadthFirstIterator(root))
|
nodes = list(BreadthFirstIterator(root))
|
||||||
group_nodes = []
|
group_nodes = []
|
||||||
|
|
||||||
|
|
@ -230,6 +242,8 @@ class BuildVolume(SceneNode):
|
||||||
|
|
||||||
# Mark the node as outside build volume if the set extruder is disabled
|
# Mark the node as outside build volume if the set extruder is disabled
|
||||||
extruder_position = node.callDecoration("getActiveExtruderPosition")
|
extruder_position = node.callDecoration("getActiveExtruderPosition")
|
||||||
|
if extruder_position not in self._global_container_stack.extruders:
|
||||||
|
continue
|
||||||
if not self._global_container_stack.extruders[extruder_position].isEnabled:
|
if not self._global_container_stack.extruders[extruder_position].isEnabled:
|
||||||
node.setOutsideBuildArea(True)
|
node.setOutsideBuildArea(True)
|
||||||
continue
|
continue
|
||||||
|
|
@ -289,11 +303,11 @@ class BuildVolume(SceneNode):
|
||||||
if not self._width or not self._height or not self._depth:
|
if not self._width or not self._height or not self._depth:
|
||||||
return
|
return
|
||||||
|
|
||||||
if not Application.getInstance()._engine:
|
if not self._engine_ready:
|
||||||
return
|
return
|
||||||
|
|
||||||
if not self._volume_outline_color:
|
if not self._volume_outline_color:
|
||||||
theme = Application.getInstance().getTheme()
|
theme = self._application.getTheme()
|
||||||
self._volume_outline_color = Color(*theme.getColor("volume_outline").getRgb())
|
self._volume_outline_color = Color(*theme.getColor("volume_outline").getRgb())
|
||||||
self._x_axis_color = Color(*theme.getColor("x_axis").getRgb())
|
self._x_axis_color = Color(*theme.getColor("x_axis").getRgb())
|
||||||
self._y_axis_color = Color(*theme.getColor("y_axis").getRgb())
|
self._y_axis_color = Color(*theme.getColor("y_axis").getRgb())
|
||||||
|
|
@ -455,7 +469,7 @@ class BuildVolume(SceneNode):
|
||||||
minimum = Vector(min_w, min_h - 1.0, min_d),
|
minimum = Vector(min_w, min_h - 1.0, min_d),
|
||||||
maximum = Vector(max_w, max_h - self._raft_thickness - self._extra_z_clearance, max_d))
|
maximum = Vector(max_w, max_h - self._raft_thickness - self._extra_z_clearance, max_d))
|
||||||
|
|
||||||
bed_adhesion_size = self._getEdgeDisallowedSize()
|
bed_adhesion_size = self.getEdgeDisallowedSize()
|
||||||
|
|
||||||
# As this works better for UM machines, we only add the disallowed_area_size for the z direction.
|
# As this works better for UM machines, we only add the disallowed_area_size for the z direction.
|
||||||
# This is probably wrong in all other cases. TODO!
|
# This is probably wrong in all other cases. TODO!
|
||||||
|
|
@ -465,7 +479,7 @@ class BuildVolume(SceneNode):
|
||||||
maximum = Vector(max_w - bed_adhesion_size - 1, max_h - self._raft_thickness - self._extra_z_clearance, max_d - disallowed_area_size + bed_adhesion_size - 1)
|
maximum = Vector(max_w - bed_adhesion_size - 1, max_h - self._raft_thickness - self._extra_z_clearance, max_d - disallowed_area_size + bed_adhesion_size - 1)
|
||||||
)
|
)
|
||||||
|
|
||||||
Application.getInstance().getController().getScene()._maximum_bounds = scale_to_max_bounds
|
self._application.getController().getScene()._maximum_bounds = scale_to_max_bounds
|
||||||
|
|
||||||
self.updateNodeBoundaryCheck()
|
self.updateNodeBoundaryCheck()
|
||||||
|
|
||||||
|
|
@ -518,7 +532,7 @@ class BuildVolume(SceneNode):
|
||||||
for extruder in extruders:
|
for extruder in extruders:
|
||||||
extruder.propertyChanged.disconnect(self._onSettingPropertyChanged)
|
extruder.propertyChanged.disconnect(self._onSettingPropertyChanged)
|
||||||
|
|
||||||
self._global_container_stack = Application.getInstance().getGlobalContainerStack()
|
self._global_container_stack = self._application.getGlobalContainerStack()
|
||||||
|
|
||||||
if self._global_container_stack:
|
if self._global_container_stack:
|
||||||
self._global_container_stack.propertyChanged.connect(self._onSettingPropertyChanged)
|
self._global_container_stack.propertyChanged.connect(self._onSettingPropertyChanged)
|
||||||
|
|
@ -547,6 +561,12 @@ class BuildVolume(SceneNode):
|
||||||
if self._engine_ready:
|
if self._engine_ready:
|
||||||
self.rebuild()
|
self.rebuild()
|
||||||
|
|
||||||
|
camera = Application.getInstance().getController().getCameraTool()
|
||||||
|
if camera:
|
||||||
|
diagonal = self.getDiagonalSize()
|
||||||
|
if diagonal > 1:
|
||||||
|
camera.setZoomRange(min = 0.1, max = diagonal * 5) #You can zoom out up to 5 times the diagonal. This gives some space around the volume.
|
||||||
|
|
||||||
def _onEngineCreated(self):
|
def _onEngineCreated(self):
|
||||||
self._engine_ready = True
|
self._engine_ready = True
|
||||||
self.rebuild()
|
self.rebuild()
|
||||||
|
|
@ -561,7 +581,7 @@ class BuildVolume(SceneNode):
|
||||||
|
|
||||||
if setting_key == "print_sequence":
|
if setting_key == "print_sequence":
|
||||||
machine_height = self._global_container_stack.getProperty("machine_height", "value")
|
machine_height = self._global_container_stack.getProperty("machine_height", "value")
|
||||||
if Application.getInstance().getGlobalContainerStack().getProperty("print_sequence", "value") == "one_at_a_time" and len(self._scene_objects) > 1:
|
if self._application.getGlobalContainerStack().getProperty("print_sequence", "value") == "one_at_a_time" and len(self._scene_objects) > 1:
|
||||||
self._height = min(self._global_container_stack.getProperty("gantry_height", "value"), machine_height)
|
self._height = min(self._global_container_stack.getProperty("gantry_height", "value"), machine_height)
|
||||||
if self._height < machine_height:
|
if self._height < machine_height:
|
||||||
self._build_volume_message.show()
|
self._build_volume_message.show()
|
||||||
|
|
@ -647,7 +667,7 @@ class BuildVolume(SceneNode):
|
||||||
|
|
||||||
extruder_manager = ExtruderManager.getInstance()
|
extruder_manager = ExtruderManager.getInstance()
|
||||||
used_extruders = extruder_manager.getUsedExtruderStacks()
|
used_extruders = extruder_manager.getUsedExtruderStacks()
|
||||||
disallowed_border_size = self._getEdgeDisallowedSize()
|
disallowed_border_size = self.getEdgeDisallowedSize()
|
||||||
|
|
||||||
if not used_extruders:
|
if not used_extruders:
|
||||||
# If no extruder is used, assume that the active extruder is used (else nothing is drawn)
|
# If no extruder is used, assume that the active extruder is used (else nothing is drawn)
|
||||||
|
|
@ -658,7 +678,8 @@ class BuildVolume(SceneNode):
|
||||||
|
|
||||||
result_areas = self._computeDisallowedAreasStatic(disallowed_border_size, used_extruders) #Normal machine disallowed areas can always be added.
|
result_areas = self._computeDisallowedAreasStatic(disallowed_border_size, used_extruders) #Normal machine disallowed areas can always be added.
|
||||||
prime_areas = self._computeDisallowedAreasPrimeBlob(disallowed_border_size, used_extruders)
|
prime_areas = self._computeDisallowedAreasPrimeBlob(disallowed_border_size, used_extruders)
|
||||||
prime_disallowed_areas = self._computeDisallowedAreasStatic(0, used_extruders) #Where the priming is not allowed to happen. This is not added to the result, just for collision checking.
|
result_areas_no_brim = self._computeDisallowedAreasStatic(0, used_extruders) #Where the priming is not allowed to happen. This is not added to the result, just for collision checking.
|
||||||
|
prime_disallowed_areas = copy.deepcopy(result_areas_no_brim)
|
||||||
|
|
||||||
#Check if prime positions intersect with disallowed areas.
|
#Check if prime positions intersect with disallowed areas.
|
||||||
for extruder in used_extruders:
|
for extruder in used_extruders:
|
||||||
|
|
@ -687,12 +708,15 @@ class BuildVolume(SceneNode):
|
||||||
break
|
break
|
||||||
|
|
||||||
result_areas[extruder_id].extend(prime_areas[extruder_id])
|
result_areas[extruder_id].extend(prime_areas[extruder_id])
|
||||||
|
result_areas_no_brim[extruder_id].extend(prime_areas[extruder_id])
|
||||||
|
|
||||||
nozzle_disallowed_areas = extruder.getProperty("nozzle_disallowed_areas", "value")
|
nozzle_disallowed_areas = extruder.getProperty("nozzle_disallowed_areas", "value")
|
||||||
for area in nozzle_disallowed_areas:
|
for area in nozzle_disallowed_areas:
|
||||||
polygon = Polygon(numpy.array(area, numpy.float32))
|
polygon = Polygon(numpy.array(area, numpy.float32))
|
||||||
polygon = polygon.getMinkowskiHull(Polygon.approximatedCircle(disallowed_border_size))
|
polygon_disallowed_border = polygon.getMinkowskiHull(Polygon.approximatedCircle(disallowed_border_size))
|
||||||
result_areas[extruder_id].append(polygon) #Don't perform the offset on these.
|
result_areas[extruder_id].append(polygon_disallowed_border) #Don't perform the offset on these.
|
||||||
|
#polygon_minimal_border = polygon.getMinkowskiHull(5)
|
||||||
|
result_areas_no_brim[extruder_id].append(polygon) # no brim
|
||||||
|
|
||||||
# Add prime tower location as disallowed area.
|
# Add prime tower location as disallowed area.
|
||||||
if len(used_extruders) > 1: #No prime tower in single-extrusion.
|
if len(used_extruders) > 1: #No prime tower in single-extrusion.
|
||||||
|
|
@ -708,6 +732,7 @@ class BuildVolume(SceneNode):
|
||||||
break
|
break
|
||||||
if not prime_tower_collision:
|
if not prime_tower_collision:
|
||||||
result_areas[extruder_id].extend(prime_tower_areas[extruder_id])
|
result_areas[extruder_id].extend(prime_tower_areas[extruder_id])
|
||||||
|
result_areas_no_brim[extruder_id].extend(prime_tower_areas[extruder_id])
|
||||||
else:
|
else:
|
||||||
self._error_areas.extend(prime_tower_areas[extruder_id])
|
self._error_areas.extend(prime_tower_areas[extruder_id])
|
||||||
|
|
||||||
|
|
@ -716,6 +741,9 @@ class BuildVolume(SceneNode):
|
||||||
self._disallowed_areas = []
|
self._disallowed_areas = []
|
||||||
for extruder_id in result_areas:
|
for extruder_id in result_areas:
|
||||||
self._disallowed_areas.extend(result_areas[extruder_id])
|
self._disallowed_areas.extend(result_areas[extruder_id])
|
||||||
|
self._disallowed_areas_no_brim = []
|
||||||
|
for extruder_id in result_areas_no_brim:
|
||||||
|
self._disallowed_areas_no_brim.extend(result_areas_no_brim[extruder_id])
|
||||||
|
|
||||||
## Computes the disallowed areas for objects that are printed with print
|
## Computes the disallowed areas for objects that are printed with print
|
||||||
# features.
|
# features.
|
||||||
|
|
@ -949,12 +977,12 @@ class BuildVolume(SceneNode):
|
||||||
all_values[i] = 0
|
all_values[i] = 0
|
||||||
return all_values
|
return all_values
|
||||||
|
|
||||||
## Convenience function to calculate the disallowed radius around the edge.
|
## Calculate the disallowed radius around the edge.
|
||||||
#
|
#
|
||||||
# This disallowed radius is to allow for space around the models that is
|
# This disallowed radius is to allow for space around the models that is
|
||||||
# not part of the collision radius, such as bed adhesion (skirt/brim/raft)
|
# not part of the collision radius, such as bed adhesion (skirt/brim/raft)
|
||||||
# and travel avoid distance.
|
# and travel avoid distance.
|
||||||
def _getEdgeDisallowedSize(self):
|
def getEdgeDisallowedSize(self):
|
||||||
if not self._global_container_stack or not self._global_container_stack.extruders:
|
if not self._global_container_stack or not self._global_container_stack.extruders:
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|
@ -1035,6 +1063,6 @@ class BuildVolume(SceneNode):
|
||||||
_prime_settings = ["extruder_prime_pos_x", "extruder_prime_pos_y", "extruder_prime_pos_z", "prime_blob_enable"]
|
_prime_settings = ["extruder_prime_pos_x", "extruder_prime_pos_y", "extruder_prime_pos_z", "prime_blob_enable"]
|
||||||
_tower_settings = ["prime_tower_enable", "prime_tower_circular", "prime_tower_size", "prime_tower_position_x", "prime_tower_position_y"]
|
_tower_settings = ["prime_tower_enable", "prime_tower_circular", "prime_tower_size", "prime_tower_position_x", "prime_tower_position_y"]
|
||||||
_ooze_shield_settings = ["ooze_shield_enabled", "ooze_shield_dist"]
|
_ooze_shield_settings = ["ooze_shield_enabled", "ooze_shield_dist"]
|
||||||
_distance_settings = ["infill_wipe_dist", "travel_avoid_distance", "support_offset", "support_enable", "travel_avoid_other_parts"]
|
_distance_settings = ["infill_wipe_dist", "travel_avoid_distance", "support_offset", "support_enable", "travel_avoid_other_parts", "travel_avoid_supports"]
|
||||||
_extruder_settings = ["support_enable", "support_bottom_enable", "support_roof_enable", "support_infill_extruder_nr", "support_extruder_nr_layer_0", "support_bottom_extruder_nr", "support_roof_extruder_nr", "brim_line_count", "adhesion_extruder_nr", "adhesion_type"] #Settings that can affect which extruders are used.
|
_extruder_settings = ["support_enable", "support_bottom_enable", "support_roof_enable", "support_infill_extruder_nr", "support_extruder_nr_layer_0", "support_bottom_extruder_nr", "support_roof_extruder_nr", "brim_line_count", "adhesion_extruder_nr", "adhesion_type"] #Settings that can affect which extruders are used.
|
||||||
_limit_to_extruder_settings = ["wall_extruder_nr", "wall_0_extruder_nr", "wall_x_extruder_nr", "top_bottom_extruder_nr", "infill_extruder_nr", "support_infill_extruder_nr", "support_extruder_nr_layer_0", "support_bottom_extruder_nr", "support_roof_extruder_nr", "adhesion_extruder_nr"]
|
_limit_to_extruder_settings = ["wall_extruder_nr", "wall_0_extruder_nr", "wall_x_extruder_nr", "top_bottom_extruder_nr", "infill_extruder_nr", "support_infill_extruder_nr", "support_extruder_nr_layer_0", "support_bottom_extruder_nr", "support_roof_extruder_nr", "adhesion_extruder_nr"]
|
||||||
|
|
|
||||||
|
|
@ -4,15 +4,20 @@ from PyQt5.QtCore import QSize
|
||||||
|
|
||||||
from UM.Application import Application
|
from UM.Application import Application
|
||||||
|
|
||||||
|
|
||||||
class CameraImageProvider(QQuickImageProvider):
|
class CameraImageProvider(QQuickImageProvider):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
QQuickImageProvider.__init__(self, QQuickImageProvider.Image)
|
super().__init__(QQuickImageProvider.Image)
|
||||||
|
|
||||||
## Request a new image.
|
## Request a new image.
|
||||||
def requestImage(self, id, size):
|
def requestImage(self, id, size):
|
||||||
for output_device in Application.getInstance().getOutputDeviceManager().getOutputDevices():
|
for output_device in Application.getInstance().getOutputDeviceManager().getOutputDevices():
|
||||||
try:
|
try:
|
||||||
return output_device.activePrinter.camera.getImage(), QSize(15, 15)
|
image = output_device.activePrinter.camera.getImage()
|
||||||
|
if image.isNull():
|
||||||
|
image = QImage()
|
||||||
|
|
||||||
|
return image, QSize(15, 15)
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
pass
|
pass
|
||||||
return QImage(), QSize(15, 15)
|
return QImage(), QSize(15, 15)
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,20 @@
|
||||||
# Copyright (c) 2017 Ultimaker B.V.
|
# Copyright (c) 2018 Ultimaker B.V.
|
||||||
# Cura is released under the terms of the LGPLv3 or higher.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
from PyQt5.QtCore import QObject, QUrl
|
from PyQt5.QtCore import QObject, QUrl
|
||||||
from PyQt5.QtGui import QDesktopServices
|
from PyQt5.QtGui import QDesktopServices
|
||||||
from UM.FlameProfiler import pyqtSlot
|
from typing import List, TYPE_CHECKING
|
||||||
|
|
||||||
from UM.Event import CallFunctionEvent
|
from UM.Event import CallFunctionEvent
|
||||||
from UM.Application import Application
|
from UM.FlameProfiler import pyqtSlot
|
||||||
from UM.Math.Vector import Vector
|
from UM.Math.Vector import Vector
|
||||||
from UM.Scene.Selection import Selection
|
from UM.Scene.Selection import Selection
|
||||||
from UM.Scene.Iterator.BreadthFirstIterator import BreadthFirstIterator
|
from UM.Scene.Iterator.BreadthFirstIterator import BreadthFirstIterator
|
||||||
from UM.Operations.GroupedOperation import GroupedOperation
|
from UM.Operations.GroupedOperation import GroupedOperation
|
||||||
from UM.Operations.RemoveSceneNodeOperation import RemoveSceneNodeOperation
|
from UM.Operations.RemoveSceneNodeOperation import RemoveSceneNodeOperation
|
||||||
from UM.Operations.SetTransformOperation import SetTransformOperation
|
|
||||||
from UM.Operations.TranslateOperation import TranslateOperation
|
from UM.Operations.TranslateOperation import TranslateOperation
|
||||||
|
|
||||||
|
import cura.CuraApplication
|
||||||
from cura.Operations.SetParentOperation import SetParentOperation
|
from cura.Operations.SetParentOperation import SetParentOperation
|
||||||
from cura.MultiplyObjectsJob import MultiplyObjectsJob
|
from cura.MultiplyObjectsJob import MultiplyObjectsJob
|
||||||
from cura.Settings.SetObjectExtruderOperation import SetObjectExtruderOperation
|
from cura.Settings.SetObjectExtruderOperation import SetObjectExtruderOperation
|
||||||
|
|
@ -24,32 +24,36 @@ from cura.Operations.SetBuildPlateNumberOperation import SetBuildPlateNumberOper
|
||||||
|
|
||||||
from UM.Logger import Logger
|
from UM.Logger import Logger
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from UM.Scene.SceneNode import SceneNode
|
||||||
|
|
||||||
class CuraActions(QObject):
|
class CuraActions(QObject):
|
||||||
def __init__(self, parent = None):
|
def __init__(self, parent: QObject = None) -> None:
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
|
|
||||||
@pyqtSlot()
|
@pyqtSlot()
|
||||||
def openDocumentation(self):
|
def openDocumentation(self) -> None:
|
||||||
# Starting a web browser from a signal handler connected to a menu will crash on windows.
|
# Starting a web browser from a signal handler connected to a menu will crash on windows.
|
||||||
# So instead, defer the call to the next run of the event loop, since that does work.
|
# So instead, defer the call to the next run of the event loop, since that does work.
|
||||||
# Note that weirdly enough, only signal handlers that open a web browser fail like that.
|
# Note that weirdly enough, only signal handlers that open a web browser fail like that.
|
||||||
event = CallFunctionEvent(self._openUrl, [QUrl("http://ultimaker.com/en/support/software")], {})
|
event = CallFunctionEvent(self._openUrl, [QUrl("http://ultimaker.com/en/support/software")], {})
|
||||||
Application.getInstance().functionEvent(event)
|
cura.CuraApplication.CuraApplication.getInstance().functionEvent(event)
|
||||||
|
|
||||||
@pyqtSlot()
|
@pyqtSlot()
|
||||||
def openBugReportPage(self):
|
def openBugReportPage(self) -> None:
|
||||||
event = CallFunctionEvent(self._openUrl, [QUrl("http://github.com/Ultimaker/Cura/issues")], {})
|
event = CallFunctionEvent(self._openUrl, [QUrl("http://github.com/Ultimaker/Cura/issues")], {})
|
||||||
Application.getInstance().functionEvent(event)
|
cura.CuraApplication.CuraApplication.getInstance().functionEvent(event)
|
||||||
|
|
||||||
## Reset camera position and direction to default
|
## Reset camera position and direction to default
|
||||||
@pyqtSlot()
|
@pyqtSlot()
|
||||||
def homeCamera(self) -> None:
|
def homeCamera(self) -> None:
|
||||||
scene = Application.getInstance().getController().getScene()
|
scene = cura.CuraApplication.CuraApplication.getInstance().getController().getScene()
|
||||||
camera = scene.getActiveCamera()
|
camera = scene.getActiveCamera()
|
||||||
camera.setPosition(Vector(-80, 250, 700))
|
if camera:
|
||||||
camera.setPerspective(True)
|
diagonal_size = cura.CuraApplication.CuraApplication.getInstance().getBuildVolume().getDiagonalSize()
|
||||||
camera.lookAt(Vector(0, 0, 0))
|
camera.setPosition(Vector(-80, 250, 700) * diagonal_size / 375)
|
||||||
|
camera.setPerspective(True)
|
||||||
|
camera.lookAt(Vector(0, 0, 0))
|
||||||
|
|
||||||
## Center all objects in the selection
|
## Center all objects in the selection
|
||||||
@pyqtSlot()
|
@pyqtSlot()
|
||||||
|
|
@ -73,16 +77,17 @@ class CuraActions(QObject):
|
||||||
# \param count The number of times to multiply the selection.
|
# \param count The number of times to multiply the selection.
|
||||||
@pyqtSlot(int)
|
@pyqtSlot(int)
|
||||||
def multiplySelection(self, count: int) -> None:
|
def multiplySelection(self, count: int) -> None:
|
||||||
job = MultiplyObjectsJob(Selection.getAllSelectedObjects(), count, min_offset = 8)
|
min_offset = cura.CuraApplication.CuraApplication.getInstance().getBuildVolume().getEdgeDisallowedSize() + 2 # Allow for some rounding errors
|
||||||
|
job = MultiplyObjectsJob(Selection.getAllSelectedObjects(), count, min_offset = max(min_offset, 8))
|
||||||
job.start()
|
job.start()
|
||||||
|
|
||||||
## Delete all selected objects.
|
## Delete all selected objects.
|
||||||
@pyqtSlot()
|
@pyqtSlot()
|
||||||
def deleteSelection(self) -> None:
|
def deleteSelection(self) -> None:
|
||||||
if not Application.getInstance().getController().getToolsEnabled():
|
if not cura.CuraApplication.CuraApplication.getInstance().getController().getToolsEnabled():
|
||||||
return
|
return
|
||||||
|
|
||||||
removed_group_nodes = []
|
removed_group_nodes = [] #type: List[SceneNode]
|
||||||
op = GroupedOperation()
|
op = GroupedOperation()
|
||||||
nodes = Selection.getAllSelectedObjects()
|
nodes = Selection.getAllSelectedObjects()
|
||||||
for node in nodes:
|
for node in nodes:
|
||||||
|
|
@ -96,7 +101,7 @@ class CuraActions(QObject):
|
||||||
op.addOperation(RemoveSceneNodeOperation(group_node))
|
op.addOperation(RemoveSceneNodeOperation(group_node))
|
||||||
|
|
||||||
# Reset the print information
|
# Reset the print information
|
||||||
Application.getInstance().getController().getScene().sceneChanged.emit(node)
|
cura.CuraApplication.CuraApplication.getInstance().getController().getScene().sceneChanged.emit(node)
|
||||||
|
|
||||||
op.push()
|
op.push()
|
||||||
|
|
||||||
|
|
@ -111,7 +116,7 @@ class CuraActions(QObject):
|
||||||
for node in Selection.getAllSelectedObjects():
|
for node in Selection.getAllSelectedObjects():
|
||||||
# If the node is a group, apply the active extruder to all children of the group.
|
# If the node is a group, apply the active extruder to all children of the group.
|
||||||
if node.callDecoration("isGroup"):
|
if node.callDecoration("isGroup"):
|
||||||
for grouped_node in BreadthFirstIterator(node):
|
for grouped_node in BreadthFirstIterator(node): #type: ignore #Ignore type error because iter() should get called automatically by Python syntax.
|
||||||
if grouped_node.callDecoration("getActiveExtruder") == extruder_id:
|
if grouped_node.callDecoration("getActiveExtruder") == extruder_id:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
|
@ -143,7 +148,7 @@ class CuraActions(QObject):
|
||||||
Logger.log("d", "Setting build plate number... %d" % build_plate_nr)
|
Logger.log("d", "Setting build plate number... %d" % build_plate_nr)
|
||||||
operation = GroupedOperation()
|
operation = GroupedOperation()
|
||||||
|
|
||||||
root = Application.getInstance().getController().getScene().getRoot()
|
root = cura.CuraApplication.CuraApplication.getInstance().getController().getScene().getRoot()
|
||||||
|
|
||||||
nodes_to_change = []
|
nodes_to_change = []
|
||||||
for node in Selection.getAllSelectedObjects():
|
for node in Selection.getAllSelectedObjects():
|
||||||
|
|
@ -151,7 +156,7 @@ class CuraActions(QObject):
|
||||||
while parent_node.getParent() != root:
|
while parent_node.getParent() != root:
|
||||||
parent_node = parent_node.getParent()
|
parent_node = parent_node.getParent()
|
||||||
|
|
||||||
for single_node in BreadthFirstIterator(parent_node):
|
for single_node in BreadthFirstIterator(parent_node): #type: ignore #Ignore type error because iter() should get called automatically by Python syntax.
|
||||||
nodes_to_change.append(single_node)
|
nodes_to_change.append(single_node)
|
||||||
|
|
||||||
if not nodes_to_change:
|
if not nodes_to_change:
|
||||||
|
|
@ -164,5 +169,5 @@ class CuraActions(QObject):
|
||||||
|
|
||||||
Selection.clear()
|
Selection.clear()
|
||||||
|
|
||||||
def _openUrl(self, url):
|
def _openUrl(self, url: QUrl) -> None:
|
||||||
QDesktopServices.openUrl(url)
|
QDesktopServices.openUrl(url)
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
41
cura/CuraPackageManager.py
Normal file
41
cura/CuraPackageManager.py
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
# Copyright (c) 2018 Ultimaker B.V.
|
||||||
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
|
from typing import List, Tuple
|
||||||
|
|
||||||
|
from cura.CuraApplication import CuraApplication #To find some resource types.
|
||||||
|
from cura.Settings.GlobalStack import GlobalStack
|
||||||
|
|
||||||
|
from UM.PackageManager import PackageManager #The class we're extending.
|
||||||
|
from UM.Resources import Resources #To find storage paths for some resource types.
|
||||||
|
|
||||||
|
|
||||||
|
class CuraPackageManager(PackageManager):
|
||||||
|
def __init__(self, application, parent = None):
|
||||||
|
super().__init__(application, parent)
|
||||||
|
|
||||||
|
def initialize(self):
|
||||||
|
self._installation_dirs_dict["materials"] = Resources.getStoragePath(CuraApplication.ResourceTypes.MaterialInstanceContainer)
|
||||||
|
self._installation_dirs_dict["qualities"] = Resources.getStoragePath(CuraApplication.ResourceTypes.QualityInstanceContainer)
|
||||||
|
|
||||||
|
super().initialize()
|
||||||
|
|
||||||
|
## Returns a list of where the package is used
|
||||||
|
# empty if it is never used.
|
||||||
|
# It loops through all the package contents and see if some of the ids are used.
|
||||||
|
# The list consists of 3-tuples: (global_stack, extruder_nr, container_id)
|
||||||
|
def getMachinesUsingPackage(self, package_id: str) -> Tuple[List[Tuple[GlobalStack, str, str]], List[Tuple[GlobalStack, str, str]]]:
|
||||||
|
ids = self.getPackageContainerIds(package_id)
|
||||||
|
container_stacks = self._application.getContainerRegistry().findContainerStacks()
|
||||||
|
global_stacks = [container_stack for container_stack in container_stacks if isinstance(container_stack, GlobalStack)]
|
||||||
|
machine_with_materials = []
|
||||||
|
machine_with_qualities = []
|
||||||
|
for container_id in ids:
|
||||||
|
for global_stack in global_stacks:
|
||||||
|
for extruder_nr, extruder_stack in global_stack.extruders.items():
|
||||||
|
if container_id in (extruder_stack.material.getId(), extruder_stack.material.getMetaData().get("base_file")):
|
||||||
|
machine_with_materials.append((global_stack, extruder_nr, container_id))
|
||||||
|
if container_id == extruder_stack.quality.getId():
|
||||||
|
machine_with_qualities.append((global_stack, extruder_nr, container_id))
|
||||||
|
|
||||||
|
return machine_with_materials, machine_with_qualities
|
||||||
|
|
@ -4,3 +4,6 @@
|
||||||
CuraVersion = "@CURA_VERSION@"
|
CuraVersion = "@CURA_VERSION@"
|
||||||
CuraBuildType = "@CURA_BUILDTYPE@"
|
CuraBuildType = "@CURA_BUILDTYPE@"
|
||||||
CuraDebugMode = True if "@_cura_debugmode@" == "ON" else False
|
CuraDebugMode = True if "@_cura_debugmode@" == "ON" else False
|
||||||
|
CuraSDKVersion = "@CURA_SDK_VERSION@"
|
||||||
|
CuraCloudAPIRoot = "@CURA_CLOUD_API_ROOT@"
|
||||||
|
CuraCloudAPIVersion = "@CURA_CLOUD_API_VERSION@"
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,13 @@
|
||||||
# Copyright (c) 2016 Ultimaker B.V.
|
# Copyright (c) 2018 Ultimaker B.V.
|
||||||
# Cura is released under the terms of the LGPLv3 or higher.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
from UM.Logger import Logger
|
|
||||||
from UM.PluginRegistry import PluginRegistry # So MachineAction can be added as plugin type
|
|
||||||
|
|
||||||
from UM.Settings.ContainerRegistry import ContainerRegistry
|
|
||||||
from UM.Settings.DefinitionContainer import DefinitionContainer
|
|
||||||
|
|
||||||
from PyQt5.QtCore import QObject
|
from PyQt5.QtCore import QObject
|
||||||
|
|
||||||
from UM.FlameProfiler import pyqtSlot
|
from UM.FlameProfiler import pyqtSlot
|
||||||
|
from UM.Logger import Logger
|
||||||
|
from UM.PluginRegistry import PluginRegistry # So MachineAction can be added as plugin type
|
||||||
|
from UM.Settings.DefinitionContainer import DefinitionContainer
|
||||||
|
|
||||||
|
|
||||||
## Raised when trying to add an unknown machine action as a required action
|
## Raised when trying to add an unknown machine action as a required action
|
||||||
class UnknownMachineActionError(Exception):
|
class UnknownMachineActionError(Exception):
|
||||||
|
|
@ -20,23 +20,27 @@ class NotUniqueMachineActionError(Exception):
|
||||||
|
|
||||||
|
|
||||||
class MachineActionManager(QObject):
|
class MachineActionManager(QObject):
|
||||||
def __init__(self, parent = None):
|
def __init__(self, application, parent = None):
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
|
self._application = application
|
||||||
|
|
||||||
self._machine_actions = {} # Dict of all known machine actions
|
self._machine_actions = {} # Dict of all known machine actions
|
||||||
self._required_actions = {} # Dict of all required actions by definition ID
|
self._required_actions = {} # Dict of all required actions by definition ID
|
||||||
self._supported_actions = {} # Dict of all supported actions by definition ID
|
self._supported_actions = {} # Dict of all supported actions by definition ID
|
||||||
self._first_start_actions = {} # Dict of all actions that need to be done when first added by definition ID
|
self._first_start_actions = {} # Dict of all actions that need to be done when first added by definition ID
|
||||||
|
|
||||||
|
def initialize(self):
|
||||||
|
container_registry = self._application.getContainerRegistry()
|
||||||
|
|
||||||
# Add machine_action as plugin type
|
# Add machine_action as plugin type
|
||||||
PluginRegistry.addType("machine_action", self.addMachineAction)
|
PluginRegistry.addType("machine_action", self.addMachineAction)
|
||||||
|
|
||||||
# Ensure that all containers that were registered before creation of this registry are also handled.
|
# Ensure that all containers that were registered before creation of this registry are also handled.
|
||||||
# This should not have any effect, but it makes it safer if we ever refactor the order of things.
|
# This should not have any effect, but it makes it safer if we ever refactor the order of things.
|
||||||
for container in ContainerRegistry.getInstance().findDefinitionContainers():
|
for container in container_registry.findDefinitionContainers():
|
||||||
self._onContainerAdded(container)
|
self._onContainerAdded(container)
|
||||||
|
|
||||||
ContainerRegistry.getInstance().containerAdded.connect(self._onContainerAdded)
|
container_registry.containerAdded.connect(self._onContainerAdded)
|
||||||
|
|
||||||
def _onContainerAdded(self, container):
|
def _onContainerAdded(self, container):
|
||||||
## Ensure that the actions are added to this manager
|
## Ensure that the actions are added to this manager
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
# Copyright (c) 2018 Ultimaker B.V.
|
# Copyright (c) 2018 Ultimaker B.V.
|
||||||
# Cura is released under the terms of the LGPLv3 or higher.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
from typing import Optional
|
from typing import Optional, Any, Dict, Union, TYPE_CHECKING
|
||||||
|
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
|
|
||||||
|
|
@ -9,6 +9,9 @@ from UM.ConfigurationErrorMessage import ConfigurationErrorMessage
|
||||||
from UM.Logger import Logger
|
from UM.Logger import Logger
|
||||||
from UM.Settings.InstanceContainer import InstanceContainer
|
from UM.Settings.InstanceContainer import InstanceContainer
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from cura.Machines.QualityGroup import QualityGroup
|
||||||
|
|
||||||
|
|
||||||
##
|
##
|
||||||
# A metadata / container combination. Use getContainer() to get the container corresponding to the metadata.
|
# A metadata / container combination. Use getContainer() to get the container corresponding to the metadata.
|
||||||
|
|
@ -23,10 +26,16 @@ from UM.Settings.InstanceContainer import InstanceContainer
|
||||||
class ContainerNode:
|
class ContainerNode:
|
||||||
__slots__ = ("metadata", "container", "children_map")
|
__slots__ = ("metadata", "container", "children_map")
|
||||||
|
|
||||||
def __init__(self, metadata: Optional[dict] = None):
|
def __init__(self, metadata: Optional[Dict[str, Any]] = None) -> None:
|
||||||
self.metadata = metadata
|
self.metadata = metadata
|
||||||
self.container = None
|
self.container = None
|
||||||
self.children_map = OrderedDict()
|
self.children_map = OrderedDict() #type: OrderedDict[str, Union[QualityGroup, ContainerNode]]
|
||||||
|
|
||||||
|
## Get an entry value from the metadata
|
||||||
|
def getMetaDataEntry(self, entry: str, default: Any = None) -> Any:
|
||||||
|
if self.metadata is None:
|
||||||
|
return default
|
||||||
|
return self.metadata.get(entry, default)
|
||||||
|
|
||||||
def getChildNode(self, child_key: str) -> Optional["ContainerNode"]:
|
def getChildNode(self, child_key: str) -> Optional["ContainerNode"]:
|
||||||
return self.children_map.get(child_key)
|
return self.children_map.get(child_key)
|
||||||
|
|
@ -50,4 +59,4 @@ class ContainerNode:
|
||||||
return self.container
|
return self.container
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
return "%s[%s]" % (self.__class__.__name__, self.metadata.get("id"))
|
return "%s[%s]" % (self.__class__.__name__, self.getMetaDataEntry("id"))
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,11 @@
|
||||||
# Copyright (c) 2018 Ultimaker B.V.
|
# Copyright (c) 2018 Ultimaker B.V.
|
||||||
# Cura is released under the terms of the LGPLv3 or higher.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
from typing import List
|
from typing import List, TYPE_CHECKING
|
||||||
from cura.Machines.MaterialNode import MaterialNode #For type checking.
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from cura.Machines.MaterialNode import MaterialNode
|
||||||
|
|
||||||
|
|
||||||
## A MaterialGroup represents a group of material InstanceContainers that are derived from a single material profile.
|
## A MaterialGroup represents a group of material InstanceContainers that are derived from a single material profile.
|
||||||
# The main InstanceContainer which has the ID of the material profile file name is called the "root_material". For
|
# The main InstanceContainer which has the ID of the material profile file name is called the "root_material". For
|
||||||
|
|
@ -18,11 +21,11 @@ from cura.Machines.MaterialNode import MaterialNode #For type checking.
|
||||||
class MaterialGroup:
|
class MaterialGroup:
|
||||||
__slots__ = ("name", "is_read_only", "root_material_node", "derived_material_node_list")
|
__slots__ = ("name", "is_read_only", "root_material_node", "derived_material_node_list")
|
||||||
|
|
||||||
def __init__(self, name: str, root_material_node: MaterialNode):
|
def __init__(self, name: str, root_material_node: "MaterialNode") -> None:
|
||||||
self.name = name
|
self.name = name
|
||||||
self.is_read_only = False
|
self.is_read_only = False
|
||||||
self.root_material_node = root_material_node
|
self.root_material_node = root_material_node # type: MaterialNode
|
||||||
self.derived_material_node_list = [] #type: List[MaterialNode]
|
self.derived_material_node_list = [] # type: List[MaterialNode]
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
return "%s[%s]" % (self.__class__.__name__, self.name)
|
return "%s[%s]" % (self.__class__.__name__, self.name)
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
from collections import defaultdict, OrderedDict
|
from collections import defaultdict, OrderedDict
|
||||||
import copy
|
import copy
|
||||||
import uuid
|
import uuid
|
||||||
from typing import Optional, TYPE_CHECKING
|
from typing import Dict, Optional, TYPE_CHECKING
|
||||||
|
|
||||||
from PyQt5.Qt import QTimer, QObject, pyqtSignal, pyqtSlot
|
from PyQt5.Qt import QTimer, QObject, pyqtSignal, pyqtSlot
|
||||||
|
|
||||||
|
|
@ -17,6 +17,7 @@ from UM.Util import parseBool
|
||||||
|
|
||||||
from .MaterialNode import MaterialNode
|
from .MaterialNode import MaterialNode
|
||||||
from .MaterialGroup import MaterialGroup
|
from .MaterialGroup import MaterialGroup
|
||||||
|
from .VariantType import VariantType
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from UM.Settings.DefinitionContainer import DefinitionContainer
|
from UM.Settings.DefinitionContainer import DefinitionContainer
|
||||||
|
|
@ -46,7 +47,7 @@ class MaterialManager(QObject):
|
||||||
|
|
||||||
self._fallback_materials_map = dict() # material_type -> generic material metadata
|
self._fallback_materials_map = dict() # material_type -> generic material metadata
|
||||||
self._material_group_map = dict() # root_material_id -> MaterialGroup
|
self._material_group_map = dict() # root_material_id -> MaterialGroup
|
||||||
self._diameter_machine_variant_material_map = dict() # approximate diameter str -> dict(machine_definition_id -> MaterialNode)
|
self._diameter_machine_nozzle_buildplate_material_map = dict() # approximate diameter str -> dict(machine_definition_id -> MaterialNode)
|
||||||
|
|
||||||
# We're using these two maps to convert between the specific diameter material id and the generic material id
|
# We're using these two maps to convert between the specific diameter material id and the generic material id
|
||||||
# because the generic material ids are used in qualities and definitions, while the specific diameter material is meant
|
# because the generic material ids are used in qualities and definitions, while the specific diameter material is meant
|
||||||
|
|
@ -76,10 +77,12 @@ class MaterialManager(QObject):
|
||||||
|
|
||||||
def initialize(self):
|
def initialize(self):
|
||||||
# Find all materials and put them in a matrix for quick search.
|
# Find all materials and put them in a matrix for quick search.
|
||||||
material_metadatas = {metadata["id"]: metadata for metadata in self._container_registry.findContainersMetadata(type = "material")}
|
material_metadatas = {metadata["id"]: metadata for metadata in
|
||||||
|
self._container_registry.findContainersMetadata(type = "material") if
|
||||||
|
metadata.get("GUID")}
|
||||||
|
|
||||||
self._material_group_map = dict()
|
self._material_group_map = dict()
|
||||||
|
|
||||||
# Map #1
|
# Map #1
|
||||||
# root_material_id -> MaterialGroup
|
# root_material_id -> MaterialGroup
|
||||||
for material_id, material_metadata in material_metadatas.items():
|
for material_id, material_metadata in material_metadatas.items():
|
||||||
|
|
@ -93,7 +96,7 @@ class MaterialManager(QObject):
|
||||||
self._material_group_map[root_material_id].is_read_only = self._container_registry.isReadOnly(root_material_id)
|
self._material_group_map[root_material_id].is_read_only = self._container_registry.isReadOnly(root_material_id)
|
||||||
group = self._material_group_map[root_material_id]
|
group = self._material_group_map[root_material_id]
|
||||||
|
|
||||||
#Store this material in the group of the appropriate root material.
|
# Store this material in the group of the appropriate root material.
|
||||||
if material_id != root_material_id:
|
if material_id != root_material_id:
|
||||||
new_node = MaterialNode(material_metadata)
|
new_node = MaterialNode(material_metadata)
|
||||||
group.derived_material_node_list.append(new_node)
|
group.derived_material_node_list.append(new_node)
|
||||||
|
|
@ -113,8 +116,6 @@ class MaterialManager(QObject):
|
||||||
grouped_by_type_dict = dict()
|
grouped_by_type_dict = dict()
|
||||||
material_types_without_fallback = set()
|
material_types_without_fallback = set()
|
||||||
for root_material_id, material_node in self._material_group_map.items():
|
for root_material_id, material_node in self._material_group_map.items():
|
||||||
if not self._container_registry.isReadOnly(root_material_id):
|
|
||||||
continue
|
|
||||||
material_type = material_node.root_material_node.metadata["material"]
|
material_type = material_node.root_material_node.metadata["material"]
|
||||||
if material_type not in grouped_by_type_dict:
|
if material_type not in grouped_by_type_dict:
|
||||||
grouped_by_type_dict[material_type] = {"generic": None,
|
grouped_by_type_dict[material_type] = {"generic": None,
|
||||||
|
|
@ -127,9 +128,15 @@ class MaterialManager(QObject):
|
||||||
diameter = material_node.root_material_node.metadata.get("approximate_diameter")
|
diameter = material_node.root_material_node.metadata.get("approximate_diameter")
|
||||||
if diameter != self._default_approximate_diameter_for_quality_search:
|
if diameter != self._default_approximate_diameter_for_quality_search:
|
||||||
to_add = False # don't add if it's not the default diameter
|
to_add = False # don't add if it's not the default diameter
|
||||||
|
|
||||||
if to_add:
|
if to_add:
|
||||||
grouped_by_type_dict[material_type] = material_node.root_material_node.metadata
|
# Checking this first allow us to differentiate between not read only materials:
|
||||||
material_types_without_fallback.remove(material_type)
|
# - if it's in the list, it means that is a new material without fallback
|
||||||
|
# - if it is not, then it is a custom material with a fallback material (parent)
|
||||||
|
if material_type in material_types_without_fallback:
|
||||||
|
grouped_by_type_dict[material_type] = material_node.root_material_node.metadata
|
||||||
|
material_types_without_fallback.remove(material_type)
|
||||||
|
|
||||||
# Remove the materials that have no fallback materials
|
# Remove the materials that have no fallback materials
|
||||||
for material_type in material_types_without_fallback:
|
for material_type in material_types_without_fallback:
|
||||||
del grouped_by_type_dict[material_type]
|
del grouped_by_type_dict[material_type]
|
||||||
|
|
@ -147,9 +154,6 @@ class MaterialManager(QObject):
|
||||||
material_group_dict = dict()
|
material_group_dict = dict()
|
||||||
keys_to_fetch = ("name", "material", "brand", "color")
|
keys_to_fetch = ("name", "material", "brand", "color")
|
||||||
for root_material_id, machine_node in self._material_group_map.items():
|
for root_material_id, machine_node in self._material_group_map.items():
|
||||||
if not self._container_registry.isReadOnly(root_material_id):
|
|
||||||
continue
|
|
||||||
|
|
||||||
root_material_metadata = machine_node.root_material_node.metadata
|
root_material_metadata = machine_node.root_material_node.metadata
|
||||||
|
|
||||||
key_data = []
|
key_data = []
|
||||||
|
|
@ -157,8 +161,13 @@ class MaterialManager(QObject):
|
||||||
key_data.append(root_material_metadata.get(key))
|
key_data.append(root_material_metadata.get(key))
|
||||||
key_data = tuple(key_data)
|
key_data = tuple(key_data)
|
||||||
|
|
||||||
|
# If the key_data doesn't exist, it doesn't matter if the material is read only...
|
||||||
if key_data not in material_group_dict:
|
if key_data not in material_group_dict:
|
||||||
material_group_dict[key_data] = dict()
|
material_group_dict[key_data] = dict()
|
||||||
|
else:
|
||||||
|
# ...but if key_data exists, we just overwrite it if the material is read only, otherwise we skip it
|
||||||
|
if not machine_node.is_read_only:
|
||||||
|
continue
|
||||||
approximate_diameter = root_material_metadata.get("approximate_diameter")
|
approximate_diameter = root_material_metadata.get("approximate_diameter")
|
||||||
material_group_dict[key_data][approximate_diameter] = root_material_metadata["id"]
|
material_group_dict[key_data][approximate_diameter] = root_material_metadata["id"]
|
||||||
|
|
||||||
|
|
@ -178,44 +187,78 @@ class MaterialManager(QObject):
|
||||||
self._diameter_material_map[root_material_id] = default_root_material_id
|
self._diameter_material_map[root_material_id] = default_root_material_id
|
||||||
|
|
||||||
# Map #4
|
# Map #4
|
||||||
# "machine" -> "variant_name" -> "root material ID" -> specific material InstanceContainer
|
# "machine" -> "nozzle name" -> "buildplate name" -> "root material ID" -> specific material InstanceContainer
|
||||||
# Construct the "machine" -> "variant" -> "root material ID" -> specific material InstanceContainer
|
self._diameter_machine_nozzle_buildplate_material_map = dict()
|
||||||
self._diameter_machine_variant_material_map = dict()
|
|
||||||
for material_metadata in material_metadatas.values():
|
for material_metadata in material_metadatas.values():
|
||||||
# We don't store empty material in the lookup tables
|
self.__addMaterialMetadataIntoLookupTree(material_metadata)
|
||||||
if material_metadata["id"] == "empty_material":
|
|
||||||
continue
|
|
||||||
|
|
||||||
root_material_id = material_metadata["base_file"]
|
|
||||||
definition = material_metadata["definition"]
|
|
||||||
approximate_diameter = material_metadata["approximate_diameter"]
|
|
||||||
|
|
||||||
if approximate_diameter not in self._diameter_machine_variant_material_map:
|
|
||||||
self._diameter_machine_variant_material_map[approximate_diameter] = {}
|
|
||||||
|
|
||||||
machine_variant_material_map = self._diameter_machine_variant_material_map[approximate_diameter]
|
|
||||||
if definition not in machine_variant_material_map:
|
|
||||||
machine_variant_material_map[definition] = MaterialNode()
|
|
||||||
|
|
||||||
machine_node = machine_variant_material_map[definition]
|
|
||||||
variant_name = material_metadata.get("variant_name")
|
|
||||||
if not variant_name:
|
|
||||||
# if there is no variant, this material is for the machine, so put its metadata in the machine node.
|
|
||||||
machine_node.material_map[root_material_id] = MaterialNode(material_metadata)
|
|
||||||
else:
|
|
||||||
# this material is variant-specific, so we save it in a variant-specific node under the
|
|
||||||
# machine-specific node
|
|
||||||
if variant_name not in machine_node.children_map:
|
|
||||||
machine_node.children_map[variant_name] = MaterialNode()
|
|
||||||
|
|
||||||
variant_node = machine_node.children_map[variant_name]
|
|
||||||
if root_material_id in variant_node.material_map: #We shouldn't have duplicated variant-specific materials for the same machine.
|
|
||||||
ConfigurationErrorMessage.getInstance().addFaultyContainers(root_material_id)
|
|
||||||
continue
|
|
||||||
variant_node.material_map[root_material_id] = MaterialNode(material_metadata)
|
|
||||||
|
|
||||||
self.materialsUpdated.emit()
|
self.materialsUpdated.emit()
|
||||||
|
|
||||||
|
def __addMaterialMetadataIntoLookupTree(self, material_metadata: dict) -> None:
|
||||||
|
material_id = material_metadata["id"]
|
||||||
|
|
||||||
|
# We don't store empty material in the lookup tables
|
||||||
|
if material_id == "empty_material":
|
||||||
|
return
|
||||||
|
|
||||||
|
root_material_id = material_metadata["base_file"]
|
||||||
|
definition = material_metadata["definition"]
|
||||||
|
approximate_diameter = material_metadata["approximate_diameter"]
|
||||||
|
|
||||||
|
if approximate_diameter not in self._diameter_machine_nozzle_buildplate_material_map:
|
||||||
|
self._diameter_machine_nozzle_buildplate_material_map[approximate_diameter] = {}
|
||||||
|
|
||||||
|
machine_nozzle_buildplate_material_map = self._diameter_machine_nozzle_buildplate_material_map[
|
||||||
|
approximate_diameter]
|
||||||
|
if definition not in machine_nozzle_buildplate_material_map:
|
||||||
|
machine_nozzle_buildplate_material_map[definition] = MaterialNode()
|
||||||
|
|
||||||
|
# This is a list of information regarding the intermediate nodes:
|
||||||
|
# nozzle -> buildplate
|
||||||
|
nozzle_name = material_metadata.get("variant_name")
|
||||||
|
buildplate_name = material_metadata.get("buildplate_name")
|
||||||
|
intermediate_node_info_list = [(nozzle_name, VariantType.NOZZLE),
|
||||||
|
(buildplate_name, VariantType.BUILD_PLATE),
|
||||||
|
]
|
||||||
|
|
||||||
|
variant_manager = self._application.getVariantManager()
|
||||||
|
|
||||||
|
machine_node = machine_nozzle_buildplate_material_map[definition]
|
||||||
|
current_node = machine_node
|
||||||
|
current_intermediate_node_info_idx = 0
|
||||||
|
error_message = None # type: Optional[str]
|
||||||
|
while current_intermediate_node_info_idx < len(intermediate_node_info_list):
|
||||||
|
variant_name, variant_type = intermediate_node_info_list[current_intermediate_node_info_idx]
|
||||||
|
if variant_name is not None:
|
||||||
|
# The new material has a specific variant, so it needs to be added to that specific branch in the tree.
|
||||||
|
variant = variant_manager.getVariantNode(definition, variant_name, variant_type)
|
||||||
|
if variant is None:
|
||||||
|
error_message = "Material {id} contains a variant {name} that does not exist.".format(
|
||||||
|
id = material_metadata["id"], name = variant_name)
|
||||||
|
break
|
||||||
|
|
||||||
|
# Update the current node to advance to a more specific branch
|
||||||
|
if variant_name not in current_node.children_map:
|
||||||
|
current_node.children_map[variant_name] = MaterialNode()
|
||||||
|
current_node = current_node.children_map[variant_name]
|
||||||
|
|
||||||
|
current_intermediate_node_info_idx += 1
|
||||||
|
|
||||||
|
if error_message is not None:
|
||||||
|
Logger.log("e", "%s It will not be added into the material lookup tree.", error_message)
|
||||||
|
self._container_registry.addWrongContainerId(material_metadata["id"])
|
||||||
|
return
|
||||||
|
|
||||||
|
# Add the material to the current tree node, which is the deepest (the most specific) branch we can find.
|
||||||
|
# Sanity check: Make sure that there is no duplicated materials.
|
||||||
|
if root_material_id in current_node.material_map:
|
||||||
|
Logger.log("e", "Duplicated material [%s] with root ID [%s]. It has already been added.",
|
||||||
|
material_id, root_material_id)
|
||||||
|
ConfigurationErrorMessage.getInstance().addFaultyContainers(root_material_id)
|
||||||
|
return
|
||||||
|
|
||||||
|
current_node.material_map[root_material_id] = MaterialNode(material_metadata)
|
||||||
|
|
||||||
def _updateMaps(self):
|
def _updateMaps(self):
|
||||||
Logger.log("i", "Updating material lookup data ...")
|
Logger.log("i", "Updating material lookup data ...")
|
||||||
self.initialize()
|
self.initialize()
|
||||||
|
|
@ -246,44 +289,52 @@ class MaterialManager(QObject):
|
||||||
#
|
#
|
||||||
# Return a dict with all root material IDs (k) and ContainerNodes (v) that's suitable for the given setup.
|
# Return a dict with all root material IDs (k) and ContainerNodes (v) that's suitable for the given setup.
|
||||||
#
|
#
|
||||||
def getAvailableMaterials(self, machine_definition: "DefinitionContainer", extruder_variant_name: Optional[str],
|
def getAvailableMaterials(self, machine_definition: "DefinitionContainer", nozzle_name: Optional[str],
|
||||||
diameter: float) -> dict:
|
buildplate_name: Optional[str], diameter: float) -> Dict[str, MaterialNode]:
|
||||||
# round the diameter to get the approximate diameter
|
# round the diameter to get the approximate diameter
|
||||||
rounded_diameter = str(round(diameter))
|
rounded_diameter = str(round(diameter))
|
||||||
if rounded_diameter not in self._diameter_machine_variant_material_map:
|
if rounded_diameter not in self._diameter_machine_nozzle_buildplate_material_map:
|
||||||
Logger.log("i", "Cannot find materials with diameter [%s] (rounded to [%s])", diameter, rounded_diameter)
|
Logger.log("i", "Cannot find materials with diameter [%s] (rounded to [%s])", diameter, rounded_diameter)
|
||||||
return dict()
|
return dict()
|
||||||
|
|
||||||
machine_definition_id = machine_definition.getId()
|
machine_definition_id = machine_definition.getId()
|
||||||
|
|
||||||
# If there are variant materials, get the variant material
|
# If there are nozzle-and-or-buildplate materials, get the nozzle-and-or-buildplate material
|
||||||
machine_variant_material_map = self._diameter_machine_variant_material_map[rounded_diameter]
|
machine_nozzle_buildplate_material_map = self._diameter_machine_nozzle_buildplate_material_map[rounded_diameter]
|
||||||
machine_node = machine_variant_material_map.get(machine_definition_id)
|
machine_node = machine_nozzle_buildplate_material_map.get(machine_definition_id)
|
||||||
default_machine_node = machine_variant_material_map.get(self._default_machine_definition_id)
|
default_machine_node = machine_nozzle_buildplate_material_map.get(self._default_machine_definition_id)
|
||||||
variant_node = None
|
nozzle_node = None
|
||||||
if extruder_variant_name is not None and machine_node is not None:
|
buildplate_node = None
|
||||||
variant_node = machine_node.getChildNode(extruder_variant_name)
|
if nozzle_name is not None and machine_node is not None:
|
||||||
|
nozzle_node = machine_node.getChildNode(nozzle_name)
|
||||||
|
# Get buildplate node if possible
|
||||||
|
if nozzle_node is not None and buildplate_name is not None:
|
||||||
|
buildplate_node = nozzle_node.getChildNode(buildplate_name)
|
||||||
|
|
||||||
nodes_to_check = [variant_node, machine_node, default_machine_node]
|
nodes_to_check = [buildplate_node, nozzle_node, machine_node, default_machine_node]
|
||||||
|
|
||||||
# Fallback mechanism of finding materials:
|
# Fallback mechanism of finding materials:
|
||||||
# 1. variant-specific material
|
# 1. buildplate-specific material
|
||||||
# 2. machine-specific material
|
# 2. nozzle-specific material
|
||||||
# 3. generic material (for fdmprinter)
|
# 3. machine-specific material
|
||||||
|
# 4. generic material (for fdmprinter)
|
||||||
machine_exclude_materials = machine_definition.getMetaDataEntry("exclude_materials", [])
|
machine_exclude_materials = machine_definition.getMetaDataEntry("exclude_materials", [])
|
||||||
|
|
||||||
material_id_metadata_dict = dict()
|
material_id_metadata_dict = dict() # type: Dict[str, MaterialNode]
|
||||||
for node in nodes_to_check:
|
for current_node in nodes_to_check:
|
||||||
if node is not None:
|
if current_node is None:
|
||||||
for material_id, node in node.material_map.items():
|
continue
|
||||||
fallback_id = self.getFallbackMaterialIdByMaterialType(node.metadata["material"])
|
|
||||||
if fallback_id in machine_exclude_materials:
|
|
||||||
Logger.log("d", "Exclude material [%s] for machine [%s]",
|
|
||||||
material_id, machine_definition.getId())
|
|
||||||
continue
|
|
||||||
|
|
||||||
if material_id not in material_id_metadata_dict:
|
# Only exclude the materials that are explicitly specified in the "exclude_materials" field.
|
||||||
material_id_metadata_dict[material_id] = node
|
# Do not exclude other materials that are of the same type.
|
||||||
|
for material_id, node in current_node.material_map.items():
|
||||||
|
if material_id in machine_exclude_materials:
|
||||||
|
Logger.log("d", "Exclude material [%s] for machine [%s]",
|
||||||
|
material_id, machine_definition.getId())
|
||||||
|
continue
|
||||||
|
|
||||||
|
if material_id not in material_id_metadata_dict:
|
||||||
|
material_id_metadata_dict[material_id] = node
|
||||||
|
|
||||||
return material_id_metadata_dict
|
return material_id_metadata_dict
|
||||||
|
|
||||||
|
|
@ -292,13 +343,14 @@ class MaterialManager(QObject):
|
||||||
#
|
#
|
||||||
def getAvailableMaterialsForMachineExtruder(self, machine: "GlobalStack",
|
def getAvailableMaterialsForMachineExtruder(self, machine: "GlobalStack",
|
||||||
extruder_stack: "ExtruderStack") -> Optional[dict]:
|
extruder_stack: "ExtruderStack") -> Optional[dict]:
|
||||||
variant_name = None
|
buildplate_name = machine.getBuildplateName()
|
||||||
|
nozzle_name = None
|
||||||
if extruder_stack.variant.getId() != "empty_variant":
|
if extruder_stack.variant.getId() != "empty_variant":
|
||||||
variant_name = extruder_stack.variant.getName()
|
nozzle_name = extruder_stack.variant.getName()
|
||||||
diameter = extruder_stack.approximateMaterialDiameter
|
diameter = extruder_stack.approximateMaterialDiameter
|
||||||
|
|
||||||
# Fetch the available materials (ContainerNode) for the current active machine and extruder setup.
|
# Fetch the available materials (ContainerNode) for the current active machine and extruder setup.
|
||||||
return self.getAvailableMaterials(machine.definition, variant_name, diameter)
|
return self.getAvailableMaterials(machine.definition, nozzle_name, buildplate_name, diameter)
|
||||||
|
|
||||||
#
|
#
|
||||||
# Gets MaterialNode for the given extruder and machine with the given material name.
|
# Gets MaterialNode for the given extruder and machine with the given material name.
|
||||||
|
|
@ -306,32 +358,36 @@ class MaterialManager(QObject):
|
||||||
# 1. the given machine doesn't have materials;
|
# 1. the given machine doesn't have materials;
|
||||||
# 2. cannot find any material InstanceContainers with the given settings.
|
# 2. cannot find any material InstanceContainers with the given settings.
|
||||||
#
|
#
|
||||||
def getMaterialNode(self, machine_definition_id: str, extruder_variant_name: Optional[str],
|
def getMaterialNode(self, machine_definition_id: str, nozzle_name: Optional[str],
|
||||||
diameter: float, root_material_id: str) -> Optional["InstanceContainer"]:
|
buildplate_name: Optional[str], diameter: float, root_material_id: str) -> Optional["InstanceContainer"]:
|
||||||
# round the diameter to get the approximate diameter
|
# round the diameter to get the approximate diameter
|
||||||
rounded_diameter = str(round(diameter))
|
rounded_diameter = str(round(diameter))
|
||||||
if rounded_diameter not in self._diameter_machine_variant_material_map:
|
if rounded_diameter not in self._diameter_machine_nozzle_buildplate_material_map:
|
||||||
Logger.log("i", "Cannot find materials with diameter [%s] (rounded to [%s]) for root material id [%s]",
|
Logger.log("i", "Cannot find materials with diameter [%s] (rounded to [%s]) for root material id [%s]",
|
||||||
diameter, rounded_diameter, root_material_id)
|
diameter, rounded_diameter, root_material_id)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# If there are variant materials, get the variant material
|
# If there are nozzle materials, get the nozzle-specific material
|
||||||
machine_variant_material_map = self._diameter_machine_variant_material_map[rounded_diameter]
|
machine_nozzle_buildplate_material_map = self._diameter_machine_nozzle_buildplate_material_map[rounded_diameter]
|
||||||
machine_node = machine_variant_material_map.get(machine_definition_id)
|
machine_node = machine_nozzle_buildplate_material_map.get(machine_definition_id)
|
||||||
variant_node = None
|
nozzle_node = None
|
||||||
|
buildplate_node = None
|
||||||
|
|
||||||
# Fallback for "fdmprinter" if the machine-specific materials cannot be found
|
# Fallback for "fdmprinter" if the machine-specific materials cannot be found
|
||||||
if machine_node is None:
|
if machine_node is None:
|
||||||
machine_node = machine_variant_material_map.get(self._default_machine_definition_id)
|
machine_node = machine_nozzle_buildplate_material_map.get(self._default_machine_definition_id)
|
||||||
if machine_node is not None and extruder_variant_name is not None:
|
if machine_node is not None and nozzle_name is not None:
|
||||||
variant_node = machine_node.getChildNode(extruder_variant_name)
|
nozzle_node = machine_node.getChildNode(nozzle_name)
|
||||||
|
if nozzle_node is not None and buildplate_name is not None:
|
||||||
|
buildplate_node = nozzle_node.getChildNode(buildplate_name)
|
||||||
|
|
||||||
# Fallback mechanism of finding materials:
|
# Fallback mechanism of finding materials:
|
||||||
# 1. variant-specific material
|
# 1. buildplate-specific material
|
||||||
# 2. machine-specific material
|
# 2. nozzle-specific material
|
||||||
# 3. generic material (for fdmprinter)
|
# 3. machine-specific material
|
||||||
nodes_to_check = [variant_node, machine_node,
|
# 4. generic material (for fdmprinter)
|
||||||
machine_variant_material_map.get(self._default_machine_definition_id)]
|
nodes_to_check = [buildplate_node, nozzle_node, machine_node,
|
||||||
|
machine_nozzle_buildplate_material_map.get(self._default_machine_definition_id)]
|
||||||
|
|
||||||
material_node = None
|
material_node = None
|
||||||
for node in nodes_to_check:
|
for node in nodes_to_check:
|
||||||
|
|
@ -348,26 +404,27 @@ class MaterialManager(QObject):
|
||||||
# 1. the given machine doesn't have materials;
|
# 1. the given machine doesn't have materials;
|
||||||
# 2. cannot find any material InstanceContainers with the given settings.
|
# 2. cannot find any material InstanceContainers with the given settings.
|
||||||
#
|
#
|
||||||
def getMaterialNodeByType(self, global_stack: "GlobalStack", extruder_variant_name: str, material_guid: str) -> Optional["MaterialNode"]:
|
def getMaterialNodeByType(self, global_stack: "GlobalStack", position: str, nozzle_name: str,
|
||||||
|
buildplate_name: Optional[str], material_guid: str) -> Optional["MaterialNode"]:
|
||||||
node = None
|
node = None
|
||||||
machine_definition = global_stack.definition
|
machine_definition = global_stack.definition
|
||||||
|
extruder_definition = global_stack.extruders[position].definition
|
||||||
if parseBool(machine_definition.getMetaDataEntry("has_materials", False)):
|
if parseBool(machine_definition.getMetaDataEntry("has_materials", False)):
|
||||||
material_diameter = machine_definition.getProperty("material_diameter", "value")
|
material_diameter = extruder_definition.getProperty("material_diameter", "value")
|
||||||
if isinstance(material_diameter, SettingFunction):
|
if isinstance(material_diameter, SettingFunction):
|
||||||
material_diameter = material_diameter(global_stack)
|
material_diameter = material_diameter(global_stack)
|
||||||
|
|
||||||
# Look at the guid to material dictionary
|
# Look at the guid to material dictionary
|
||||||
root_material_id = None
|
root_material_id = None
|
||||||
for material_group in self._guid_material_groups_map[material_guid]:
|
for material_group in self._guid_material_groups_map[material_guid]:
|
||||||
if material_group.is_read_only:
|
root_material_id = material_group.root_material_node.metadata["id"]
|
||||||
root_material_id = material_group.root_material_node.metadata["id"]
|
break
|
||||||
break
|
|
||||||
|
|
||||||
if not root_material_id:
|
if not root_material_id:
|
||||||
Logger.log("i", "Cannot find materials with guid [%s] ", material_guid)
|
Logger.log("i", "Cannot find materials with guid [%s] ", material_guid)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
node = self.getMaterialNode(machine_definition.getId(), extruder_variant_name,
|
node = self.getMaterialNode(machine_definition.getId(), nozzle_name, buildplate_name,
|
||||||
material_diameter, root_material_id)
|
material_diameter, root_material_id)
|
||||||
return node
|
return node
|
||||||
|
|
||||||
|
|
@ -395,17 +452,26 @@ class MaterialManager(QObject):
|
||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def getDefaultMaterial(self, global_stack: "GlobalStack", extruder_variant_name: Optional[str]) -> Optional["MaterialNode"]:
|
## Get default material for given global stack, extruder position and extruder nozzle name
|
||||||
|
# you can provide the extruder_definition and then the position is ignored (useful when building up global stack in CuraStackBuilder)
|
||||||
|
def getDefaultMaterial(self, global_stack: "GlobalStack", position: str, nozzle_name: Optional[str],
|
||||||
|
extruder_definition: Optional["DefinitionContainer"] = None) -> Optional["MaterialNode"]:
|
||||||
node = None
|
node = None
|
||||||
|
|
||||||
|
buildplate_name = global_stack.getBuildplateName()
|
||||||
machine_definition = global_stack.definition
|
machine_definition = global_stack.definition
|
||||||
if parseBool(global_stack.getMetaDataEntry("has_materials", False)):
|
if extruder_definition is None:
|
||||||
material_diameter = machine_definition.getProperty("material_diameter", "value")
|
extruder_definition = global_stack.extruders[position].definition
|
||||||
|
|
||||||
|
if extruder_definition and parseBool(global_stack.getMetaDataEntry("has_materials", False)):
|
||||||
|
# At this point the extruder_definition is not None
|
||||||
|
material_diameter = extruder_definition.getProperty("material_diameter", "value")
|
||||||
if isinstance(material_diameter, SettingFunction):
|
if isinstance(material_diameter, SettingFunction):
|
||||||
material_diameter = material_diameter(global_stack)
|
material_diameter = material_diameter(global_stack)
|
||||||
approximate_material_diameter = str(round(material_diameter))
|
approximate_material_diameter = str(round(material_diameter))
|
||||||
root_material_id = machine_definition.getMetaDataEntry("preferred_material")
|
root_material_id = machine_definition.getMetaDataEntry("preferred_material")
|
||||||
root_material_id = self.getRootMaterialIDForDiameter(root_material_id, approximate_material_diameter)
|
root_material_id = self.getRootMaterialIDForDiameter(root_material_id, approximate_material_diameter)
|
||||||
node = self.getMaterialNode(machine_definition.getId(), extruder_variant_name,
|
node = self.getMaterialNode(machine_definition.getId(), nozzle_name, buildplate_name,
|
||||||
material_diameter, root_material_id)
|
material_diameter, root_material_id)
|
||||||
return node
|
return node
|
||||||
|
|
||||||
|
|
@ -417,7 +483,7 @@ class MaterialManager(QObject):
|
||||||
|
|
||||||
nodes_to_remove = [material_group.root_material_node] + material_group.derived_material_node_list
|
nodes_to_remove = [material_group.root_material_node] + material_group.derived_material_node_list
|
||||||
for node in nodes_to_remove:
|
for node in nodes_to_remove:
|
||||||
self._container_registry.removeContainer(node.metadata["id"])
|
self._container_registry.removeContainer(node.getMetaDataEntry("id", ""))
|
||||||
|
|
||||||
#
|
#
|
||||||
# Methods for GUI
|
# Methods for GUI
|
||||||
|
|
@ -428,22 +494,27 @@ class MaterialManager(QObject):
|
||||||
#
|
#
|
||||||
@pyqtSlot("QVariant", str)
|
@pyqtSlot("QVariant", str)
|
||||||
def setMaterialName(self, material_node: "MaterialNode", name: str):
|
def setMaterialName(self, material_node: "MaterialNode", name: str):
|
||||||
root_material_id = material_node.metadata["base_file"]
|
root_material_id = material_node.getMetaDataEntry("base_file")
|
||||||
|
if root_material_id is None:
|
||||||
|
return
|
||||||
if self._container_registry.isReadOnly(root_material_id):
|
if self._container_registry.isReadOnly(root_material_id):
|
||||||
Logger.log("w", "Cannot set name of read-only container %s.", root_material_id)
|
Logger.log("w", "Cannot set name of read-only container %s.", root_material_id)
|
||||||
return
|
return
|
||||||
|
|
||||||
material_group = self.getMaterialGroup(root_material_id)
|
material_group = self.getMaterialGroup(root_material_id)
|
||||||
if material_group:
|
if material_group:
|
||||||
material_group.root_material_node.getContainer().setName(name)
|
container = material_group.root_material_node.getContainer()
|
||||||
|
if container:
|
||||||
|
container.setName(name)
|
||||||
|
|
||||||
#
|
#
|
||||||
# Removes the given material.
|
# Removes the given material.
|
||||||
#
|
#
|
||||||
@pyqtSlot("QVariant")
|
@pyqtSlot("QVariant")
|
||||||
def removeMaterial(self, material_node: "MaterialNode"):
|
def removeMaterial(self, material_node: "MaterialNode"):
|
||||||
root_material_id = material_node.metadata["base_file"]
|
root_material_id = material_node.getMetaDataEntry("base_file")
|
||||||
self.removeMaterialByRootId(root_material_id)
|
if root_material_id is not None:
|
||||||
|
self.removeMaterialByRootId(root_material_id)
|
||||||
|
|
||||||
#
|
#
|
||||||
# Creates a duplicate of a material, which has the same GUID and base_file metadata.
|
# Creates a duplicate of a material, which has the same GUID and base_file metadata.
|
||||||
|
|
@ -487,8 +558,8 @@ class MaterialManager(QObject):
|
||||||
if container_to_copy.getMetaDataEntry("definition") != "fdmprinter":
|
if container_to_copy.getMetaDataEntry("definition") != "fdmprinter":
|
||||||
new_id += "_" + container_to_copy.getMetaDataEntry("definition")
|
new_id += "_" + container_to_copy.getMetaDataEntry("definition")
|
||||||
if container_to_copy.getMetaDataEntry("variant_name"):
|
if container_to_copy.getMetaDataEntry("variant_name"):
|
||||||
variant_name = container_to_copy.getMetaDataEntry("variant_name")
|
nozzle_name = container_to_copy.getMetaDataEntry("variant_name")
|
||||||
new_id += "_" + variant_name.replace(" ", "_")
|
new_id += "_" + nozzle_name.replace(" ", "_")
|
||||||
|
|
||||||
new_container = copy.deepcopy(container_to_copy)
|
new_container = copy.deepcopy(container_to_copy)
|
||||||
new_container.getMetaData()["id"] = new_id
|
new_container.getMetaData()["id"] = new_id
|
||||||
|
|
@ -522,6 +593,10 @@ class MaterialManager(QObject):
|
||||||
root_material_id = self.getRootMaterialIDForDiameter(root_material_id, approximate_diameter)
|
root_material_id = self.getRootMaterialIDForDiameter(root_material_id, approximate_diameter)
|
||||||
material_group = self.getMaterialGroup(root_material_id)
|
material_group = self.getMaterialGroup(root_material_id)
|
||||||
|
|
||||||
|
if not material_group: # This should never happen
|
||||||
|
Logger.log("w", "Cannot get the material group of %s.", root_material_id)
|
||||||
|
return ""
|
||||||
|
|
||||||
# Create a new ID & container to hold the data.
|
# Create a new ID & container to hold the data.
|
||||||
new_id = self._container_registry.uniqueName("custom_material")
|
new_id = self._container_registry.uniqueName("custom_material")
|
||||||
new_metadata = {"name": catalog.i18nc("@label", "Custom Material"),
|
new_metadata = {"name": catalog.i18nc("@label", "Custom Material"),
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
# Copyright (c) 2018 Ultimaker B.V.
|
# Copyright (c) 2018 Ultimaker B.V.
|
||||||
# Cura is released under the terms of the LGPLv3 or higher.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
from typing import Optional, Dict
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
from .ContainerNode import ContainerNode
|
from .ContainerNode import ContainerNode
|
||||||
|
|
||||||
|
|
@ -15,7 +14,6 @@ from .ContainerNode import ContainerNode
|
||||||
class MaterialNode(ContainerNode):
|
class MaterialNode(ContainerNode):
|
||||||
__slots__ = ("material_map", "children_map")
|
__slots__ = ("material_map", "children_map")
|
||||||
|
|
||||||
def __init__(self, metadata: Optional[dict] = None):
|
def __init__(self, metadata: Optional[dict] = None) -> None:
|
||||||
super().__init__(metadata = metadata)
|
super().__init__(metadata = metadata)
|
||||||
self.material_map = {} # material_root_id -> material_node
|
self.material_map = {} # type: Dict[str, MaterialNode] # material_root_id -> material_node
|
||||||
self.children_map = {} # mapping for the child nodes
|
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,8 @@ class BaseMaterialsModel(ListModel):
|
||||||
|
|
||||||
self._extruder_position = 0
|
self._extruder_position = 0
|
||||||
self._extruder_stack = None
|
self._extruder_stack = None
|
||||||
|
# Update the stack and the model data when the machine changes
|
||||||
|
self._machine_manager.globalContainerChanged.connect(self._updateExtruderStack)
|
||||||
|
|
||||||
def _updateExtruderStack(self):
|
def _updateExtruderStack(self):
|
||||||
global_stack = self._machine_manager.activeMachine
|
global_stack = self._machine_manager.activeMachine
|
||||||
|
|
@ -50,9 +52,11 @@ class BaseMaterialsModel(ListModel):
|
||||||
self._extruder_stack = global_stack.extruders.get(str(self._extruder_position))
|
self._extruder_stack = global_stack.extruders.get(str(self._extruder_position))
|
||||||
if self._extruder_stack is not None:
|
if self._extruder_stack is not None:
|
||||||
self._extruder_stack.pyqtContainersChanged.connect(self._update)
|
self._extruder_stack.pyqtContainersChanged.connect(self._update)
|
||||||
|
# Force update the model when the extruder stack changes
|
||||||
|
self._update()
|
||||||
|
|
||||||
def setExtruderPosition(self, position: int):
|
def setExtruderPosition(self, position: int):
|
||||||
if self._extruder_position != position:
|
if self._extruder_stack is None or self._extruder_position != position:
|
||||||
self._extruder_position = position
|
self._extruder_position = position
|
||||||
self._updateExtruderStack()
|
self._updateExtruderStack()
|
||||||
self.extruderPositionChanged.emit()
|
self.extruderPositionChanged.emit()
|
||||||
|
|
|
||||||
|
|
@ -47,22 +47,38 @@ class BrandMaterialsModel(ListModel):
|
||||||
self.addRoleName(self.MaterialsRole, "materials")
|
self.addRoleName(self.MaterialsRole, "materials")
|
||||||
|
|
||||||
self._extruder_position = 0
|
self._extruder_position = 0
|
||||||
|
self._extruder_stack = None
|
||||||
|
|
||||||
from cura.CuraApplication import CuraApplication
|
from cura.CuraApplication import CuraApplication
|
||||||
self._machine_manager = CuraApplication.getInstance().getMachineManager()
|
self._machine_manager = CuraApplication.getInstance().getMachineManager()
|
||||||
self._extruder_manager = CuraApplication.getInstance().getExtruderManager()
|
self._extruder_manager = CuraApplication.getInstance().getExtruderManager()
|
||||||
self._material_manager = CuraApplication.getInstance().getMaterialManager()
|
self._material_manager = CuraApplication.getInstance().getMaterialManager()
|
||||||
|
|
||||||
|
self._machine_manager.globalContainerChanged.connect(self._updateExtruderStack)
|
||||||
self._machine_manager.activeStackChanged.connect(self._update) #Update when switching machines.
|
self._machine_manager.activeStackChanged.connect(self._update) #Update when switching machines.
|
||||||
self._material_manager.materialsUpdated.connect(self._update) #Update when the list of materials changes.
|
self._material_manager.materialsUpdated.connect(self._update) #Update when the list of materials changes.
|
||||||
self._update()
|
self._update()
|
||||||
|
|
||||||
|
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 = global_stack.extruders.get(str(self._extruder_position))
|
||||||
|
if self._extruder_stack is not None:
|
||||||
|
self._extruder_stack.pyqtContainersChanged.connect(self._update)
|
||||||
|
# Force update the model when the extruder stack changes
|
||||||
|
self._update()
|
||||||
|
|
||||||
def setExtruderPosition(self, position: int):
|
def setExtruderPosition(self, position: int):
|
||||||
if self._extruder_position != position:
|
if self._extruder_stack is None or self._extruder_position != position:
|
||||||
self._extruder_position = position
|
self._extruder_position = position
|
||||||
|
self._updateExtruderStack()
|
||||||
self.extruderPositionChanged.emit()
|
self.extruderPositionChanged.emit()
|
||||||
|
|
||||||
@pyqtProperty(int, fset = setExtruderPosition, notify = extruderPositionChanged)
|
@pyqtProperty(int, fset=setExtruderPosition, notify=extruderPositionChanged)
|
||||||
def extruderPosition(self) -> int:
|
def extruderPosition(self) -> int:
|
||||||
return self._extruder_position
|
return self._extruder_position
|
||||||
|
|
||||||
|
|
@ -93,6 +109,10 @@ class BrandMaterialsModel(ListModel):
|
||||||
if brand.lower() == "generic":
|
if brand.lower() == "generic":
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
# Do not include the materials from a to-be-removed package
|
||||||
|
if bool(metadata.get("removed", False)):
|
||||||
|
continue
|
||||||
|
|
||||||
if brand not in brand_group_dict:
|
if brand not in brand_group_dict:
|
||||||
brand_group_dict[brand] = {}
|
brand_group_dict[brand] = {}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ from UM.Logger import Logger
|
||||||
from UM.Qt.ListModel import ListModel
|
from UM.Qt.ListModel import ListModel
|
||||||
from UM.Util import parseBool
|
from UM.Util import parseBool
|
||||||
|
|
||||||
from cura.Machines.VariantManager import VariantType
|
from cura.Machines.VariantType import VariantType
|
||||||
|
|
||||||
|
|
||||||
class BuildPlateModel(ListModel):
|
class BuildPlateModel(ListModel):
|
||||||
|
|
|
||||||
|
|
@ -41,10 +41,15 @@ class GenericMaterialsModel(BaseMaterialsModel):
|
||||||
item_list = []
|
item_list = []
|
||||||
for root_material_id, container_node in available_material_dict.items():
|
for root_material_id, container_node in available_material_dict.items():
|
||||||
metadata = container_node.metadata
|
metadata = container_node.metadata
|
||||||
|
|
||||||
# Only add results for generic materials
|
# Only add results for generic materials
|
||||||
if metadata["brand"].lower() != "generic":
|
if metadata["brand"].lower() != "generic":
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
# Do not include the materials from a to-be-removed package
|
||||||
|
if bool(metadata.get("removed", False)):
|
||||||
|
continue
|
||||||
|
|
||||||
item = {"root_material_id": root_material_id,
|
item = {"root_material_id": root_material_id,
|
||||||
"id": metadata["id"],
|
"id": metadata["id"],
|
||||||
"name": metadata["name"],
|
"name": metadata["name"],
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,8 @@ from UM.Logger import Logger
|
||||||
from UM.Qt.ListModel import ListModel
|
from UM.Qt.ListModel import ListModel
|
||||||
from UM.Util import parseBool
|
from UM.Util import parseBool
|
||||||
|
|
||||||
|
from cura.Machines.VariantType import VariantType
|
||||||
|
|
||||||
|
|
||||||
class NozzleModel(ListModel):
|
class NozzleModel(ListModel):
|
||||||
IdRole = Qt.UserRole + 1
|
IdRole = Qt.UserRole + 1
|
||||||
|
|
@ -43,7 +45,6 @@ class NozzleModel(ListModel):
|
||||||
self.setItems([])
|
self.setItems([])
|
||||||
return
|
return
|
||||||
|
|
||||||
from cura.Machines.VariantManager import VariantType
|
|
||||||
variant_node_dict = self._variant_manager.getVariantNodes(global_stack, VariantType.NOZZLE)
|
variant_node_dict = self._variant_manager.getVariantNodes(global_stack, VariantType.NOZZLE)
|
||||||
if not variant_node_dict:
|
if not variant_node_dict:
|
||||||
self.setItems([])
|
self.setItems([])
|
||||||
|
|
|
||||||
|
|
@ -83,7 +83,7 @@ class QualityProfilesDropDownMenuModel(ListModel):
|
||||||
|
|
||||||
self.setItems(item_list)
|
self.setItems(item_list)
|
||||||
|
|
||||||
def _fetchLayerHeight(self, quality_group: "QualityGroup"):
|
def _fetchLayerHeight(self, quality_group: "QualityGroup") -> float:
|
||||||
global_stack = self._machine_manager.activeMachine
|
global_stack = self._machine_manager.activeMachine
|
||||||
if not self._layer_height_unit:
|
if not self._layer_height_unit:
|
||||||
unit = global_stack.definition.getProperty("layer_height", "unit")
|
unit = global_stack.definition.getProperty("layer_height", "unit")
|
||||||
|
|
@ -94,14 +94,16 @@ class QualityProfilesDropDownMenuModel(ListModel):
|
||||||
default_layer_height = global_stack.definition.getProperty("layer_height", "value")
|
default_layer_height = global_stack.definition.getProperty("layer_height", "value")
|
||||||
|
|
||||||
# Get layer_height from the quality profile for the GlobalStack
|
# Get layer_height from the quality profile for the GlobalStack
|
||||||
|
if quality_group.node_for_global is None:
|
||||||
|
return float(default_layer_height)
|
||||||
container = quality_group.node_for_global.getContainer()
|
container = quality_group.node_for_global.getContainer()
|
||||||
|
|
||||||
layer_height = default_layer_height
|
layer_height = default_layer_height
|
||||||
if container.hasProperty("layer_height", "value"):
|
if container and container.hasProperty("layer_height", "value"):
|
||||||
layer_height = container.getProperty("layer_height", "value")
|
layer_height = container.getProperty("layer_height", "value")
|
||||||
else:
|
else:
|
||||||
# Look for layer_height in the GlobalStack from material -> definition
|
# Look for layer_height in the GlobalStack from material -> definition
|
||||||
container = global_stack.definition
|
container = global_stack.definition
|
||||||
if container.hasProperty("layer_height", "value"):
|
if container and container.hasProperty("layer_height", "value"):
|
||||||
layer_height = container.getProperty("layer_height", "value")
|
layer_height = container.getProperty("layer_height", "value")
|
||||||
return float(layer_height)
|
return float(layer_height)
|
||||||
|
|
|
||||||
|
|
@ -8,9 +8,9 @@ from configparser import ConfigParser
|
||||||
|
|
||||||
from PyQt5.QtCore import pyqtProperty, Qt, pyqtSignal, pyqtSlot
|
from PyQt5.QtCore import pyqtProperty, Qt, pyqtSignal, pyqtSlot
|
||||||
|
|
||||||
|
from UM.Application import Application
|
||||||
from UM.Logger import Logger
|
from UM.Logger import Logger
|
||||||
from UM.Qt.ListModel import ListModel
|
from UM.Qt.ListModel import ListModel
|
||||||
from UM.Preferences import Preferences
|
|
||||||
from UM.Resources import Resources
|
from UM.Resources import Resources
|
||||||
from UM.MimeTypeDatabase import MimeTypeDatabase, MimeTypeNotFoundError
|
from UM.MimeTypeDatabase import MimeTypeDatabase, MimeTypeNotFoundError
|
||||||
|
|
||||||
|
|
@ -33,7 +33,7 @@ class SettingVisibilityPresetsModel(ListModel):
|
||||||
basic_item = self.items[1]
|
basic_item = self.items[1]
|
||||||
basic_visibile_settings = ";".join(basic_item["settings"])
|
basic_visibile_settings = ";".join(basic_item["settings"])
|
||||||
|
|
||||||
self._preferences = Preferences.getInstance()
|
self._preferences = Application.getInstance().getPreferences()
|
||||||
# Preference to store which preset is currently selected
|
# Preference to store which preset is currently selected
|
||||||
self._preferences.addPreference("cura/active_setting_visibility_preset", "basic")
|
self._preferences.addPreference("cura/active_setting_visibility_preset", "basic")
|
||||||
# Preference that stores the "custom" set so it can always be restored (even after a restart)
|
# Preference that stores the "custom" set so it can always be restored (even after a restart)
|
||||||
|
|
|
||||||
|
|
@ -1,22 +1,27 @@
|
||||||
# Copyright (c) 2018 Ultimaker B.V.
|
# Copyright (c) 2018 Ultimaker B.V.
|
||||||
# Cura is released under the terms of the LGPLv3 or higher.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from UM.Application import Application
|
from UM.Application import Application
|
||||||
from UM.ConfigurationErrorMessage import ConfigurationErrorMessage
|
from UM.ConfigurationErrorMessage import ConfigurationErrorMessage
|
||||||
|
|
||||||
from .QualityGroup import QualityGroup
|
from .QualityGroup import QualityGroup
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from cura.Machines.QualityNode import QualityNode
|
||||||
|
|
||||||
|
|
||||||
class QualityChangesGroup(QualityGroup):
|
class QualityChangesGroup(QualityGroup):
|
||||||
def __init__(self, name: str, quality_type: str, parent = None):
|
def __init__(self, name: str, quality_type: str, parent = None) -> None:
|
||||||
super().__init__(name, quality_type, parent)
|
super().__init__(name, quality_type, parent)
|
||||||
self._container_registry = Application.getInstance().getContainerRegistry()
|
self._container_registry = Application.getInstance().getContainerRegistry()
|
||||||
|
|
||||||
def addNode(self, node: "QualityNode"):
|
def addNode(self, node: "QualityNode"):
|
||||||
extruder_position = node.metadata.get("position")
|
extruder_position = node.getMetaDataEntry("position")
|
||||||
|
|
||||||
if extruder_position is None and self.node_for_global is not None or extruder_position in self.nodes_for_extruders: #We would be overwriting another node.
|
if extruder_position is None and self.node_for_global is not None or extruder_position in self.nodes_for_extruders: #We would be overwriting another node.
|
||||||
ConfigurationErrorMessage.getInstance().addFaultyContainers(node.metadata["id"])
|
ConfigurationErrorMessage.getInstance().addFaultyContainers(node.getMetaDataEntry("id"))
|
||||||
return
|
return
|
||||||
|
|
||||||
if extruder_position is None: #Then we're a global quality changes profile.
|
if extruder_position is None: #Then we're a global quality changes profile.
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
# Copyright (c) 2018 Ultimaker B.V.
|
# Copyright (c) 2018 Ultimaker B.V.
|
||||||
# Cura is released under the terms of the LGPLv3 or higher.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
from typing import Dict, Optional, List
|
from typing import Dict, Optional, List, Set
|
||||||
|
|
||||||
from PyQt5.QtCore import QObject, pyqtSlot
|
from PyQt5.QtCore import QObject, pyqtSlot
|
||||||
|
from cura.Machines.ContainerNode import ContainerNode
|
||||||
|
|
||||||
#
|
#
|
||||||
# A QualityGroup represents a group of containers that must be applied to each ContainerStack when it's used.
|
# A QualityGroup represents a group of containers that must be applied to each ContainerStack when it's used.
|
||||||
|
|
@ -21,11 +21,11 @@ from PyQt5.QtCore import QObject, pyqtSlot
|
||||||
#
|
#
|
||||||
class QualityGroup(QObject):
|
class QualityGroup(QObject):
|
||||||
|
|
||||||
def __init__(self, name: str, quality_type: str, parent = None):
|
def __init__(self, name: str, quality_type: str, parent = None) -> None:
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
self.name = name
|
self.name = name
|
||||||
self.node_for_global = None # type: Optional["QualityGroup"]
|
self.node_for_global = None # type: Optional[ContainerNode]
|
||||||
self.nodes_for_extruders = {} # type: Dict[int, "QualityGroup"]
|
self.nodes_for_extruders = {} # type: Dict[int, ContainerNode]
|
||||||
self.quality_type = quality_type
|
self.quality_type = quality_type
|
||||||
self.is_available = False
|
self.is_available = False
|
||||||
|
|
||||||
|
|
@ -33,15 +33,17 @@ class QualityGroup(QObject):
|
||||||
def getName(self) -> str:
|
def getName(self) -> str:
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
def getAllKeys(self) -> set:
|
def getAllKeys(self) -> Set[str]:
|
||||||
result = set()
|
result = set() #type: Set[str]
|
||||||
for node in [self.node_for_global] + list(self.nodes_for_extruders.values()):
|
for node in [self.node_for_global] + list(self.nodes_for_extruders.values()):
|
||||||
if node is None:
|
if node is None:
|
||||||
continue
|
continue
|
||||||
result.update(node.getContainer().getAllKeys())
|
container = node.getContainer()
|
||||||
|
if container:
|
||||||
|
result.update(container.getAllKeys())
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def getAllNodes(self) -> List["QualityGroup"]:
|
def getAllNodes(self) -> List[ContainerNode]:
|
||||||
result = []
|
result = []
|
||||||
if self.node_for_global is not None:
|
if self.node_for_global is not None:
|
||||||
result.append(self.node_for_global)
|
result.append(self.node_for_global)
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
# Copyright (c) 2018 Ultimaker B.V.
|
# Copyright (c) 2018 Ultimaker B.V.
|
||||||
# Cura is released under the terms of the LGPLv3 or higher.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
from typing import TYPE_CHECKING, Optional
|
from typing import TYPE_CHECKING, Optional, cast
|
||||||
|
|
||||||
from PyQt5.QtCore import QObject, QTimer, pyqtSignal, pyqtSlot
|
from PyQt5.QtCore import QObject, QTimer, pyqtSignal, pyqtSlot
|
||||||
|
|
||||||
|
|
@ -45,7 +45,7 @@ class QualityManager(QObject):
|
||||||
self._empty_quality_container = self._application.empty_quality_container
|
self._empty_quality_container = self._application.empty_quality_container
|
||||||
self._empty_quality_changes_container = self._application.empty_quality_changes_container
|
self._empty_quality_changes_container = self._application.empty_quality_changes_container
|
||||||
|
|
||||||
self._machine_variant_material_quality_type_to_quality_dict = {} # for quality lookup
|
self._machine_nozzle_buildplate_material_quality_type_to_quality_dict = {} # for quality lookup
|
||||||
self._machine_quality_type_to_quality_changes_dict = {} # for quality_changes lookup
|
self._machine_quality_type_to_quality_changes_dict = {} # for quality_changes lookup
|
||||||
|
|
||||||
self._default_machine_definition_id = "fdmprinter"
|
self._default_machine_definition_id = "fdmprinter"
|
||||||
|
|
@ -64,10 +64,10 @@ class QualityManager(QObject):
|
||||||
|
|
||||||
def initialize(self):
|
def initialize(self):
|
||||||
# Initialize the lookup tree for quality profiles with following structure:
|
# Initialize the lookup tree for quality profiles with following structure:
|
||||||
# <machine> -> <variant> -> <material>
|
# <machine> -> <nozzle> -> <buildplate> -> <material>
|
||||||
# -> <material>
|
# <machine> -> <material>
|
||||||
|
|
||||||
self._machine_variant_material_quality_type_to_quality_dict = {} # for quality lookup
|
self._machine_nozzle_buildplate_material_quality_type_to_quality_dict = {} # for quality lookup
|
||||||
self._machine_quality_type_to_quality_changes_dict = {} # for quality_changes lookup
|
self._machine_quality_type_to_quality_changes_dict = {} # for quality_changes lookup
|
||||||
|
|
||||||
quality_metadata_list = self._container_registry.findContainersMetadata(type = "quality")
|
quality_metadata_list = self._container_registry.findContainersMetadata(type = "quality")
|
||||||
|
|
@ -79,53 +79,41 @@ class QualityManager(QObject):
|
||||||
quality_type = metadata["quality_type"]
|
quality_type = metadata["quality_type"]
|
||||||
|
|
||||||
root_material_id = metadata.get("material")
|
root_material_id = metadata.get("material")
|
||||||
variant_name = metadata.get("variant")
|
nozzle_name = metadata.get("variant")
|
||||||
|
buildplate_name = metadata.get("buildplate")
|
||||||
is_global_quality = metadata.get("global_quality", False)
|
is_global_quality = metadata.get("global_quality", False)
|
||||||
is_global_quality = is_global_quality or (root_material_id is None and variant_name is None)
|
is_global_quality = is_global_quality or (root_material_id is None and nozzle_name is None and buildplate_name is None)
|
||||||
|
|
||||||
# Sanity check: material+variant and is_global_quality cannot be present at the same time
|
# Sanity check: material+variant and is_global_quality cannot be present at the same time
|
||||||
if is_global_quality and (root_material_id or variant_name):
|
if is_global_quality and (root_material_id or nozzle_name):
|
||||||
ConfigurationErrorMessage.getInstance().addFaultyContainers(metadata["id"])
|
ConfigurationErrorMessage.getInstance().addFaultyContainers(metadata["id"])
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if definition_id not in self._machine_variant_material_quality_type_to_quality_dict:
|
if definition_id not in self._machine_nozzle_buildplate_material_quality_type_to_quality_dict:
|
||||||
self._machine_variant_material_quality_type_to_quality_dict[definition_id] = QualityNode()
|
self._machine_nozzle_buildplate_material_quality_type_to_quality_dict[definition_id] = QualityNode()
|
||||||
machine_node = self._machine_variant_material_quality_type_to_quality_dict[definition_id]
|
machine_node = cast(QualityNode, self._machine_nozzle_buildplate_material_quality_type_to_quality_dict[definition_id])
|
||||||
|
|
||||||
if is_global_quality:
|
if is_global_quality:
|
||||||
# For global qualities, save data in the machine node
|
# For global qualities, save data in the machine node
|
||||||
machine_node.addQualityMetadata(quality_type, metadata)
|
machine_node.addQualityMetadata(quality_type, metadata)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if variant_name is not None:
|
current_node = machine_node
|
||||||
# If variant_name is specified in the quality/quality_changes profile, check if material is specified,
|
intermediate_node_info_list = [nozzle_name, buildplate_name, root_material_id]
|
||||||
# too.
|
current_intermediate_node_info_idx = 0
|
||||||
if variant_name not in machine_node.children_map:
|
|
||||||
machine_node.children_map[variant_name] = QualityNode()
|
|
||||||
variant_node = machine_node.children_map[variant_name]
|
|
||||||
|
|
||||||
if root_material_id is None:
|
while current_intermediate_node_info_idx < len(intermediate_node_info_list):
|
||||||
# If only variant_name is specified but material is not, add the quality/quality_changes metadata
|
node_name = intermediate_node_info_list[current_intermediate_node_info_idx]
|
||||||
# into the current variant node.
|
if node_name is not None:
|
||||||
variant_node.addQualityMetadata(quality_type, metadata)
|
# There is specific information, update the current node to go deeper so we can add this quality
|
||||||
else:
|
# at the most specific branch in the lookup tree.
|
||||||
# If only variant_name and material are both specified, go one level deeper: create a material node
|
if node_name not in current_node.children_map:
|
||||||
# under the current variant node, and then add the quality/quality_changes metadata into the
|
current_node.children_map[node_name] = QualityNode()
|
||||||
# material node.
|
current_node = cast(QualityNode, current_node.children_map[node_name])
|
||||||
if root_material_id not in variant_node.children_map:
|
|
||||||
variant_node.children_map[root_material_id] = QualityNode()
|
|
||||||
material_node = variant_node.children_map[root_material_id]
|
|
||||||
|
|
||||||
material_node.addQualityMetadata(quality_type, metadata)
|
current_intermediate_node_info_idx += 1
|
||||||
|
|
||||||
else:
|
current_node.addQualityMetadata(quality_type, metadata)
|
||||||
# If variant_name is not specified, check if material is specified.
|
|
||||||
if root_material_id is not None:
|
|
||||||
if root_material_id not in machine_node.children_map:
|
|
||||||
machine_node.children_map[root_material_id] = QualityNode()
|
|
||||||
material_node = machine_node.children_map[root_material_id]
|
|
||||||
|
|
||||||
material_node.addQualityMetadata(quality_type, metadata)
|
|
||||||
|
|
||||||
# Initialize the lookup tree for quality_changes profiles with following structure:
|
# Initialize the lookup tree for quality_changes profiles with following structure:
|
||||||
# <machine> -> <quality_type> -> <name>
|
# <machine> -> <quality_type> -> <name>
|
||||||
|
|
@ -163,7 +151,7 @@ class QualityManager(QObject):
|
||||||
def _updateQualityGroupsAvailability(self, machine: "GlobalStack", quality_group_list):
|
def _updateQualityGroupsAvailability(self, machine: "GlobalStack", quality_group_list):
|
||||||
used_extruders = set()
|
used_extruders = set()
|
||||||
for i in range(machine.getProperty("machine_extruder_count", "value")):
|
for i in range(machine.getProperty("machine_extruder_count", "value")):
|
||||||
if machine.extruders[str(i)].isEnabled:
|
if str(i) in machine.extruders and machine.extruders[str(i)].isEnabled:
|
||||||
used_extruders.add(str(i))
|
used_extruders.add(str(i))
|
||||||
|
|
||||||
# Update the "is_available" flag for each quality group.
|
# Update the "is_available" flag for each quality group.
|
||||||
|
|
@ -217,8 +205,8 @@ class QualityManager(QObject):
|
||||||
# To find the quality container for the GlobalStack, check in the following fall-back manner:
|
# To find the quality container for the GlobalStack, check in the following fall-back manner:
|
||||||
# (1) the machine-specific node
|
# (1) the machine-specific node
|
||||||
# (2) the generic node
|
# (2) the generic node
|
||||||
machine_node = self._machine_variant_material_quality_type_to_quality_dict.get(machine_definition_id)
|
machine_node = self._machine_nozzle_buildplate_material_quality_type_to_quality_dict.get(machine_definition_id)
|
||||||
default_machine_node = self._machine_variant_material_quality_type_to_quality_dict.get(self._default_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]
|
nodes_to_check = [machine_node, default_machine_node]
|
||||||
|
|
||||||
# Iterate over all quality_types in the machine node
|
# Iterate over all quality_types in the machine node
|
||||||
|
|
@ -238,16 +226,19 @@ class QualityManager(QObject):
|
||||||
quality_group_dict[quality_type] = quality_group
|
quality_group_dict[quality_type] = quality_group
|
||||||
break
|
break
|
||||||
|
|
||||||
|
buildplate_name = machine.getBuildplateName()
|
||||||
|
|
||||||
# Iterate over all extruders to find quality containers for each extruder
|
# Iterate over all extruders to find quality containers for each extruder
|
||||||
for position, extruder in machine.extruders.items():
|
for position, extruder in machine.extruders.items():
|
||||||
variant_name = None
|
nozzle_name = None
|
||||||
if extruder.variant.getId() != "empty_variant":
|
if extruder.variant.getId() != "empty_variant":
|
||||||
variant_name = extruder.variant.getName()
|
nozzle_name = extruder.variant.getName()
|
||||||
|
|
||||||
# This is a list of root material IDs to use for searching for suitable quality profiles.
|
# This is a list of root material IDs to use for searching for suitable quality profiles.
|
||||||
# The root material IDs in this list are in prioritized order.
|
# The root material IDs in this list are in prioritized order.
|
||||||
root_material_id_list = []
|
root_material_id_list = []
|
||||||
has_material = False # flag indicating whether this extruder has a material assigned
|
has_material = False # flag indicating whether this extruder has a material assigned
|
||||||
|
root_material_id = None
|
||||||
if extruder.material.getId() != "empty_material":
|
if extruder.material.getId() != "empty_material":
|
||||||
has_material = True
|
has_material = True
|
||||||
root_material_id = extruder.material.getMetaDataEntry("base_file")
|
root_material_id = extruder.material.getMetaDataEntry("base_file")
|
||||||
|
|
@ -264,34 +255,39 @@ class QualityManager(QObject):
|
||||||
# Here we construct a list of nodes we want to look for qualities with the highest priority first.
|
# Here we construct a list of nodes we want to look for qualities with the highest priority first.
|
||||||
# The use case is that, when we look for qualities for a machine, we first want to search in the following
|
# The use case is that, when we look for qualities for a machine, we first want to search in the following
|
||||||
# order:
|
# order:
|
||||||
# 1. machine-variant-and-material-specific qualities if exist
|
# 1. machine-nozzle-buildplate-and-material-specific qualities if exist
|
||||||
# 2. machine-variant-specific qualities if exist
|
# 2. machine-nozzle-and-material-specific qualities if exist
|
||||||
# 3. machine-material-specific qualities if exist
|
# 3. machine-nozzle-specific qualities if exist
|
||||||
# 4. machine-specific qualities if exist
|
# 4. machine-material-specific qualities if exist
|
||||||
# 5. generic qualities if exist
|
# 5. machine-specific qualities if exist
|
||||||
|
# 6. generic qualities if exist
|
||||||
# Each points above can be represented as a node in the lookup tree, so here we simply put those nodes into
|
# Each points above can be represented as a node in the lookup tree, so here we simply put those nodes into
|
||||||
# the list with priorities as the order. Later, we just need to loop over each node in this list and fetch
|
# the list with priorities as the order. Later, we just need to loop over each node in this list and fetch
|
||||||
# qualities from there.
|
# qualities from there.
|
||||||
|
node_info_list_0 = [nozzle_name, buildplate_name, root_material_id]
|
||||||
nodes_to_check = []
|
nodes_to_check = []
|
||||||
|
|
||||||
if variant_name:
|
# This function tries to recursively find the deepest (the most specific) branch and add those nodes to
|
||||||
# In this case, we have both a specific variant and a specific material
|
# the search list in the order described above. So, by iterating over that search node list, we first look
|
||||||
variant_node = machine_node.getChildNode(variant_name)
|
# in the more specific branches and then the less specific (generic) ones.
|
||||||
if variant_node and has_material:
|
def addNodesToCheck(node, nodes_to_check_list, node_info_list, node_info_idx):
|
||||||
for root_material_id in root_material_id_list:
|
if node_info_idx < len(node_info_list):
|
||||||
material_node = variant_node.getChildNode(root_material_id)
|
node_name = node_info_list[node_info_idx]
|
||||||
if material_node:
|
if node_name is not None:
|
||||||
nodes_to_check.append(material_node)
|
current_node = node.getChildNode(node_name)
|
||||||
break
|
if current_node is not None and has_material:
|
||||||
nodes_to_check.append(variant_node)
|
addNodesToCheck(current_node, nodes_to_check_list, node_info_list, node_info_idx + 1)
|
||||||
|
|
||||||
# In this case, we only have a specific material but NOT a variant
|
if has_material:
|
||||||
if has_material:
|
for rmid in root_material_id_list:
|
||||||
for root_material_id in root_material_id_list:
|
material_node = node.getChildNode(rmid)
|
||||||
material_node = machine_node.getChildNode(root_material_id)
|
if material_node:
|
||||||
if material_node:
|
nodes_to_check_list.append(material_node)
|
||||||
nodes_to_check.append(material_node)
|
break
|
||||||
break
|
|
||||||
|
nodes_to_check_list.append(node)
|
||||||
|
|
||||||
|
addNodesToCheck(machine_node, nodes_to_check, node_info_list_0, 0)
|
||||||
|
|
||||||
nodes_to_check += [machine_node, default_machine_node]
|
nodes_to_check += [machine_node, default_machine_node]
|
||||||
for node in nodes_to_check:
|
for node in nodes_to_check:
|
||||||
|
|
@ -309,8 +305,8 @@ class QualityManager(QObject):
|
||||||
quality_group_dict[quality_type] = quality_group
|
quality_group_dict[quality_type] = quality_group
|
||||||
|
|
||||||
quality_group = quality_group_dict[quality_type]
|
quality_group = quality_group_dict[quality_type]
|
||||||
quality_group.nodes_for_extruders[position] = quality_node
|
if position not in quality_group.nodes_for_extruders:
|
||||||
break
|
quality_group.nodes_for_extruders[position] = quality_node
|
||||||
|
|
||||||
# Update availabilities for each quality group
|
# Update availabilities for each quality group
|
||||||
self._updateQualityGroupsAvailability(machine, quality_group_dict.values())
|
self._updateQualityGroupsAvailability(machine, quality_group_dict.values())
|
||||||
|
|
@ -323,8 +319,8 @@ class QualityManager(QObject):
|
||||||
# To find the quality container for the GlobalStack, check in the following fall-back manner:
|
# To find the quality container for the GlobalStack, check in the following fall-back manner:
|
||||||
# (1) the machine-specific node
|
# (1) the machine-specific node
|
||||||
# (2) the generic node
|
# (2) the generic node
|
||||||
machine_node = self._machine_variant_material_quality_type_to_quality_dict.get(machine_definition_id)
|
machine_node = self._machine_nozzle_buildplate_material_quality_type_to_quality_dict.get(machine_definition_id)
|
||||||
default_machine_node = self._machine_variant_material_quality_type_to_quality_dict.get(
|
default_machine_node = self._machine_nozzle_buildplate_material_quality_type_to_quality_dict.get(
|
||||||
self._default_machine_definition_id)
|
self._default_machine_definition_id)
|
||||||
nodes_to_check = [machine_node, default_machine_node]
|
nodes_to_check = [machine_node, default_machine_node]
|
||||||
|
|
||||||
|
|
@ -340,6 +336,13 @@ class QualityManager(QObject):
|
||||||
|
|
||||||
return quality_group_dict
|
return quality_group_dict
|
||||||
|
|
||||||
|
def getDefaultQualityType(self, machine: "GlobalStack") -> Optional[QualityGroup]:
|
||||||
|
preferred_quality_type = machine.definition.getMetaDataEntry("preferred_quality_type")
|
||||||
|
quality_group_dict = self.getQualityGroups(machine)
|
||||||
|
quality_group = quality_group_dict.get(preferred_quality_type)
|
||||||
|
return quality_group
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Methods for GUI
|
# Methods for GUI
|
||||||
#
|
#
|
||||||
|
|
@ -351,7 +354,7 @@ class QualityManager(QObject):
|
||||||
def removeQualityChangesGroup(self, quality_changes_group: "QualityChangesGroup"):
|
def removeQualityChangesGroup(self, quality_changes_group: "QualityChangesGroup"):
|
||||||
Logger.log("i", "Removing quality changes group [%s]", quality_changes_group.name)
|
Logger.log("i", "Removing quality changes group [%s]", quality_changes_group.name)
|
||||||
for node in quality_changes_group.getAllNodes():
|
for node in quality_changes_group.getAllNodes():
|
||||||
self._container_registry.removeContainer(node.metadata["id"])
|
self._container_registry.removeContainer(node.getMetaDataEntry("id"))
|
||||||
|
|
||||||
#
|
#
|
||||||
# Rename a set of quality changes containers. Returns the new name.
|
# Rename a set of quality changes containers. Returns the new name.
|
||||||
|
|
@ -365,7 +368,9 @@ class QualityManager(QObject):
|
||||||
|
|
||||||
new_name = self._container_registry.uniqueName(new_name)
|
new_name = self._container_registry.uniqueName(new_name)
|
||||||
for node in quality_changes_group.getAllNodes():
|
for node in quality_changes_group.getAllNodes():
|
||||||
node.getContainer().setName(new_name)
|
container = node.getContainer()
|
||||||
|
if container:
|
||||||
|
container.setName(new_name)
|
||||||
|
|
||||||
quality_changes_group.name = new_name
|
quality_changes_group.name = new_name
|
||||||
|
|
||||||
|
|
@ -457,18 +462,18 @@ class QualityManager(QObject):
|
||||||
# Create a new quality_changes container for the quality.
|
# Create a new quality_changes container for the quality.
|
||||||
quality_changes = InstanceContainer(new_id)
|
quality_changes = InstanceContainer(new_id)
|
||||||
quality_changes.setName(new_name)
|
quality_changes.setName(new_name)
|
||||||
quality_changes.addMetaDataEntry("type", "quality_changes")
|
quality_changes.setMetaDataEntry("type", "quality_changes")
|
||||||
quality_changes.addMetaDataEntry("quality_type", quality_type)
|
quality_changes.setMetaDataEntry("quality_type", quality_type)
|
||||||
|
|
||||||
# If we are creating a container for an extruder, ensure we add that to the container
|
# If we are creating a container for an extruder, ensure we add that to the container
|
||||||
if extruder_stack is not None:
|
if extruder_stack is not None:
|
||||||
quality_changes.addMetaDataEntry("position", extruder_stack.getMetaDataEntry("position"))
|
quality_changes.setMetaDataEntry("position", extruder_stack.getMetaDataEntry("position"))
|
||||||
|
|
||||||
# If the machine specifies qualities should be filtered, ensure we match the current criteria.
|
# If the machine specifies qualities should be filtered, ensure we match the current criteria.
|
||||||
machine_definition_id = getMachineDefinitionIDForQualitySearch(machine.definition)
|
machine_definition_id = getMachineDefinitionIDForQualitySearch(machine.definition)
|
||||||
quality_changes.setDefinition(machine_definition_id)
|
quality_changes.setDefinition(machine_definition_id)
|
||||||
|
|
||||||
quality_changes.addMetaDataEntry("setting_version", self._application.SettingVersion)
|
quality_changes.setMetaDataEntry("setting_version", self._application.SettingVersion)
|
||||||
return quality_changes
|
return quality_changes
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
# Copyright (c) 2018 Ultimaker B.V.
|
# Copyright (c) 2018 Ultimaker B.V.
|
||||||
# Cura is released under the terms of the LGPLv3 or higher.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
from typing import Optional
|
from typing import Optional, Dict, cast
|
||||||
|
|
||||||
from .ContainerNode import ContainerNode
|
from .ContainerNode import ContainerNode
|
||||||
from .QualityChangesGroup import QualityChangesGroup
|
from .QualityChangesGroup import QualityChangesGroup
|
||||||
|
|
@ -12,9 +12,9 @@ from .QualityChangesGroup import QualityChangesGroup
|
||||||
#
|
#
|
||||||
class QualityNode(ContainerNode):
|
class QualityNode(ContainerNode):
|
||||||
|
|
||||||
def __init__(self, metadata: Optional[dict] = None):
|
def __init__(self, metadata: Optional[dict] = None) -> None:
|
||||||
super().__init__(metadata = metadata)
|
super().__init__(metadata = metadata)
|
||||||
self.quality_type_map = {} # quality_type -> QualityNode for InstanceContainer
|
self.quality_type_map = {} # type: Dict[str, QualityNode] # quality_type -> QualityNode for InstanceContainer
|
||||||
|
|
||||||
def addQualityMetadata(self, quality_type: str, metadata: dict):
|
def addQualityMetadata(self, quality_type: str, metadata: dict):
|
||||||
if quality_type not in self.quality_type_map:
|
if quality_type not in self.quality_type_map:
|
||||||
|
|
@ -32,4 +32,4 @@ class QualityNode(ContainerNode):
|
||||||
if name not in quality_type_node.children_map:
|
if name not in quality_type_node.children_map:
|
||||||
quality_type_node.children_map[name] = QualityChangesGroup(name, quality_type)
|
quality_type_node.children_map[name] = QualityChangesGroup(name, quality_type)
|
||||||
quality_changes_group = quality_type_node.children_map[name]
|
quality_changes_group = quality_type_node.children_map[name]
|
||||||
quality_changes_group.addNode(QualityNode(metadata))
|
cast(QualityChangesGroup, quality_changes_group).addNode(QualityNode(metadata))
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
# Copyright (c) 2018 Ultimaker B.V.
|
# Copyright (c) 2018 Ultimaker B.V.
|
||||||
# Cura is released under the terms of the LGPLv3 or higher.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
from enum import Enum
|
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
from typing import Optional, TYPE_CHECKING
|
from typing import Optional, TYPE_CHECKING
|
||||||
|
|
||||||
|
|
@ -11,20 +10,13 @@ from UM.Settings.ContainerRegistry import ContainerRegistry
|
||||||
from UM.Util import parseBool
|
from UM.Util import parseBool
|
||||||
|
|
||||||
from cura.Machines.ContainerNode import ContainerNode
|
from cura.Machines.ContainerNode import ContainerNode
|
||||||
|
from cura.Machines.VariantType import VariantType, ALL_VARIANT_TYPES
|
||||||
from cura.Settings.GlobalStack import GlobalStack
|
from cura.Settings.GlobalStack import GlobalStack
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from UM.Settings.DefinitionContainer import DefinitionContainer
|
from UM.Settings.DefinitionContainer import DefinitionContainer
|
||||||
|
|
||||||
|
|
||||||
class VariantType(Enum):
|
|
||||||
BUILD_PLATE = "buildplate"
|
|
||||||
NOZZLE = "nozzle"
|
|
||||||
|
|
||||||
|
|
||||||
ALL_VARIANT_TYPES = (VariantType.BUILD_PLATE, VariantType.NOZZLE)
|
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# VariantManager is THE place to look for a specific variant. It maintains two variant lookup tables with the following
|
# VariantManager is THE place to look for a specific variant. It maintains two variant lookup tables with the following
|
||||||
# structure:
|
# structure:
|
||||||
|
|
@ -74,7 +66,11 @@ class VariantManager:
|
||||||
for variant_type in ALL_VARIANT_TYPES:
|
for variant_type in ALL_VARIANT_TYPES:
|
||||||
self._machine_to_variant_dict_map[variant_definition][variant_type] = dict()
|
self._machine_to_variant_dict_map[variant_definition][variant_type] = dict()
|
||||||
|
|
||||||
variant_type = variant_metadata["hardware_type"]
|
try:
|
||||||
|
variant_type = variant_metadata["hardware_type"]
|
||||||
|
except KeyError:
|
||||||
|
Logger.log("w", "Variant %s does not specify a hardware_type; assuming 'nozzle'", variant_metadata["id"])
|
||||||
|
variant_type = VariantType.NOZZLE
|
||||||
variant_type = VariantType(variant_type)
|
variant_type = VariantType(variant_type)
|
||||||
variant_dict = self._machine_to_variant_dict_map[variant_definition][variant_type]
|
variant_dict = self._machine_to_variant_dict_map[variant_definition][variant_type]
|
||||||
if variant_name in variant_dict:
|
if variant_name in variant_dict:
|
||||||
|
|
|
||||||
15
cura/Machines/VariantType.py
Normal file
15
cura/Machines/VariantType.py
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
# Copyright (c) 2018 Ultimaker B.V.
|
||||||
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
|
from enum import Enum
|
||||||
|
|
||||||
|
|
||||||
|
class VariantType(Enum):
|
||||||
|
BUILD_PLATE = "buildplate"
|
||||||
|
NOZZLE = "nozzle"
|
||||||
|
|
||||||
|
|
||||||
|
ALL_VARIANT_TYPES = (VariantType.BUILD_PLATE, VariantType.NOZZLE)
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = ["VariantType", "ALL_VARIANT_TYPES"]
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
# Copyright (c) 2017 Ultimaker B.V.
|
# Copyright (c) 2018 Ultimaker B.V.
|
||||||
# Cura is released under the terms of the LGPLv3 or higher.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
|
import copy
|
||||||
|
|
||||||
from UM.Job import Job
|
from UM.Job import Job
|
||||||
from UM.Operations.GroupedOperation import GroupedOperation
|
from UM.Operations.GroupedOperation import GroupedOperation
|
||||||
from UM.Message import Message
|
from UM.Message import Message
|
||||||
|
|
@ -30,11 +32,18 @@ class MultiplyObjectsJob(Job):
|
||||||
total_progress = len(self._objects) * self._count
|
total_progress = len(self._objects) * self._count
|
||||||
current_progress = 0
|
current_progress = 0
|
||||||
|
|
||||||
|
global_container_stack = Application.getInstance().getGlobalContainerStack()
|
||||||
|
machine_width = global_container_stack.getProperty("machine_width", "value")
|
||||||
|
machine_depth = global_container_stack.getProperty("machine_depth", "value")
|
||||||
|
|
||||||
root = scene.getRoot()
|
root = scene.getRoot()
|
||||||
arranger = Arrange.create(scene_root=root)
|
scale = 0.5
|
||||||
|
arranger = Arrange.create(x = machine_width, y = machine_depth, scene_root = root, scale = scale, min_offset = self._min_offset)
|
||||||
processed_nodes = []
|
processed_nodes = []
|
||||||
nodes = []
|
nodes = []
|
||||||
|
|
||||||
|
not_fit_count = 0
|
||||||
|
|
||||||
for node in self._objects:
|
for node in self._objects:
|
||||||
# If object is part of a group, multiply group
|
# If object is part of a group, multiply group
|
||||||
current_node = node
|
current_node = node
|
||||||
|
|
@ -46,21 +55,26 @@ class MultiplyObjectsJob(Job):
|
||||||
processed_nodes.append(current_node)
|
processed_nodes.append(current_node)
|
||||||
|
|
||||||
node_too_big = False
|
node_too_big = False
|
||||||
if node.getBoundingBox().width < 300 or node.getBoundingBox().depth < 300:
|
if node.getBoundingBox().width < machine_width or node.getBoundingBox().depth < machine_depth:
|
||||||
offset_shape_arr, hull_shape_arr = ShapeArray.fromNode(current_node, min_offset=self._min_offset)
|
offset_shape_arr, hull_shape_arr = ShapeArray.fromNode(current_node, min_offset = self._min_offset, scale = scale)
|
||||||
else:
|
else:
|
||||||
node_too_big = True
|
node_too_big = True
|
||||||
|
|
||||||
found_solution_for_all = True
|
found_solution_for_all = True
|
||||||
|
arranger.resetLastPriority()
|
||||||
for i in range(self._count):
|
for i in range(self._count):
|
||||||
# We do place the nodes one by one, as we want to yield in between.
|
# We do place the nodes one by one, as we want to yield in between.
|
||||||
|
new_node = copy.deepcopy(node)
|
||||||
|
solution_found = False
|
||||||
if not node_too_big:
|
if not node_too_big:
|
||||||
new_node, solution_found = arranger.findNodePlacement(current_node, offset_shape_arr, hull_shape_arr)
|
solution_found = arranger.findNodePlacement(new_node, offset_shape_arr, hull_shape_arr)
|
||||||
|
|
||||||
if node_too_big or not solution_found:
|
if node_too_big or not solution_found:
|
||||||
found_solution_for_all = False
|
found_solution_for_all = False
|
||||||
new_location = new_node.getPosition()
|
new_location = new_node.getPosition()
|
||||||
new_location = new_location.set(z = 100 - i * 20)
|
new_location = new_location.set(z = - not_fit_count * 20)
|
||||||
new_node.setPosition(new_location)
|
new_node.setPosition(new_location)
|
||||||
|
not_fit_count += 1
|
||||||
|
|
||||||
# Same build plate
|
# Same build plate
|
||||||
build_plate_number = current_node.callDecoration("getBuildPlateNumber")
|
build_plate_number = current_node.callDecoration("getBuildPlateNumber")
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,6 @@ from UM.Qt.ListModel import ListModel
|
||||||
from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
|
from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
|
||||||
from UM.Scene.SceneNode import SceneNode
|
from UM.Scene.SceneNode import SceneNode
|
||||||
from UM.Scene.Selection import Selection
|
from UM.Scene.Selection import Selection
|
||||||
from UM.Preferences import Preferences
|
|
||||||
from UM.i18n import i18nCatalog
|
from UM.i18n import i18nCatalog
|
||||||
|
|
||||||
catalog = i18nCatalog("cura")
|
catalog = i18nCatalog("cura")
|
||||||
|
|
@ -20,7 +19,7 @@ class ObjectsModel(ListModel):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
|
||||||
Application.getInstance().getController().getScene().sceneChanged.connect(self._updateDelayed)
|
Application.getInstance().getController().getScene().sceneChanged.connect(self._updateDelayed)
|
||||||
Preferences.getInstance().preferenceChanged.connect(self._updateDelayed)
|
Application.getInstance().getPreferences().preferenceChanged.connect(self._updateDelayed)
|
||||||
|
|
||||||
self._update_timer = QTimer()
|
self._update_timer = QTimer()
|
||||||
self._update_timer.setInterval(100)
|
self._update_timer.setInterval(100)
|
||||||
|
|
@ -38,7 +37,7 @@ class ObjectsModel(ListModel):
|
||||||
|
|
||||||
def _update(self, *args):
|
def _update(self, *args):
|
||||||
nodes = []
|
nodes = []
|
||||||
filter_current_build_plate = Preferences.getInstance().getValue("view/filter_current_build_plate")
|
filter_current_build_plate = Application.getInstance().getPreferences().getValue("view/filter_current_build_plate")
|
||||||
active_build_plate_number = self._build_plate_number
|
active_build_plate_number = self._build_plate_number
|
||||||
group_nr = 1
|
group_nr = 1
|
||||||
for node in DepthFirstIterator(Application.getInstance().getController().getScene().getRoot()):
|
for node in DepthFirstIterator(Application.getInstance().getController().getScene().getRoot()):
|
||||||
|
|
|
||||||
|
|
@ -1,112 +1,149 @@
|
||||||
# Copyright (c) 2015 Ultimaker B.V.
|
# Copyright (c) 2018 Ultimaker B.V.
|
||||||
# Cura is released under the terms of the LGPLv3 or higher.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
from UM.Scene.Iterator import Iterator
|
import sys
|
||||||
from UM.Scene.SceneNode import SceneNode
|
|
||||||
from functools import cmp_to_key
|
from shapely import affinity
|
||||||
from UM.Application import Application
|
from shapely.geometry import Polygon
|
||||||
|
|
||||||
|
from UM.Scene.Iterator.Iterator import Iterator
|
||||||
|
from UM.Scene.SceneNode import SceneNode
|
||||||
|
|
||||||
|
|
||||||
|
# Iterator that determines the object print order when one-at a time mode is enabled.
|
||||||
|
#
|
||||||
|
# In one-at-a-time mode, only one extruder can be enabled to print. In order to maximize the number of objects we can
|
||||||
|
# print, we need to print from the corner that's closest to the extruder that's being used. Here is an illustration:
|
||||||
|
#
|
||||||
|
# +--------------------------------+
|
||||||
|
# | |
|
||||||
|
# | |
|
||||||
|
# | | - Rectangle represents the complete print head including fans, etc.
|
||||||
|
# | X X | y - X's are the nozzles
|
||||||
|
# | (1) (2) | ^
|
||||||
|
# | | |
|
||||||
|
# +--------------------------------+ +--> x
|
||||||
|
#
|
||||||
|
# In this case, the nozzles are symmetric, nozzle (1) is closer to the bottom left corner while (2) is closer to the
|
||||||
|
# bottom right. If we use nozzle (1) to print, then we better off printing from the bottom left corner so the print
|
||||||
|
# head will not collide into an object on its top-right side, which is a very large unused area. Following the same
|
||||||
|
# logic, if we are printing with nozzle (2), then it's better to print from the bottom-right side.
|
||||||
|
#
|
||||||
|
# This iterator determines the print order following the rules above.
|
||||||
|
#
|
||||||
|
class OneAtATimeIterator(Iterator):
|
||||||
|
|
||||||
## Iterator that returns a list of nodes in the order that they need to be printed
|
|
||||||
# If there is no solution an empty list is returned.
|
|
||||||
# Take note that the list of nodes can have children (that may or may not contain mesh data)
|
|
||||||
class OneAtATimeIterator(Iterator.Iterator):
|
|
||||||
def __init__(self, scene_node):
|
def __init__(self, scene_node):
|
||||||
super().__init__(scene_node) # Call super to make multiple inheritence work.
|
from cura.CuraApplication import CuraApplication
|
||||||
self._hit_map = [[]]
|
self._global_stack = CuraApplication.getInstance().getGlobalContainerStack()
|
||||||
self._original_node_list = []
|
self._original_node_list = []
|
||||||
|
|
||||||
|
super().__init__(scene_node) # Call super to make multiple inheritance work.
|
||||||
|
|
||||||
|
def getMachineNearestCornerToExtruder(self, global_stack):
|
||||||
|
head_and_fans_coordinates = global_stack.getHeadAndFansCoordinates()
|
||||||
|
|
||||||
|
used_extruder = None
|
||||||
|
for extruder in global_stack.extruders.values():
|
||||||
|
if extruder.isEnabled:
|
||||||
|
used_extruder = extruder
|
||||||
|
break
|
||||||
|
|
||||||
|
extruder_offsets = [used_extruder.getProperty("machine_nozzle_offset_x", "value"),
|
||||||
|
used_extruder.getProperty("machine_nozzle_offset_y", "value")]
|
||||||
|
|
||||||
|
# find the corner that's closest to the origin
|
||||||
|
min_distance2 = sys.maxsize
|
||||||
|
min_coord = None
|
||||||
|
for coord in head_and_fans_coordinates:
|
||||||
|
x = coord[0] - extruder_offsets[0]
|
||||||
|
y = coord[1] - extruder_offsets[1]
|
||||||
|
|
||||||
|
distance2 = x**2 + y**2
|
||||||
|
if distance2 <= min_distance2:
|
||||||
|
min_distance2 = distance2
|
||||||
|
min_coord = coord
|
||||||
|
|
||||||
|
return min_coord
|
||||||
|
|
||||||
|
def _checkForCollisions(self) -> bool:
|
||||||
|
all_nodes = []
|
||||||
|
for node in self._scene_node.getChildren():
|
||||||
|
if not issubclass(type(node), SceneNode):
|
||||||
|
continue
|
||||||
|
convex_hull = node.callDecoration("getConvexHullHead")
|
||||||
|
if not convex_hull:
|
||||||
|
continue
|
||||||
|
|
||||||
|
bounding_box = node.getBoundingBox()
|
||||||
|
if not bounding_box:
|
||||||
|
continue
|
||||||
|
from UM.Math.Polygon import Polygon
|
||||||
|
bounding_box_polygon = Polygon([[bounding_box.left, bounding_box.front],
|
||||||
|
[bounding_box.left, bounding_box.back],
|
||||||
|
[bounding_box.right, bounding_box.back],
|
||||||
|
[bounding_box.right, bounding_box.front]])
|
||||||
|
|
||||||
|
all_nodes.append({"node": node,
|
||||||
|
"bounding_box": bounding_box_polygon,
|
||||||
|
"convex_hull": convex_hull})
|
||||||
|
|
||||||
|
has_collisions = False
|
||||||
|
for i, node_dict in enumerate(all_nodes):
|
||||||
|
for j, other_node_dict in enumerate(all_nodes):
|
||||||
|
if i == j:
|
||||||
|
continue
|
||||||
|
if node_dict["bounding_box"].intersectsPolygon(other_node_dict["convex_hull"]):
|
||||||
|
has_collisions = True
|
||||||
|
break
|
||||||
|
|
||||||
|
if has_collisions:
|
||||||
|
break
|
||||||
|
|
||||||
|
return has_collisions
|
||||||
|
|
||||||
def _fillStack(self):
|
def _fillStack(self):
|
||||||
|
min_coord = self.getMachineNearestCornerToExtruder(self._global_stack)
|
||||||
|
transform_x = -int(round(min_coord[0] / abs(min_coord[0])))
|
||||||
|
transform_y = -int(round(min_coord[1] / abs(min_coord[1])))
|
||||||
|
|
||||||
|
machine_size = [self._global_stack.getProperty("machine_width", "value"),
|
||||||
|
self._global_stack.getProperty("machine_depth", "value")]
|
||||||
|
|
||||||
|
def flip_x(polygon):
|
||||||
|
tm2 = [-1, 0, 0, 1, 0, 0]
|
||||||
|
return affinity.affine_transform(affinity.translate(polygon, xoff = -machine_size[0]), tm2)
|
||||||
|
|
||||||
|
def flip_y(polygon):
|
||||||
|
tm2 = [1, 0, 0, -1, 0, 0]
|
||||||
|
return affinity.affine_transform(affinity.translate(polygon, yoff = -machine_size[1]), tm2)
|
||||||
|
|
||||||
|
if self._checkForCollisions():
|
||||||
|
self._node_stack = []
|
||||||
|
return
|
||||||
|
|
||||||
node_list = []
|
node_list = []
|
||||||
for node in self._scene_node.getChildren():
|
for node in self._scene_node.getChildren():
|
||||||
if not issubclass(type(node), SceneNode):
|
if not issubclass(type(node), SceneNode):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if node.callDecoration("getConvexHull"):
|
convex_hull = node.callDecoration("getConvexHull")
|
||||||
node_list.append(node)
|
if convex_hull:
|
||||||
|
xmin = min(x for x, _ in convex_hull._points)
|
||||||
|
xmax = max(x for x, _ in convex_hull._points)
|
||||||
|
ymin = min(y for _, y in convex_hull._points)
|
||||||
|
ymax = max(y for _, y in convex_hull._points)
|
||||||
|
|
||||||
|
convex_hull_polygon = Polygon.from_bounds(xmin, ymin, xmax, ymax)
|
||||||
|
if transform_x < 0:
|
||||||
|
convex_hull_polygon = flip_x(convex_hull_polygon)
|
||||||
|
if transform_y < 0:
|
||||||
|
convex_hull_polygon = flip_y(convex_hull_polygon)
|
||||||
|
|
||||||
if len(node_list) < 2:
|
node_list.append({"node": node,
|
||||||
self._node_stack = node_list[:]
|
"min_coord": [convex_hull_polygon.bounds[0], convex_hull_polygon.bounds[1]],
|
||||||
return
|
})
|
||||||
|
|
||||||
# Copy the list
|
node_list = sorted(node_list, key = lambda d: d["min_coord"])
|
||||||
self._original_node_list = node_list[:]
|
|
||||||
|
|
||||||
## Initialise the hit map (pre-compute all hits between all objects)
|
|
||||||
self._hit_map = [[self._checkHit(i,j) for i in node_list] for j in node_list]
|
|
||||||
|
|
||||||
# Check if we have to files that block eachother. If this is the case, there is no solution!
|
|
||||||
for a in range(0,len(node_list)):
|
|
||||||
for b in range(0,len(node_list)):
|
|
||||||
if a != b and self._hit_map[a][b] and self._hit_map[b][a]:
|
|
||||||
return
|
|
||||||
|
|
||||||
# Sort the original list so that items that block the most other objects are at the beginning.
|
|
||||||
# This does not decrease the worst case running time, but should improve it in most cases.
|
|
||||||
sorted(node_list, key = cmp_to_key(self._calculateScore))
|
|
||||||
|
|
||||||
todo_node_list = [_ObjectOrder([], node_list)]
|
|
||||||
while len(todo_node_list) > 0:
|
|
||||||
current = todo_node_list.pop()
|
|
||||||
for node in current.todo:
|
|
||||||
# Check if the object can be placed with what we have and still allows for a solution in the future
|
|
||||||
if not self._checkHitMultiple(node, current.order) and not self._checkBlockMultiple(node, current.todo):
|
|
||||||
# We found a possible result. Create new todo & order list.
|
|
||||||
new_todo_list = current.todo[:]
|
|
||||||
new_todo_list.remove(node)
|
|
||||||
new_order = current.order[:] + [node]
|
|
||||||
if len(new_todo_list) == 0:
|
|
||||||
# We have no more nodes to check, so quit looking.
|
|
||||||
todo_node_list = None
|
|
||||||
self._node_stack = new_order
|
|
||||||
|
|
||||||
return
|
|
||||||
todo_node_list.append(_ObjectOrder(new_order, new_todo_list))
|
|
||||||
self._node_stack = [] #No result found!
|
|
||||||
|
|
||||||
|
|
||||||
# Check if first object can be printed before the provided list (using the hit map)
|
|
||||||
def _checkHitMultiple(self, node, other_nodes):
|
|
||||||
node_index = self._original_node_list.index(node)
|
|
||||||
for other_node in other_nodes:
|
|
||||||
other_node_index = self._original_node_list.index(other_node)
|
|
||||||
if self._hit_map[node_index][other_node_index]:
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
def _checkBlockMultiple(self, node, other_nodes):
|
|
||||||
node_index = self._original_node_list.index(node)
|
|
||||||
for other_node in other_nodes:
|
|
||||||
other_node_index = self._original_node_list.index(other_node)
|
|
||||||
if self._hit_map[other_node_index][node_index] and node_index != other_node_index:
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
## Calculate score simply sums the number of other objects it 'blocks'
|
|
||||||
def _calculateScore(self, a, b):
|
|
||||||
score_a = sum(self._hit_map[self._original_node_list.index(a)])
|
|
||||||
score_b = sum(self._hit_map[self._original_node_list.index(b)])
|
|
||||||
return score_a - score_b
|
|
||||||
|
|
||||||
# Checks if A can be printed before B
|
|
||||||
def _checkHit(self, a, b):
|
|
||||||
if a == b:
|
|
||||||
return False
|
|
||||||
|
|
||||||
overlap = a.callDecoration("getConvexHullBoundary").intersectsPolygon(b.callDecoration("getConvexHullHeadFull"))
|
|
||||||
if overlap:
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
## Internal object used to keep track of a possible order in which to print objects.
|
|
||||||
class _ObjectOrder():
|
|
||||||
def __init__(self, order, todo):
|
|
||||||
"""
|
|
||||||
:param order: List of indexes in which to print objects, ordered by printing order.
|
|
||||||
:param todo: List of indexes which are not yet inserted into the order list.
|
|
||||||
"""
|
|
||||||
self.order = order
|
|
||||||
self.todo = todo
|
|
||||||
|
|
||||||
|
self._node_stack = [d["node"] for d in node_list]
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,9 @@
|
||||||
# Copyright (c) 2018 Ultimaker B.V.
|
# Copyright (c) 2018 Ultimaker B.V.
|
||||||
# Cura is released under the terms of the LGPLv3 or higher.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
from UM.Application import Application
|
|
||||||
|
from typing import Optional, TYPE_CHECKING
|
||||||
|
|
||||||
|
from UM.Qt.QtApplication import QtApplication
|
||||||
from UM.Math.Vector import Vector
|
from UM.Math.Vector import Vector
|
||||||
from UM.Resources import Resources
|
from UM.Resources import Resources
|
||||||
|
|
||||||
|
|
@ -10,19 +13,21 @@ from UM.View.RenderBatch import RenderBatch
|
||||||
|
|
||||||
from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
|
from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from UM.View.GL.ShaderProgram import ShaderProgram
|
||||||
|
|
||||||
## A RenderPass subclass that renders a the distance of selectable objects from the active camera to a texture.
|
## A RenderPass subclass that renders a the distance of selectable objects from the active camera to a texture.
|
||||||
# The texture is used to map a 2d location (eg the mouse location) to a world space position
|
# The texture is used to map a 2d location (eg the mouse location) to a world space position
|
||||||
#
|
#
|
||||||
# Note that in order to increase precision, the 24 bit depth value is encoded into all three of the R,G & B channels
|
# Note that in order to increase precision, the 24 bit depth value is encoded into all three of the R,G & B channels
|
||||||
class PickingPass(RenderPass):
|
class PickingPass(RenderPass):
|
||||||
def __init__(self, width: int, height: int):
|
def __init__(self, width: int, height: int) -> None:
|
||||||
super().__init__("picking", width, height)
|
super().__init__("picking", width, height)
|
||||||
|
|
||||||
self._renderer = Application.getInstance().getRenderer()
|
self._renderer = QtApplication.getInstance().getRenderer()
|
||||||
|
|
||||||
self._shader = None
|
self._shader = None #type: Optional[ShaderProgram]
|
||||||
self._scene = Application.getInstance().getController().getScene()
|
self._scene = QtApplication.getInstance().getController().getScene()
|
||||||
|
|
||||||
def render(self) -> None:
|
def render(self) -> None:
|
||||||
if not self._shader:
|
if not self._shader:
|
||||||
|
|
@ -37,7 +42,7 @@ class PickingPass(RenderPass):
|
||||||
batch = RenderBatch(self._shader)
|
batch = RenderBatch(self._shader)
|
||||||
|
|
||||||
# Fill up the batch with objects that can be sliced. `
|
# Fill up the batch with objects that can be sliced. `
|
||||||
for node in DepthFirstIterator(self._scene.getRoot()):
|
for node in DepthFirstIterator(self._scene.getRoot()): #type: ignore #Ignore type error because iter() should get called automatically by Python syntax.
|
||||||
if node.callDecoration("isSliceable") and node.getMeshData() and node.isVisible():
|
if node.callDecoration("isSliceable") and node.getMeshData() and node.isVisible():
|
||||||
batch.addItem(node.getWorldTransformation(), node.getMeshData())
|
batch.addItem(node.getWorldTransformation(), node.getMeshData())
|
||||||
|
|
||||||
|
|
@ -64,6 +69,7 @@ class PickingPass(RenderPass):
|
||||||
## Get the world coordinates of a picked point
|
## Get the world coordinates of a picked point
|
||||||
def getPickedPosition(self, x: int, y: int) -> Vector:
|
def getPickedPosition(self, x: int, y: int) -> Vector:
|
||||||
distance = self.getPickedDepth(x, y)
|
distance = self.getPickedDepth(x, y)
|
||||||
ray = self._scene.getActiveCamera().getRay(x, y)
|
camera = self._scene.getActiveCamera()
|
||||||
|
if camera:
|
||||||
return ray.getPointAlongRay(distance)
|
return camera.getRay(x, y).getPointAlongRay(distance)
|
||||||
|
return Vector()
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ from UM.Scene.SceneNode import SceneNode
|
||||||
from UM.Scene.Iterator.BreadthFirstIterator import BreadthFirstIterator
|
from UM.Scene.Iterator.BreadthFirstIterator import BreadthFirstIterator
|
||||||
from UM.Math.Vector import Vector
|
from UM.Math.Vector import Vector
|
||||||
from UM.Scene.Selection import Selection
|
from UM.Scene.Selection import Selection
|
||||||
from UM.Preferences import Preferences
|
from UM.Scene.SceneNodeSettings import SceneNodeSettings
|
||||||
|
|
||||||
from cura.Scene.ConvexHullDecorator import ConvexHullDecorator
|
from cura.Scene.ConvexHullDecorator import ConvexHullDecorator
|
||||||
|
|
||||||
|
|
@ -36,8 +36,8 @@ class PlatformPhysics:
|
||||||
self._max_overlap_checks = 10 # How many times should we try to find a new spot per tick?
|
self._max_overlap_checks = 10 # How many times should we try to find a new spot per tick?
|
||||||
self._minimum_gap = 2 # It is a minimum distance (in mm) between two models, applicable for small models
|
self._minimum_gap = 2 # It is a minimum distance (in mm) between two models, applicable for small models
|
||||||
|
|
||||||
Preferences.getInstance().addPreference("physics/automatic_push_free", True)
|
Application.getInstance().getPreferences().addPreference("physics/automatic_push_free", False)
|
||||||
Preferences.getInstance().addPreference("physics/automatic_drop_down", True)
|
Application.getInstance().getPreferences().addPreference("physics/automatic_drop_down", True)
|
||||||
|
|
||||||
def _onSceneChanged(self, source):
|
def _onSceneChanged(self, source):
|
||||||
if not source.getMeshData():
|
if not source.getMeshData():
|
||||||
|
|
@ -71,7 +71,7 @@ class PlatformPhysics:
|
||||||
# Move it downwards if bottom is above platform
|
# Move it downwards if bottom is above platform
|
||||||
move_vector = Vector()
|
move_vector = Vector()
|
||||||
|
|
||||||
if Preferences.getInstance().getValue("physics/automatic_drop_down") and not (node.getParent() and node.getParent().callDecoration("isGroup") or node.getParent() != root) and node.isEnabled(): #If an object is grouped, don't move it down
|
if Application.getInstance().getPreferences().getValue("physics/automatic_drop_down") and not (node.getParent() and node.getParent().callDecoration("isGroup") or node.getParent() != root) and node.isEnabled(): #If an object is grouped, don't move it down
|
||||||
z_offset = node.callDecoration("getZOffset") if node.getDecorator(ZOffsetDecorator.ZOffsetDecorator) else 0
|
z_offset = node.callDecoration("getZOffset") if node.getDecorator(ZOffsetDecorator.ZOffsetDecorator) else 0
|
||||||
move_vector = move_vector.set(y = -bbox.bottom + z_offset)
|
move_vector = move_vector.set(y = -bbox.bottom + z_offset)
|
||||||
|
|
||||||
|
|
@ -80,7 +80,11 @@ class PlatformPhysics:
|
||||||
node.addDecorator(ConvexHullDecorator())
|
node.addDecorator(ConvexHullDecorator())
|
||||||
|
|
||||||
# only push away objects if this node is a printing mesh
|
# only push away objects if this node is a printing mesh
|
||||||
if not node.callDecoration("isNonPrintingMesh") and Preferences.getInstance().getValue("physics/automatic_push_free"):
|
if not node.callDecoration("isNonPrintingMesh") and Application.getInstance().getPreferences().getValue("physics/automatic_push_free"):
|
||||||
|
# Do not move locked nodes
|
||||||
|
if node.getSetting(SceneNodeSettings.LockPosition):
|
||||||
|
continue
|
||||||
|
|
||||||
# Check for collisions between convex hulls
|
# Check for collisions between convex hulls
|
||||||
for other_node in BreadthFirstIterator(root):
|
for other_node in BreadthFirstIterator(root):
|
||||||
# Ignore root, ourselves and anything that is not a normal SceneNode.
|
# Ignore root, ourselves and anything that is not a normal SceneNode.
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,9 @@
|
||||||
# Copyright (c) 2018 Ultimaker B.V.
|
# Copyright (c) 2018 Ultimaker B.V.
|
||||||
# Cura is released under the terms of the LGPLv3 or higher.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
|
from typing import Optional, TYPE_CHECKING
|
||||||
|
|
||||||
from UM.Application import Application
|
from UM.Application import Application
|
||||||
from UM.Math.Color import Color
|
|
||||||
from UM.Resources import Resources
|
from UM.Resources import Resources
|
||||||
|
|
||||||
from UM.View.RenderPass import RenderPass
|
from UM.View.RenderPass import RenderPass
|
||||||
|
|
@ -11,7 +13,8 @@ from UM.View.RenderBatch import RenderBatch
|
||||||
|
|
||||||
from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
|
from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
|
||||||
|
|
||||||
from typing import Optional
|
if TYPE_CHECKING:
|
||||||
|
from UM.View.GL.ShaderProgram import ShaderProgram
|
||||||
|
|
||||||
MYPY = False
|
MYPY = False
|
||||||
if MYPY:
|
if MYPY:
|
||||||
|
|
@ -34,16 +37,16 @@ def prettier_color(color_list):
|
||||||
#
|
#
|
||||||
# This is useful to get a preview image of a scene taken from a different location as the active camera.
|
# This is useful to get a preview image of a scene taken from a different location as the active camera.
|
||||||
class PreviewPass(RenderPass):
|
class PreviewPass(RenderPass):
|
||||||
def __init__(self, width: int, height: int):
|
def __init__(self, width: int, height: int) -> None:
|
||||||
super().__init__("preview", width, height, 0)
|
super().__init__("preview", width, height, 0)
|
||||||
|
|
||||||
self._camera = None # type: Optional[Camera]
|
self._camera = None # type: Optional[Camera]
|
||||||
|
|
||||||
self._renderer = Application.getInstance().getRenderer()
|
self._renderer = Application.getInstance().getRenderer()
|
||||||
|
|
||||||
self._shader = None
|
self._shader = None #type: Optional[ShaderProgram]
|
||||||
self._non_printing_shader = None
|
self._non_printing_shader = None #type: Optional[ShaderProgram]
|
||||||
self._support_mesh_shader = None
|
self._support_mesh_shader = None #type: Optional[ShaderProgram]
|
||||||
self._scene = Application.getInstance().getController().getScene()
|
self._scene = Application.getInstance().getController().getScene()
|
||||||
|
|
||||||
# Set the camera to be used by this render pass
|
# Set the camera to be used by this render pass
|
||||||
|
|
@ -54,37 +57,39 @@ class PreviewPass(RenderPass):
|
||||||
def render(self) -> None:
|
def render(self) -> None:
|
||||||
if not self._shader:
|
if not self._shader:
|
||||||
self._shader = OpenGL.getInstance().createShaderProgram(Resources.getPath(Resources.Shaders, "overhang.shader"))
|
self._shader = OpenGL.getInstance().createShaderProgram(Resources.getPath(Resources.Shaders, "overhang.shader"))
|
||||||
self._shader.setUniformValue("u_overhangAngle", 1.0)
|
if self._shader:
|
||||||
self._shader.setUniformValue("u_ambientColor", [0.1, 0.1, 0.1, 1.0])
|
self._shader.setUniformValue("u_overhangAngle", 1.0)
|
||||||
self._shader.setUniformValue("u_specularColor", [0.6, 0.6, 0.6, 1.0])
|
self._shader.setUniformValue("u_ambientColor", [0.1, 0.1, 0.1, 1.0])
|
||||||
self._shader.setUniformValue("u_shininess", 20.0)
|
self._shader.setUniformValue("u_specularColor", [0.6, 0.6, 0.6, 1.0])
|
||||||
|
self._shader.setUniformValue("u_shininess", 20.0)
|
||||||
|
|
||||||
if not self._non_printing_shader:
|
if not self._non_printing_shader:
|
||||||
self._non_printing_shader = OpenGL.getInstance().createShaderProgram(Resources.getPath(Resources.Shaders, "transparent_object.shader"))
|
if self._non_printing_shader:
|
||||||
self._non_printing_shader.setUniformValue("u_diffuseColor", [0.5, 0.5, 0.5, 0.5])
|
self._non_printing_shader = OpenGL.getInstance().createShaderProgram(Resources.getPath(Resources.Shaders, "transparent_object.shader"))
|
||||||
self._non_printing_shader.setUniformValue("u_opacity", 0.6)
|
self._non_printing_shader.setUniformValue("u_diffuseColor", [0.5, 0.5, 0.5, 0.5])
|
||||||
|
self._non_printing_shader.setUniformValue("u_opacity", 0.6)
|
||||||
|
|
||||||
if not self._support_mesh_shader:
|
if not self._support_mesh_shader:
|
||||||
self._support_mesh_shader = OpenGL.getInstance().createShaderProgram(Resources.getPath(Resources.Shaders, "striped.shader"))
|
self._support_mesh_shader = OpenGL.getInstance().createShaderProgram(Resources.getPath(Resources.Shaders, "striped.shader"))
|
||||||
self._support_mesh_shader.setUniformValue("u_vertical_stripes", True)
|
if self._support_mesh_shader:
|
||||||
self._support_mesh_shader.setUniformValue("u_width", 5.0)
|
self._support_mesh_shader.setUniformValue("u_vertical_stripes", True)
|
||||||
|
self._support_mesh_shader.setUniformValue("u_width", 5.0)
|
||||||
|
|
||||||
self._gl.glClearColor(0.0, 0.0, 0.0, 0.0)
|
self._gl.glClearColor(0.0, 0.0, 0.0, 0.0)
|
||||||
self._gl.glClear(self._gl.GL_COLOR_BUFFER_BIT | self._gl.GL_DEPTH_BUFFER_BIT)
|
self._gl.glClear(self._gl.GL_COLOR_BUFFER_BIT | self._gl.GL_DEPTH_BUFFER_BIT)
|
||||||
|
|
||||||
# Create batches to be rendered
|
# Create batches to be rendered
|
||||||
batch = RenderBatch(self._shader)
|
batch = RenderBatch(self._shader)
|
||||||
batch_non_printing = RenderBatch(self._non_printing_shader, type = RenderBatch.RenderType.Transparent)
|
|
||||||
batch_support_mesh = RenderBatch(self._support_mesh_shader)
|
batch_support_mesh = RenderBatch(self._support_mesh_shader)
|
||||||
|
|
||||||
# Fill up the batch with objects that can be sliced. `
|
# Fill up the batch with objects that can be sliced.
|
||||||
for node in DepthFirstIterator(self._scene.getRoot()):
|
for node in DepthFirstIterator(self._scene.getRoot()): #type: ignore #Ignore type error because iter() should get called automatically by Python syntax.
|
||||||
if node.callDecoration("isSliceable") and node.getMeshData() and node.isVisible():
|
if node.callDecoration("isSliceable") and node.getMeshData() and node.isVisible():
|
||||||
per_mesh_stack = node.callDecoration("getStack")
|
per_mesh_stack = node.callDecoration("getStack")
|
||||||
if node.callDecoration("isNonPrintingMesh"):
|
if node.callDecoration("isNonThumbnailVisibleMesh"):
|
||||||
# Non printing mesh
|
# Non printing mesh
|
||||||
batch_non_printing.addItem(node.getWorldTransformation(), node.getMeshData(), uniforms = {})
|
continue
|
||||||
elif per_mesh_stack is not None and per_mesh_stack.getProperty("support_mesh", "value") == True:
|
elif per_mesh_stack is not None and per_mesh_stack.getProperty("support_mesh", "value"):
|
||||||
# Support mesh
|
# Support mesh
|
||||||
uniforms = {}
|
uniforms = {}
|
||||||
shade_factor = 0.6
|
shade_factor = 0.6
|
||||||
|
|
@ -112,7 +117,5 @@ class PreviewPass(RenderPass):
|
||||||
|
|
||||||
batch.render(render_camera)
|
batch.render(render_camera)
|
||||||
batch_support_mesh.render(render_camera)
|
batch_support_mesh.render(render_camera)
|
||||||
batch_non_printing.render(render_camera)
|
|
||||||
|
|
||||||
self.release()
|
self.release()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,24 +1,25 @@
|
||||||
# Copyright (c) 2018 Ultimaker B.V.
|
# Copyright (c) 2018 Ultimaker B.V.
|
||||||
# Cura is released under the terms of the LGPLv3 or higher.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
from typing import Dict
|
|
||||||
import math
|
|
||||||
import os.path
|
|
||||||
import unicodedata
|
|
||||||
import json
|
import json
|
||||||
|
import math
|
||||||
|
import os
|
||||||
|
import unicodedata
|
||||||
import re # To create abbreviations for printer names.
|
import re # To create abbreviations for printer names.
|
||||||
|
from typing import Dict
|
||||||
|
|
||||||
from PyQt5.QtCore import QObject, pyqtSignal, pyqtProperty, pyqtSlot
|
from PyQt5.QtCore import QObject, pyqtSignal, pyqtProperty, pyqtSlot
|
||||||
|
|
||||||
from UM.Application import Application
|
from UM.i18n import i18nCatalog
|
||||||
from UM.Logger import Logger
|
from UM.Logger import Logger
|
||||||
from UM.Qt.Duration import Duration
|
from UM.Qt.Duration import Duration
|
||||||
from UM.Preferences import Preferences
|
|
||||||
from UM.Scene.SceneNode import SceneNode
|
from UM.Scene.SceneNode import SceneNode
|
||||||
from UM.i18n import i18nCatalog
|
from UM.i18n import i18nCatalog
|
||||||
|
from UM.MimeTypeDatabase import MimeTypeDatabase
|
||||||
|
|
||||||
catalog = i18nCatalog("cura")
|
catalog = i18nCatalog("cura")
|
||||||
|
|
||||||
|
|
||||||
## A class for processing and calculating minimum, current and maximum print time as well as managing the job name
|
## A class for processing and calculating minimum, current and maximum print time as well as managing the job name
|
||||||
#
|
#
|
||||||
# This class contains all the logic relating to calculation and slicing for the
|
# This class contains all the logic relating to calculation and slicing for the
|
||||||
|
|
@ -47,8 +48,9 @@ class PrintInformation(QObject):
|
||||||
ActiveMachineChanged = 3
|
ActiveMachineChanged = 3
|
||||||
Other = 4
|
Other = 4
|
||||||
|
|
||||||
def __init__(self, parent = None):
|
def __init__(self, application, parent = None):
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
|
self._application = application
|
||||||
|
|
||||||
self.initializeCuraMessagePrintTimeProperties()
|
self.initializeCuraMessagePrintTimeProperties()
|
||||||
|
|
||||||
|
|
@ -59,11 +61,12 @@ class PrintInformation(QObject):
|
||||||
|
|
||||||
self._pre_sliced = False
|
self._pre_sliced = False
|
||||||
|
|
||||||
self._backend = Application.getInstance().getBackend()
|
self._backend = self._application.getBackend()
|
||||||
if self._backend:
|
if self._backend:
|
||||||
self._backend.printDurationMessage.connect(self._onPrintDurationMessage)
|
self._backend.printDurationMessage.connect(self._onPrintDurationMessage)
|
||||||
Application.getInstance().getController().getScene().sceneChanged.connect(self._onSceneChanged)
|
self._application.getController().getScene().sceneChanged.connect(self._onSceneChanged)
|
||||||
|
|
||||||
|
self._is_user_specified_job_name = False
|
||||||
self._base_name = ""
|
self._base_name = ""
|
||||||
self._abbr_machine = ""
|
self._abbr_machine = ""
|
||||||
self._job_name = ""
|
self._job_name = ""
|
||||||
|
|
@ -71,7 +74,6 @@ class PrintInformation(QObject):
|
||||||
self._active_build_plate = 0
|
self._active_build_plate = 0
|
||||||
self._initVariablesWithBuildPlate(self._active_build_plate)
|
self._initVariablesWithBuildPlate(self._active_build_plate)
|
||||||
|
|
||||||
self._application = Application.getInstance()
|
|
||||||
self._multi_build_plate_model = self._application.getMultiBuildPlateModel()
|
self._multi_build_plate_model = self._application.getMultiBuildPlateModel()
|
||||||
|
|
||||||
self._application.globalContainerStackChanged.connect(self._updateJobName)
|
self._application.globalContainerStackChanged.connect(self._updateJobName)
|
||||||
|
|
@ -80,7 +82,7 @@ class PrintInformation(QObject):
|
||||||
self._application.workspaceLoaded.connect(self.setProjectName)
|
self._application.workspaceLoaded.connect(self.setProjectName)
|
||||||
self._multi_build_plate_model.activeBuildPlateChanged.connect(self._onActiveBuildPlateChanged)
|
self._multi_build_plate_model.activeBuildPlateChanged.connect(self._onActiveBuildPlateChanged)
|
||||||
|
|
||||||
Preferences.getInstance().preferenceChanged.connect(self._onPreferencesChanged)
|
self._application.getInstance().getPreferences().preferenceChanged.connect(self._onPreferencesChanged)
|
||||||
|
|
||||||
self._application.getMachineManager().rootMaterialChanged.connect(self._onActiveMaterialsChanged)
|
self._application.getMachineManager().rootMaterialChanged.connect(self._onActiveMaterialsChanged)
|
||||||
self._onActiveMaterialsChanged()
|
self._onActiveMaterialsChanged()
|
||||||
|
|
@ -199,7 +201,7 @@ class PrintInformation(QObject):
|
||||||
self._current_print_time[build_plate_number].setDuration(total_estimated_time)
|
self._current_print_time[build_plate_number].setDuration(total_estimated_time)
|
||||||
|
|
||||||
def _calculateInformation(self, build_plate_number):
|
def _calculateInformation(self, build_plate_number):
|
||||||
global_stack = Application.getInstance().getGlobalContainerStack()
|
global_stack = self._application.getGlobalContainerStack()
|
||||||
if global_stack is None:
|
if global_stack is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
@ -208,7 +210,7 @@ class PrintInformation(QObject):
|
||||||
self._material_costs[build_plate_number] = []
|
self._material_costs[build_plate_number] = []
|
||||||
self._material_names[build_plate_number] = []
|
self._material_names[build_plate_number] = []
|
||||||
|
|
||||||
material_preference_values = json.loads(Preferences.getInstance().getValue("cura/material_settings"))
|
material_preference_values = json.loads(self._application.getInstance().getPreferences().getValue("cura/material_settings"))
|
||||||
|
|
||||||
extruder_stacks = global_stack.extruders
|
extruder_stacks = global_stack.extruders
|
||||||
for position, extruder_stack in extruder_stacks.items():
|
for position, extruder_stack in extruder_stacks.items():
|
||||||
|
|
@ -265,6 +267,7 @@ class PrintInformation(QObject):
|
||||||
new_active_build_plate = self._multi_build_plate_model.activeBuildPlate
|
new_active_build_plate = self._multi_build_plate_model.activeBuildPlate
|
||||||
if new_active_build_plate != self._active_build_plate:
|
if new_active_build_plate != self._active_build_plate:
|
||||||
self._active_build_plate = new_active_build_plate
|
self._active_build_plate = new_active_build_plate
|
||||||
|
self._updateJobName()
|
||||||
|
|
||||||
self._initVariablesWithBuildPlate(self._active_build_plate)
|
self._initVariablesWithBuildPlate(self._active_build_plate)
|
||||||
|
|
||||||
|
|
@ -278,9 +281,15 @@ class PrintInformation(QObject):
|
||||||
for build_plate_number in range(self._multi_build_plate_model.maxBuildPlate + 1):
|
for build_plate_number in range(self._multi_build_plate_model.maxBuildPlate + 1):
|
||||||
self._calculateInformation(build_plate_number)
|
self._calculateInformation(build_plate_number)
|
||||||
|
|
||||||
@pyqtSlot(str)
|
# Manual override of job name should also set the base name so that when the printer prefix is updated, it the
|
||||||
def setJobName(self, name):
|
# prefix can be added to the manually added name, not the old base name
|
||||||
|
@pyqtSlot(str, bool)
|
||||||
|
def setJobName(self, name, is_user_specified_job_name = False):
|
||||||
|
self._is_user_specified_job_name = is_user_specified_job_name
|
||||||
self._job_name = name
|
self._job_name = name
|
||||||
|
self._base_name = name.replace(self._abbr_machine + "_", "")
|
||||||
|
if name == "":
|
||||||
|
self._is_user_specified_job_name = False
|
||||||
self.jobNameChanged.emit()
|
self.jobNameChanged.emit()
|
||||||
|
|
||||||
jobNameChanged = pyqtSignal()
|
jobNameChanged = pyqtSignal()
|
||||||
|
|
@ -291,22 +300,35 @@ class PrintInformation(QObject):
|
||||||
|
|
||||||
def _updateJobName(self):
|
def _updateJobName(self):
|
||||||
if self._base_name == "":
|
if self._base_name == "":
|
||||||
self._job_name = ""
|
self._job_name = "unnamed"
|
||||||
|
self._is_user_specified_job_name = False
|
||||||
self.jobNameChanged.emit()
|
self.jobNameChanged.emit()
|
||||||
return
|
return
|
||||||
|
|
||||||
base_name = self._stripAccents(self._base_name)
|
base_name = self._stripAccents(self._base_name)
|
||||||
self._setAbbreviatedMachineName()
|
self._setAbbreviatedMachineName()
|
||||||
if self._pre_sliced:
|
|
||||||
self._job_name = catalog.i18nc("@label", "Pre-sliced file {0}", base_name)
|
# Only update the job name when it's not user-specified.
|
||||||
elif Preferences.getInstance().getValue("cura/jobname_prefix"):
|
if not self._is_user_specified_job_name:
|
||||||
# Don't add abbreviation if it already has the exact same abbreviation.
|
if self._pre_sliced:
|
||||||
if base_name.startswith(self._abbr_machine + "_"):
|
self._job_name = catalog.i18nc("@label", "Pre-sliced file {0}", base_name)
|
||||||
self._job_name = base_name
|
elif self._application.getInstance().getPreferences().getValue("cura/jobname_prefix"):
|
||||||
|
# Don't add abbreviation if it already has the exact same abbreviation.
|
||||||
|
if base_name.startswith(self._abbr_machine + "_"):
|
||||||
|
self._job_name = base_name
|
||||||
|
else:
|
||||||
|
self._job_name = self._abbr_machine + "_" + base_name
|
||||||
else:
|
else:
|
||||||
self._job_name = self._abbr_machine + "_" + base_name
|
self._job_name = base_name
|
||||||
else:
|
|
||||||
self._job_name = base_name
|
# In case there are several buildplates, a suffix is attached
|
||||||
|
if self._multi_build_plate_model.maxBuildPlate > 0:
|
||||||
|
connector = "_#"
|
||||||
|
suffix = connector + str(self._active_build_plate + 1)
|
||||||
|
if connector in self._job_name:
|
||||||
|
self._job_name = self._job_name.split(connector)[0] # get the real name
|
||||||
|
if self._active_build_plate != 0:
|
||||||
|
self._job_name += suffix
|
||||||
|
|
||||||
self.jobNameChanged.emit()
|
self.jobNameChanged.emit()
|
||||||
|
|
||||||
|
|
@ -317,12 +339,14 @@ class PrintInformation(QObject):
|
||||||
baseNameChanged = pyqtSignal()
|
baseNameChanged = pyqtSignal()
|
||||||
|
|
||||||
def setBaseName(self, base_name: str, is_project_file: bool = False):
|
def setBaseName(self, base_name: str, is_project_file: bool = False):
|
||||||
|
self._is_user_specified_job_name = False
|
||||||
|
|
||||||
# Ensure that we don't use entire path but only filename
|
# Ensure that we don't use entire path but only filename
|
||||||
name = os.path.basename(base_name)
|
name = os.path.basename(base_name)
|
||||||
|
|
||||||
# when a file is opened using the terminal; the filename comes from _onFileLoaded and still contains its
|
# when a file is opened using the terminal; the filename comes from _onFileLoaded and still contains its
|
||||||
# extension. This cuts the extension off if necessary.
|
# extension. This cuts the extension off if necessary.
|
||||||
name = os.path.splitext(name)[0]
|
check_name = os.path.splitext(name)[0]
|
||||||
filename_parts = os.path.basename(base_name).split(".")
|
filename_parts = os.path.basename(base_name).split(".")
|
||||||
|
|
||||||
# If it's a gcode, also always update the job name
|
# If it's a gcode, also always update the job name
|
||||||
|
|
@ -333,20 +357,33 @@ class PrintInformation(QObject):
|
||||||
|
|
||||||
# if this is a profile file, always update the job name
|
# if this is a profile file, always update the job name
|
||||||
# name is "" when I first had some meshes and afterwards I deleted them so the naming should start again
|
# name is "" when I first had some meshes and afterwards I deleted them so the naming should start again
|
||||||
is_empty = name == ""
|
is_empty = check_name == ""
|
||||||
if is_gcode or is_project_file or (is_empty or (self._base_name == "" and self._base_name != name)):
|
if is_gcode or is_project_file or (is_empty or (self._base_name == "" and self._base_name != check_name)):
|
||||||
# Only take the file name part
|
# Only take the file name part, Note : file name might have 'dot' in name as well
|
||||||
self._base_name = filename_parts[0]
|
|
||||||
|
data = ""
|
||||||
|
try:
|
||||||
|
mime_type = MimeTypeDatabase.getMimeTypeForFile(name)
|
||||||
|
data = mime_type.stripExtension(name)
|
||||||
|
except:
|
||||||
|
Logger.log("w", "Unsupported Mime Type Database file extension %s", name)
|
||||||
|
|
||||||
|
if data is not None and check_name is not None:
|
||||||
|
self._base_name = data
|
||||||
|
else:
|
||||||
|
self._base_name = ""
|
||||||
|
|
||||||
self._updateJobName()
|
self._updateJobName()
|
||||||
|
|
||||||
@pyqtProperty(str, fset = setBaseName, notify = baseNameChanged)
|
@pyqtProperty(str, fset = setBaseName, notify = baseNameChanged)
|
||||||
def baseName(self):
|
def baseName(self):
|
||||||
return self._base_name
|
return self._base_name
|
||||||
|
|
||||||
## Created an acronymn-like abbreviated machine name from the currently active machine name
|
## Created an acronym-like abbreviated machine name from the currently
|
||||||
# Called each time the global stack is switched
|
# active machine name.
|
||||||
|
# Called each time the global stack is switched.
|
||||||
def _setAbbreviatedMachineName(self):
|
def _setAbbreviatedMachineName(self):
|
||||||
global_container_stack = Application.getInstance().getGlobalContainerStack()
|
global_container_stack = self._application.getGlobalContainerStack()
|
||||||
if not global_container_stack:
|
if not global_container_stack:
|
||||||
self._abbr_machine = ""
|
self._abbr_machine = ""
|
||||||
return
|
return
|
||||||
|
|
|
||||||
|
|
@ -4,10 +4,9 @@
|
||||||
from PyQt5.QtCore import pyqtSignal, pyqtProperty, QObject, pyqtSlot
|
from PyQt5.QtCore import pyqtSignal, pyqtProperty, QObject, pyqtSlot
|
||||||
from cura.PrinterOutput.ExtruderConfigurationModel import ExtruderConfigurationModel
|
from cura.PrinterOutput.ExtruderConfigurationModel import ExtruderConfigurationModel
|
||||||
|
|
||||||
from typing import Optional
|
from typing import Optional, TYPE_CHECKING
|
||||||
|
|
||||||
MYPY = False
|
if TYPE_CHECKING:
|
||||||
if MYPY:
|
|
||||||
from cura.PrinterOutput.PrinterOutputModel import PrinterOutputModel
|
from cura.PrinterOutput.PrinterOutputModel import PrinterOutputModel
|
||||||
from cura.PrinterOutput.MaterialOutputModel import MaterialOutputModel
|
from cura.PrinterOutput.MaterialOutputModel import MaterialOutputModel
|
||||||
|
|
||||||
|
|
@ -20,12 +19,12 @@ class ExtruderOutputModel(QObject):
|
||||||
extruderConfigurationChanged = pyqtSignal()
|
extruderConfigurationChanged = pyqtSignal()
|
||||||
isPreheatingChanged = pyqtSignal()
|
isPreheatingChanged = pyqtSignal()
|
||||||
|
|
||||||
def __init__(self, printer: "PrinterOutputModel", position, parent=None):
|
def __init__(self, printer: "PrinterOutputModel", position, parent=None) -> None:
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
self._printer = printer
|
self._printer = printer
|
||||||
self._position = position
|
self._position = position
|
||||||
self._target_hotend_temperature = 0
|
self._target_hotend_temperature = 0 # type: float
|
||||||
self._hotend_temperature = 0
|
self._hotend_temperature = 0 # type: float
|
||||||
self._hotend_id = ""
|
self._hotend_id = ""
|
||||||
self._active_material = None # type: Optional[MaterialOutputModel]
|
self._active_material = None # type: Optional[MaterialOutputModel]
|
||||||
self._extruder_configuration = ExtruderConfigurationModel()
|
self._extruder_configuration = ExtruderConfigurationModel()
|
||||||
|
|
@ -47,7 +46,7 @@ class ExtruderOutputModel(QObject):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@pyqtProperty(QObject, notify = activeMaterialChanged)
|
@pyqtProperty(QObject, notify = activeMaterialChanged)
|
||||||
def activeMaterial(self) -> "MaterialOutputModel":
|
def activeMaterial(self) -> Optional["MaterialOutputModel"]:
|
||||||
return self._active_material
|
return self._active_material
|
||||||
|
|
||||||
def updateActiveMaterial(self, material: Optional["MaterialOutputModel"]):
|
def updateActiveMaterial(self, material: Optional["MaterialOutputModel"]):
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,15 @@
|
||||||
# Copyright (c) 2018 Ultimaker B.V.
|
# Copyright (c) 2018 Ultimaker B.V.
|
||||||
# Cura is released under the terms of the LGPLv3 or higher.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from cura.PrinterOutput.PrinterOutputController import PrinterOutputController
|
from cura.PrinterOutput.PrinterOutputController import PrinterOutputController
|
||||||
from PyQt5.QtCore import QTimer
|
from PyQt5.QtCore import QTimer
|
||||||
|
|
||||||
MYPY = False
|
if TYPE_CHECKING:
|
||||||
if MYPY:
|
|
||||||
from cura.PrinterOutput.PrintJobOutputModel import PrintJobOutputModel
|
from cura.PrinterOutput.PrintJobOutputModel import PrintJobOutputModel
|
||||||
from cura.PrinterOutput.PrinterOutputModel import PrinterOutputModel
|
from cura.PrinterOutput.PrinterOutputModel import PrinterOutputModel
|
||||||
|
from cura.PrinterOutput.ExtruderOutputModel import ExtruderOutputModel
|
||||||
|
|
||||||
|
|
||||||
class GenericOutputController(PrinterOutputController):
|
class GenericOutputController(PrinterOutputController):
|
||||||
|
|
@ -58,8 +60,7 @@ class GenericOutputController(PrinterOutputController):
|
||||||
self._output_device.sendCommand("G90")
|
self._output_device.sendCommand("G90")
|
||||||
|
|
||||||
def homeHead(self, printer):
|
def homeHead(self, printer):
|
||||||
self._output_device.sendCommand("G28 X")
|
self._output_device.sendCommand("G28 X Y")
|
||||||
self._output_device.sendCommand("G28 Y")
|
|
||||||
|
|
||||||
def homeBed(self, printer):
|
def homeBed(self, printer):
|
||||||
self._output_device.sendCommand("G28 Z")
|
self._output_device.sendCommand("G28 Z")
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,18 @@
|
||||||
# Copyright (c) 2018 Ultimaker B.V.
|
# Copyright (c) 2018 Ultimaker B.V.
|
||||||
# Cura is released under the terms of the LGPLv3 or higher.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
from UM.Application import Application
|
from UM.FileHandler.FileHandler import FileHandler #For typing.
|
||||||
from UM.Logger import Logger
|
from UM.Logger import Logger
|
||||||
|
from UM.Scene.SceneNode import SceneNode #For typing.
|
||||||
from cura.CuraApplication import CuraApplication
|
from cura.CuraApplication import CuraApplication
|
||||||
|
|
||||||
from cura.PrinterOutputDevice import PrinterOutputDevice, ConnectionState
|
from cura.PrinterOutputDevice import PrinterOutputDevice, ConnectionState
|
||||||
|
|
||||||
from PyQt5.QtNetwork import QHttpMultiPart, QHttpPart, QNetworkRequest, QNetworkAccessManager, QNetworkReply
|
from PyQt5.QtNetwork import QHttpMultiPart, QHttpPart, QNetworkRequest, QNetworkAccessManager, QNetworkReply, QAuthenticator
|
||||||
from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, pyqtSignal, QUrl, QCoreApplication
|
from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject, QUrl, QCoreApplication
|
||||||
from time import time
|
from time import time
|
||||||
from typing import Callable, Any, Optional, Dict, Tuple
|
from typing import Any, Callable, Dict, List, Optional
|
||||||
from enum import IntEnum
|
from enum import IntEnum
|
||||||
from typing import List
|
|
||||||
|
|
||||||
import os # To get the username
|
import os # To get the username
|
||||||
import gzip
|
import gzip
|
||||||
|
|
@ -28,20 +28,20 @@ class AuthState(IntEnum):
|
||||||
class NetworkedPrinterOutputDevice(PrinterOutputDevice):
|
class NetworkedPrinterOutputDevice(PrinterOutputDevice):
|
||||||
authenticationStateChanged = pyqtSignal()
|
authenticationStateChanged = pyqtSignal()
|
||||||
|
|
||||||
def __init__(self, device_id, address: str, properties, parent = None) -> None:
|
def __init__(self, device_id, address: str, properties: Dict[bytes, bytes], parent: QObject = None) -> None:
|
||||||
super().__init__(device_id = device_id, parent = parent)
|
super().__init__(device_id = device_id, parent = parent)
|
||||||
self._manager = None # type: QNetworkAccessManager
|
self._manager = None # type: Optional[QNetworkAccessManager]
|
||||||
self._last_manager_create_time = None # type: float
|
self._last_manager_create_time = None # type: Optional[float]
|
||||||
self._recreate_network_manager_time = 30
|
self._recreate_network_manager_time = 30
|
||||||
self._timeout_time = 10 # After how many seconds of no response should a timeout occur?
|
self._timeout_time = 10 # After how many seconds of no response should a timeout occur?
|
||||||
|
|
||||||
self._last_response_time = None # type: float
|
self._last_response_time = None # type: Optional[float]
|
||||||
self._last_request_time = None # type: float
|
self._last_request_time = None # type: Optional[float]
|
||||||
|
|
||||||
self._api_prefix = ""
|
self._api_prefix = ""
|
||||||
self._address = address
|
self._address = address
|
||||||
self._properties = properties
|
self._properties = properties
|
||||||
self._user_agent = "%s/%s " % (Application.getInstance().getApplicationName(), Application.getInstance().getVersion())
|
self._user_agent = "%s/%s " % (CuraApplication.getInstance().getApplicationName(), CuraApplication.getInstance().getVersion())
|
||||||
|
|
||||||
self._onFinishedCallbacks = {} # type: Dict[str, Callable[[QNetworkReply], None]]
|
self._onFinishedCallbacks = {} # type: Dict[str, Callable[[QNetworkReply], None]]
|
||||||
self._authentication_state = AuthState.NotAuthenticated
|
self._authentication_state = AuthState.NotAuthenticated
|
||||||
|
|
@ -68,16 +68,16 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice):
|
||||||
self._printer_type = value
|
self._printer_type = value
|
||||||
break
|
break
|
||||||
|
|
||||||
def requestWrite(self, nodes, file_name=None, filter_by_machine=False, file_handler=None, **kwargs) -> None:
|
def requestWrite(self, nodes: List[SceneNode], file_name: Optional[str] = None, limit_mimetypes: bool = False, file_handler: Optional[FileHandler] = None, **kwargs: str) -> None:
|
||||||
raise NotImplementedError("requestWrite needs to be implemented")
|
raise NotImplementedError("requestWrite needs to be implemented")
|
||||||
|
|
||||||
def setAuthenticationState(self, authentication_state) -> None:
|
def setAuthenticationState(self, authentication_state: AuthState) -> None:
|
||||||
if self._authentication_state != authentication_state:
|
if self._authentication_state != authentication_state:
|
||||||
self._authentication_state = authentication_state
|
self._authentication_state = authentication_state
|
||||||
self.authenticationStateChanged.emit()
|
self.authenticationStateChanged.emit()
|
||||||
|
|
||||||
@pyqtProperty(int, notify=authenticationStateChanged)
|
@pyqtProperty(int, notify = authenticationStateChanged)
|
||||||
def authenticationState(self) -> int:
|
def authenticationState(self) -> AuthState:
|
||||||
return self._authentication_state
|
return self._authentication_state
|
||||||
|
|
||||||
def _compressDataAndNotifyQt(self, data_to_append: str) -> bytes:
|
def _compressDataAndNotifyQt(self, data_to_append: str) -> bytes:
|
||||||
|
|
@ -122,7 +122,7 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice):
|
||||||
self._compressing_gcode = False
|
self._compressing_gcode = False
|
||||||
return b"".join(file_data_bytes_list)
|
return b"".join(file_data_bytes_list)
|
||||||
|
|
||||||
def _update(self) -> bool:
|
def _update(self) -> None:
|
||||||
if self._last_response_time:
|
if self._last_response_time:
|
||||||
time_since_last_response = time() - self._last_response_time
|
time_since_last_response = time() - self._last_response_time
|
||||||
else:
|
else:
|
||||||
|
|
@ -145,16 +145,16 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice):
|
||||||
if time_since_last_response > self._recreate_network_manager_time:
|
if time_since_last_response > self._recreate_network_manager_time:
|
||||||
if self._last_manager_create_time is None:
|
if self._last_manager_create_time is None:
|
||||||
self._createNetworkManager()
|
self._createNetworkManager()
|
||||||
if time() - self._last_manager_create_time > self._recreate_network_manager_time:
|
elif time() - self._last_manager_create_time > self._recreate_network_manager_time:
|
||||||
self._createNetworkManager()
|
self._createNetworkManager()
|
||||||
|
assert(self._manager is not None)
|
||||||
elif self._connection_state == ConnectionState.closed:
|
elif self._connection_state == ConnectionState.closed:
|
||||||
# Go out of timeout.
|
# Go out of timeout.
|
||||||
self.setConnectionState(self._connection_state_before_timeout)
|
if self._connection_state_before_timeout is not None: # sanity check, but it should never be None here
|
||||||
self._connection_state_before_timeout = None
|
self.setConnectionState(self._connection_state_before_timeout)
|
||||||
|
self._connection_state_before_timeout = None
|
||||||
|
|
||||||
return True
|
def _createEmptyRequest(self, target: str, content_type: Optional[str] = "application/json") -> QNetworkRequest:
|
||||||
|
|
||||||
def _createEmptyRequest(self, target, content_type: Optional[str] = "application/json") -> QNetworkRequest:
|
|
||||||
url = QUrl("http://" + self._address + self._api_prefix + target)
|
url = QUrl("http://" + self._address + self._api_prefix + target)
|
||||||
request = QNetworkRequest(url)
|
request = QNetworkRequest(url)
|
||||||
if content_type is not None:
|
if content_type is not None:
|
||||||
|
|
@ -162,7 +162,7 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice):
|
||||||
request.setHeader(QNetworkRequest.UserAgentHeader, self._user_agent)
|
request.setHeader(QNetworkRequest.UserAgentHeader, self._user_agent)
|
||||||
return request
|
return request
|
||||||
|
|
||||||
def _createFormPart(self, content_header, data, content_type = None) -> QHttpPart:
|
def _createFormPart(self, content_header: str, data: bytes, content_type: Optional[str] = None) -> QHttpPart:
|
||||||
part = QHttpPart()
|
part = QHttpPart()
|
||||||
|
|
||||||
if not content_header.startswith("form-data;"):
|
if not content_header.startswith("form-data;"):
|
||||||
|
|
@ -188,35 +188,40 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice):
|
||||||
if reply in self._kept_alive_multiparts:
|
if reply in self._kept_alive_multiparts:
|
||||||
del self._kept_alive_multiparts[reply]
|
del self._kept_alive_multiparts[reply]
|
||||||
|
|
||||||
def put(self, target: str, data: str, onFinished: Optional[Callable[[Any, QNetworkReply], None]]) -> None:
|
def put(self, target: str, data: str, on_finished: Optional[Callable[[QNetworkReply], None]]) -> None:
|
||||||
if self._manager is None:
|
if self._manager is None:
|
||||||
self._createNetworkManager()
|
self._createNetworkManager()
|
||||||
|
assert (self._manager is not None)
|
||||||
request = self._createEmptyRequest(target)
|
request = self._createEmptyRequest(target)
|
||||||
self._last_request_time = time()
|
self._last_request_time = time()
|
||||||
reply = self._manager.put(request, data.encode())
|
reply = self._manager.put(request, data.encode())
|
||||||
self._registerOnFinishedCallback(reply, onFinished)
|
self._registerOnFinishedCallback(reply, on_finished)
|
||||||
|
|
||||||
def get(self, target: str, onFinished: Optional[Callable[[Any, QNetworkReply], None]]) -> None:
|
def get(self, target: str, on_finished: Optional[Callable[[QNetworkReply], None]]) -> None:
|
||||||
if self._manager is None:
|
if self._manager is None:
|
||||||
self._createNetworkManager()
|
self._createNetworkManager()
|
||||||
|
assert (self._manager is not None)
|
||||||
request = self._createEmptyRequest(target)
|
request = self._createEmptyRequest(target)
|
||||||
self._last_request_time = time()
|
self._last_request_time = time()
|
||||||
reply = self._manager.get(request)
|
reply = self._manager.get(request)
|
||||||
self._registerOnFinishedCallback(reply, onFinished)
|
self._registerOnFinishedCallback(reply, on_finished)
|
||||||
|
|
||||||
def post(self, target: str, data: str, onFinished: Optional[Callable[[Any, QNetworkReply], None]], onProgress: Callable = None) -> None:
|
def post(self, target: str, data: str, on_finished: Optional[Callable[[QNetworkReply], None]], on_progress: Callable = None) -> None:
|
||||||
if self._manager is None:
|
if self._manager is None:
|
||||||
self._createNetworkManager()
|
self._createNetworkManager()
|
||||||
|
assert (self._manager is not None)
|
||||||
request = self._createEmptyRequest(target)
|
request = self._createEmptyRequest(target)
|
||||||
self._last_request_time = time()
|
self._last_request_time = time()
|
||||||
reply = self._manager.post(request, data)
|
reply = self._manager.post(request, data)
|
||||||
if onProgress is not None:
|
if on_progress is not None:
|
||||||
reply.uploadProgress.connect(onProgress)
|
reply.uploadProgress.connect(on_progress)
|
||||||
self._registerOnFinishedCallback(reply, onFinished)
|
self._registerOnFinishedCallback(reply, on_finished)
|
||||||
|
|
||||||
|
def postFormWithParts(self, target: str, parts: List[QHttpPart], on_finished: Optional[Callable[[QNetworkReply], None]], on_progress: Callable = None) -> QNetworkReply:
|
||||||
|
|
||||||
def postFormWithParts(self, target:str, parts: List[QHttpPart], onFinished: Optional[Callable[[Any, QNetworkReply], None]], onProgress: Callable = None) -> None:
|
|
||||||
if self._manager is None:
|
if self._manager is None:
|
||||||
self._createNetworkManager()
|
self._createNetworkManager()
|
||||||
|
assert (self._manager is not None)
|
||||||
request = self._createEmptyRequest(target, content_type=None)
|
request = self._createEmptyRequest(target, content_type=None)
|
||||||
multi_post_part = QHttpMultiPart(QHttpMultiPart.FormDataType)
|
multi_post_part = QHttpMultiPart(QHttpMultiPart.FormDataType)
|
||||||
for part in parts:
|
for part in parts:
|
||||||
|
|
@ -228,20 +233,20 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice):
|
||||||
|
|
||||||
self._kept_alive_multiparts[reply] = multi_post_part
|
self._kept_alive_multiparts[reply] = multi_post_part
|
||||||
|
|
||||||
if onProgress is not None:
|
if on_progress is not None:
|
||||||
reply.uploadProgress.connect(onProgress)
|
reply.uploadProgress.connect(on_progress)
|
||||||
self._registerOnFinishedCallback(reply, onFinished)
|
self._registerOnFinishedCallback(reply, on_finished)
|
||||||
|
|
||||||
return reply
|
return reply
|
||||||
|
|
||||||
def postForm(self, target: str, header_data: str, body_data: bytes, onFinished: Optional[Callable[[Any, QNetworkReply], None]], onProgress: Callable = None) -> None:
|
def postForm(self, target: str, header_data: str, body_data: bytes, on_finished: Optional[Callable[[QNetworkReply], None]], on_progress: Callable = None) -> None:
|
||||||
post_part = QHttpPart()
|
post_part = QHttpPart()
|
||||||
post_part.setHeader(QNetworkRequest.ContentDispositionHeader, header_data)
|
post_part.setHeader(QNetworkRequest.ContentDispositionHeader, header_data)
|
||||||
post_part.setBody(body_data)
|
post_part.setBody(body_data)
|
||||||
|
|
||||||
self.postFormWithParts(target, [post_part], onFinished, onProgress)
|
self.postFormWithParts(target, [post_part], on_finished, on_progress)
|
||||||
|
|
||||||
def _onAuthenticationRequired(self, reply, authenticator) -> None:
|
def _onAuthenticationRequired(self, reply: QNetworkReply, authenticator: QAuthenticator) -> None:
|
||||||
Logger.log("w", "Request to {url} required authentication, which was not implemented".format(url = reply.url().toString()))
|
Logger.log("w", "Request to {url} required authentication, which was not implemented".format(url = reply.url().toString()))
|
||||||
|
|
||||||
def _createNetworkManager(self) -> None:
|
def _createNetworkManager(self) -> None:
|
||||||
|
|
@ -255,12 +260,12 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice):
|
||||||
self._last_manager_create_time = time()
|
self._last_manager_create_time = time()
|
||||||
self._manager.authenticationRequired.connect(self._onAuthenticationRequired)
|
self._manager.authenticationRequired.connect(self._onAuthenticationRequired)
|
||||||
|
|
||||||
machine_manager = CuraApplication.getInstance().getMachineManager()
|
if self._properties.get(b"temporary", b"false") != b"true":
|
||||||
machine_manager.checkCorrectGroupName(self.getId(), self.name)
|
CuraApplication.getInstance().getMachineManager().checkCorrectGroupName(self.getId(), self.name)
|
||||||
|
|
||||||
def _registerOnFinishedCallback(self, reply: QNetworkReply, onFinished: Optional[Callable[[Any, QNetworkReply], None]]) -> None:
|
def _registerOnFinishedCallback(self, reply: QNetworkReply, on_finished: Optional[Callable[[QNetworkReply], None]]) -> None:
|
||||||
if onFinished is not None:
|
if on_finished is not None:
|
||||||
self._onFinishedCallbacks[reply.url().toString() + str(reply.operation())] = onFinished
|
self._onFinishedCallbacks[reply.url().toString() + str(reply.operation())] = on_finished
|
||||||
|
|
||||||
def __handleOnFinished(self, reply: QNetworkReply) -> None:
|
def __handleOnFinished(self, reply: QNetworkReply) -> None:
|
||||||
# Due to garbage collection, we need to cache certain bits of post operations.
|
# Due to garbage collection, we need to cache certain bits of post operations.
|
||||||
|
|
@ -297,30 +302,30 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice):
|
||||||
|
|
||||||
## Get the unique key of this machine
|
## Get the unique key of this machine
|
||||||
# \return key String containing the key of the machine.
|
# \return key String containing the key of the machine.
|
||||||
@pyqtProperty(str, constant=True)
|
@pyqtProperty(str, constant = True)
|
||||||
def key(self) -> str:
|
def key(self) -> str:
|
||||||
return self._id
|
return self._id
|
||||||
|
|
||||||
## The IP address of the printer.
|
## The IP address of the printer.
|
||||||
@pyqtProperty(str, constant=True)
|
@pyqtProperty(str, constant = True)
|
||||||
def address(self) -> str:
|
def address(self) -> str:
|
||||||
return self._properties.get(b"address", b"").decode("utf-8")
|
return self._properties.get(b"address", b"").decode("utf-8")
|
||||||
|
|
||||||
## Name of the printer (as returned from the ZeroConf properties)
|
## Name of the printer (as returned from the ZeroConf properties)
|
||||||
@pyqtProperty(str, constant=True)
|
@pyqtProperty(str, constant = True)
|
||||||
def name(self) -> str:
|
def name(self) -> str:
|
||||||
return self._properties.get(b"name", b"").decode("utf-8")
|
return self._properties.get(b"name", b"").decode("utf-8")
|
||||||
|
|
||||||
## Firmware version (as returned from the ZeroConf properties)
|
## Firmware version (as returned from the ZeroConf properties)
|
||||||
@pyqtProperty(str, constant=True)
|
@pyqtProperty(str, constant = True)
|
||||||
def firmwareVersion(self) -> str:
|
def firmwareVersion(self) -> str:
|
||||||
return self._properties.get(b"firmware_version", b"").decode("utf-8")
|
return self._properties.get(b"firmware_version", b"").decode("utf-8")
|
||||||
|
|
||||||
@pyqtProperty(str, constant=True)
|
@pyqtProperty(str, constant = True)
|
||||||
def printerType(self) -> str:
|
def printerType(self) -> str:
|
||||||
return self._printer_type
|
return self._printer_type
|
||||||
|
|
||||||
## IPadress of this printer
|
## IP adress of this printer
|
||||||
@pyqtProperty(str, constant=True)
|
@pyqtProperty(str, constant = True)
|
||||||
def ipAddress(self) -> str:
|
def ipAddress(self) -> str:
|
||||||
return self._address
|
return self._address
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,9 @@
|
||||||
# Cura is released under the terms of the LGPLv3 or higher.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
from PyQt5.QtCore import pyqtSignal, pyqtProperty, QObject, pyqtSlot
|
from PyQt5.QtCore import pyqtSignal, pyqtProperty, QObject, pyqtSlot
|
||||||
from typing import Optional
|
from typing import Optional, TYPE_CHECKING
|
||||||
MYPY = False
|
|
||||||
if MYPY:
|
if TYPE_CHECKING:
|
||||||
from cura.PrinterOutput.PrinterOutputController import PrinterOutputController
|
from cura.PrinterOutput.PrinterOutputController import PrinterOutputController
|
||||||
from cura.PrinterOutput.PrinterOutputModel import PrinterOutputModel
|
from cura.PrinterOutput.PrinterOutputModel import PrinterOutputModel
|
||||||
|
|
||||||
|
|
@ -18,7 +18,7 @@ class PrintJobOutputModel(QObject):
|
||||||
assignedPrinterChanged = pyqtSignal()
|
assignedPrinterChanged = pyqtSignal()
|
||||||
ownerChanged = pyqtSignal()
|
ownerChanged = pyqtSignal()
|
||||||
|
|
||||||
def __init__(self, output_controller: "PrinterOutputController", key: str = "", name: str = "", parent=None):
|
def __init__(self, output_controller: "PrinterOutputController", key: str = "", name: str = "", parent=None) -> None:
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
self._output_controller = output_controller
|
self._output_controller = output_controller
|
||||||
self._state = ""
|
self._state = ""
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ class PrinterOutputModel(QObject):
|
||||||
cameraChanged = pyqtSignal()
|
cameraChanged = pyqtSignal()
|
||||||
configurationChanged = pyqtSignal()
|
configurationChanged = pyqtSignal()
|
||||||
|
|
||||||
def __init__(self, output_controller: "PrinterOutputController", number_of_extruders: int = 1, parent=None, firmware_version = ""):
|
def __init__(self, output_controller: "PrinterOutputController", number_of_extruders: int = 1, parent=None, firmware_version = "") -> None:
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
self._bed_temperature = -1 # Use -1 for no heated bed.
|
self._bed_temperature = -1 # Use -1 for no heated bed.
|
||||||
self._target_bed_temperature = 0
|
self._target_bed_temperature = 0
|
||||||
|
|
@ -120,7 +120,7 @@ class PrinterOutputModel(QObject):
|
||||||
|
|
||||||
@pyqtProperty(QVariant, notify = headPositionChanged)
|
@pyqtProperty(QVariant, notify = headPositionChanged)
|
||||||
def headPosition(self):
|
def headPosition(self):
|
||||||
return {"x": self._head_position.x, "y": self._head_position.y, "z": self.head_position_z}
|
return {"x": self._head_position.x, "y": self._head_position.y, "z": self.head_position.z}
|
||||||
|
|
||||||
def updateHeadPosition(self, x, y, z):
|
def updateHeadPosition(self, x, y, z):
|
||||||
if self._head_position.x != x or self._head_position.y != y or self._head_position.z != z:
|
if self._head_position.x != x or self._head_position.y != y or self._head_position.z != z:
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,20 @@
|
||||||
# Copyright (c) 2018 Ultimaker B.V.
|
# Copyright (c) 2018 Ultimaker B.V.
|
||||||
# Cura is released under the terms of the LGPLv3 or higher.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
|
from UM.Decorators import deprecated
|
||||||
from UM.i18n import i18nCatalog
|
from UM.i18n import i18nCatalog
|
||||||
from UM.OutputDevice.OutputDevice import OutputDevice
|
from UM.OutputDevice.OutputDevice import OutputDevice
|
||||||
from PyQt5.QtCore import pyqtProperty, QObject, QTimer, pyqtSignal, QVariant
|
from PyQt5.QtCore import pyqtProperty, QObject, QTimer, pyqtSignal
|
||||||
from PyQt5.QtWidgets import QMessageBox
|
from PyQt5.QtWidgets import QMessageBox
|
||||||
|
|
||||||
from UM.Logger import Logger
|
from UM.Logger import Logger
|
||||||
|
from UM.FileHandler.FileHandler import FileHandler #For typing.
|
||||||
|
from UM.Scene.SceneNode import SceneNode #For typing.
|
||||||
from UM.Signal import signalemitter
|
from UM.Signal import signalemitter
|
||||||
from UM.Application import Application
|
from UM.Qt.QtApplication import QtApplication
|
||||||
|
|
||||||
from enum import IntEnum # For the connection state tracking.
|
from enum import IntEnum # For the connection state tracking.
|
||||||
from typing import List, Optional
|
from typing import Callable, List, Optional
|
||||||
|
|
||||||
MYPY = False
|
MYPY = False
|
||||||
if MYPY:
|
if MYPY:
|
||||||
|
|
@ -20,6 +23,16 @@ if MYPY:
|
||||||
|
|
||||||
i18n_catalog = i18nCatalog("cura")
|
i18n_catalog = i18nCatalog("cura")
|
||||||
|
|
||||||
|
|
||||||
|
## The current processing state of the backend.
|
||||||
|
class ConnectionState(IntEnum):
|
||||||
|
closed = 0
|
||||||
|
connecting = 1
|
||||||
|
connected = 2
|
||||||
|
busy = 3
|
||||||
|
error = 4
|
||||||
|
|
||||||
|
|
||||||
## Printer output device adds extra interface options on top of output device.
|
## Printer output device adds extra interface options on top of output device.
|
||||||
#
|
#
|
||||||
# The assumption is made the printer is a FDM printer.
|
# The assumption is made the printer is a FDM printer.
|
||||||
|
|
@ -47,38 +60,37 @@ class PrinterOutputDevice(QObject, OutputDevice):
|
||||||
# Signal to indicate that the configuration of one of the printers has changed.
|
# Signal to indicate that the configuration of one of the printers has changed.
|
||||||
uniqueConfigurationsChanged = pyqtSignal()
|
uniqueConfigurationsChanged = pyqtSignal()
|
||||||
|
|
||||||
def __init__(self, device_id, parent = None):
|
def __init__(self, device_id: str, parent: QObject = None) -> None:
|
||||||
super().__init__(device_id = device_id, parent = parent)
|
super().__init__(device_id = device_id, parent = parent) # type: ignore # MyPy complains with the multiple inheritance
|
||||||
|
|
||||||
self._printers = [] # type: List[PrinterOutputModel]
|
self._printers = [] # type: List[PrinterOutputModel]
|
||||||
self._unique_configurations = [] # type: List[ConfigurationModel]
|
self._unique_configurations = [] # type: List[ConfigurationModel]
|
||||||
|
|
||||||
self._monitor_view_qml_path = ""
|
self._monitor_view_qml_path = "" #type: str
|
||||||
self._monitor_component = None
|
self._monitor_component = None #type: Optional[QObject]
|
||||||
self._monitor_item = None
|
self._monitor_item = None #type: Optional[QObject]
|
||||||
|
|
||||||
self._control_view_qml_path = ""
|
self._control_view_qml_path = "" #type: str
|
||||||
self._control_component = None
|
self._control_component = None #type: Optional[QObject]
|
||||||
self._control_item = None
|
self._control_item = None #type: Optional[QObject]
|
||||||
|
|
||||||
self._qml_context = None
|
self._accepts_commands = False #type: bool
|
||||||
self._accepts_commands = False
|
|
||||||
|
|
||||||
self._update_timer = QTimer()
|
self._update_timer = QTimer() #type: QTimer
|
||||||
self._update_timer.setInterval(2000) # TODO; Add preference for update interval
|
self._update_timer.setInterval(2000) # TODO; Add preference for update interval
|
||||||
self._update_timer.setSingleShot(False)
|
self._update_timer.setSingleShot(False)
|
||||||
self._update_timer.timeout.connect(self._update)
|
self._update_timer.timeout.connect(self._update)
|
||||||
|
|
||||||
self._connection_state = ConnectionState.closed
|
self._connection_state = ConnectionState.closed #type: ConnectionState
|
||||||
|
|
||||||
self._firmware_name = None
|
self._firmware_name = None #type: Optional[str]
|
||||||
self._address = ""
|
self._address = "" #type: str
|
||||||
self._connection_text = ""
|
self._connection_text = "" #type: str
|
||||||
self.printersChanged.connect(self._onPrintersChanged)
|
self.printersChanged.connect(self._onPrintersChanged)
|
||||||
Application.getInstance().getOutputDeviceManager().outputDevicesChanged.connect(self._updateUniqueConfigurations)
|
QtApplication.getInstance().getOutputDeviceManager().outputDevicesChanged.connect(self._updateUniqueConfigurations)
|
||||||
|
|
||||||
@pyqtProperty(str, notify = connectionTextChanged)
|
@pyqtProperty(str, notify = connectionTextChanged)
|
||||||
def address(self):
|
def address(self) -> str:
|
||||||
return self._address
|
return self._address
|
||||||
|
|
||||||
def setConnectionText(self, connection_text):
|
def setConnectionText(self, connection_text):
|
||||||
|
|
@ -87,36 +99,36 @@ class PrinterOutputDevice(QObject, OutputDevice):
|
||||||
self.connectionTextChanged.emit()
|
self.connectionTextChanged.emit()
|
||||||
|
|
||||||
@pyqtProperty(str, constant=True)
|
@pyqtProperty(str, constant=True)
|
||||||
def connectionText(self):
|
def connectionText(self) -> str:
|
||||||
return self._connection_text
|
return self._connection_text
|
||||||
|
|
||||||
def materialHotendChangedMessage(self, callback):
|
def materialHotendChangedMessage(self, callback: Callable[[int], None]) -> None:
|
||||||
Logger.log("w", "materialHotendChangedMessage needs to be implemented, returning 'Yes'")
|
Logger.log("w", "materialHotendChangedMessage needs to be implemented, returning 'Yes'")
|
||||||
callback(QMessageBox.Yes)
|
callback(QMessageBox.Yes)
|
||||||
|
|
||||||
def isConnected(self):
|
def isConnected(self) -> bool:
|
||||||
return self._connection_state != ConnectionState.closed and self._connection_state != ConnectionState.error
|
return self._connection_state != ConnectionState.closed and self._connection_state != ConnectionState.error
|
||||||
|
|
||||||
def setConnectionState(self, connection_state):
|
def setConnectionState(self, connection_state: ConnectionState) -> None:
|
||||||
if self._connection_state != connection_state:
|
if self._connection_state != connection_state:
|
||||||
self._connection_state = connection_state
|
self._connection_state = connection_state
|
||||||
self.connectionStateChanged.emit(self._id)
|
self.connectionStateChanged.emit(self._id)
|
||||||
|
|
||||||
@pyqtProperty(str, notify = connectionStateChanged)
|
@pyqtProperty(str, notify = connectionStateChanged)
|
||||||
def connectionState(self):
|
def connectionState(self) -> ConnectionState:
|
||||||
return self._connection_state
|
return self._connection_state
|
||||||
|
|
||||||
def _update(self):
|
def _update(self) -> None:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def _getPrinterByKey(self, key) -> Optional["PrinterOutputModel"]:
|
def _getPrinterByKey(self, key: str) -> Optional["PrinterOutputModel"]:
|
||||||
for printer in self._printers:
|
for printer in self._printers:
|
||||||
if printer.key == key:
|
if printer.key == key:
|
||||||
return printer
|
return printer
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def requestWrite(self, nodes, file_name = None, filter_by_machine = False, file_handler = None, **kwargs):
|
def requestWrite(self, nodes: List[SceneNode], file_name: Optional[str] = None, limit_mimetypes: bool = False, file_handler: Optional[FileHandler] = None, **kwargs: str) -> None:
|
||||||
raise NotImplementedError("requestWrite needs to be implemented")
|
raise NotImplementedError("requestWrite needs to be implemented")
|
||||||
|
|
||||||
@pyqtProperty(QObject, notify = printersChanged)
|
@pyqtProperty(QObject, notify = printersChanged)
|
||||||
|
|
@ -126,11 +138,11 @@ class PrinterOutputDevice(QObject, OutputDevice):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@pyqtProperty("QVariantList", notify = printersChanged)
|
@pyqtProperty("QVariantList", notify = printersChanged)
|
||||||
def printers(self):
|
def printers(self) -> List["PrinterOutputModel"]:
|
||||||
return self._printers
|
return self._printers
|
||||||
|
|
||||||
@pyqtProperty(QObject, constant=True)
|
@pyqtProperty(QObject, constant = True)
|
||||||
def monitorItem(self):
|
def monitorItem(self) -> QObject:
|
||||||
# Note that we specifically only check if the monitor component is created.
|
# Note that we specifically only check if the monitor component is created.
|
||||||
# It could be that it failed to actually create the qml item! If we check if the item was created, it will try to
|
# It could be that it failed to actually create the qml item! If we check if the item was created, it will try to
|
||||||
# create the item (and fail) every time.
|
# create the item (and fail) every time.
|
||||||
|
|
@ -138,45 +150,49 @@ class PrinterOutputDevice(QObject, OutputDevice):
|
||||||
self._createMonitorViewFromQML()
|
self._createMonitorViewFromQML()
|
||||||
return self._monitor_item
|
return self._monitor_item
|
||||||
|
|
||||||
@pyqtProperty(QObject, constant=True)
|
@pyqtProperty(QObject, constant = True)
|
||||||
def controlItem(self):
|
def controlItem(self) -> QObject:
|
||||||
if not self._control_component:
|
if not self._control_component:
|
||||||
self._createControlViewFromQML()
|
self._createControlViewFromQML()
|
||||||
return self._control_item
|
return self._control_item
|
||||||
|
|
||||||
def _createControlViewFromQML(self):
|
def _createControlViewFromQML(self) -> None:
|
||||||
if not self._control_view_qml_path:
|
if not self._control_view_qml_path:
|
||||||
return
|
return
|
||||||
if self._control_item is None:
|
if self._control_item is None:
|
||||||
self._control_item = Application.getInstance().createQmlComponent(self._control_view_qml_path, {"OutputDevice": self})
|
self._control_item = QtApplication.getInstance().createQmlComponent(self._control_view_qml_path, {"OutputDevice": self})
|
||||||
|
|
||||||
def _createMonitorViewFromQML(self):
|
def _createMonitorViewFromQML(self) -> None:
|
||||||
if not self._monitor_view_qml_path:
|
if not self._monitor_view_qml_path:
|
||||||
return
|
return
|
||||||
|
|
||||||
if self._monitor_item is None:
|
if self._monitor_item is None:
|
||||||
self._monitor_item = Application.getInstance().createQmlComponent(self._monitor_view_qml_path, {"OutputDevice": self})
|
self._monitor_item = QtApplication.getInstance().createQmlComponent(self._monitor_view_qml_path, {"OutputDevice": self})
|
||||||
|
|
||||||
## Attempt to establish connection
|
## Attempt to establish connection
|
||||||
def connect(self):
|
def connect(self) -> None:
|
||||||
self.setConnectionState(ConnectionState.connecting)
|
self.setConnectionState(ConnectionState.connecting)
|
||||||
self._update_timer.start()
|
self._update_timer.start()
|
||||||
|
|
||||||
## Attempt to close the connection
|
## Attempt to close the connection
|
||||||
def close(self):
|
def close(self) -> None:
|
||||||
self._update_timer.stop()
|
self._update_timer.stop()
|
||||||
self.setConnectionState(ConnectionState.closed)
|
self.setConnectionState(ConnectionState.closed)
|
||||||
|
|
||||||
## Ensure that close gets called when object is destroyed
|
## Ensure that close gets called when object is destroyed
|
||||||
def __del__(self):
|
def __del__(self) -> None:
|
||||||
self.close()
|
self.close()
|
||||||
|
|
||||||
@pyqtProperty(bool, notify=acceptsCommandsChanged)
|
@pyqtProperty(bool, notify = acceptsCommandsChanged)
|
||||||
def acceptsCommands(self):
|
def acceptsCommands(self) -> bool:
|
||||||
return self._accepts_commands
|
return self._accepts_commands
|
||||||
|
|
||||||
|
@deprecated("Please use the protected function instead", "3.2")
|
||||||
|
def setAcceptsCommands(self, accepts_commands: bool) -> None:
|
||||||
|
self._setAcceptsCommands(accepts_commands)
|
||||||
|
|
||||||
## Set a flag to signal the UI that the printer is not (yet) ready to receive commands
|
## Set a flag to signal the UI that the printer is not (yet) ready to receive commands
|
||||||
def _setAcceptsCommands(self, accepts_commands):
|
def _setAcceptsCommands(self, accepts_commands: bool) -> None:
|
||||||
if self._accepts_commands != accepts_commands:
|
if self._accepts_commands != accepts_commands:
|
||||||
self._accepts_commands = accepts_commands
|
self._accepts_commands = accepts_commands
|
||||||
|
|
||||||
|
|
@ -184,15 +200,15 @@ class PrinterOutputDevice(QObject, OutputDevice):
|
||||||
|
|
||||||
# Returns the unique configurations of the printers within this output device
|
# Returns the unique configurations of the printers within this output device
|
||||||
@pyqtProperty("QVariantList", notify = uniqueConfigurationsChanged)
|
@pyqtProperty("QVariantList", notify = uniqueConfigurationsChanged)
|
||||||
def uniqueConfigurations(self):
|
def uniqueConfigurations(self) -> List["ConfigurationModel"]:
|
||||||
return self._unique_configurations
|
return self._unique_configurations
|
||||||
|
|
||||||
def _updateUniqueConfigurations(self):
|
def _updateUniqueConfigurations(self) -> None:
|
||||||
self._unique_configurations = list(set([printer.printerConfiguration for printer in self._printers if printer.printerConfiguration is not None]))
|
self._unique_configurations = list(set([printer.printerConfiguration for printer in self._printers if printer.printerConfiguration is not None]))
|
||||||
self._unique_configurations.sort(key = lambda k: k.printerType)
|
self._unique_configurations.sort(key = lambda k: k.printerType)
|
||||||
self.uniqueConfigurationsChanged.emit()
|
self.uniqueConfigurationsChanged.emit()
|
||||||
|
|
||||||
def _onPrintersChanged(self):
|
def _onPrintersChanged(self) -> None:
|
||||||
for printer in self._printers:
|
for printer in self._printers:
|
||||||
printer.configurationChanged.connect(self._updateUniqueConfigurations)
|
printer.configurationChanged.connect(self._updateUniqueConfigurations)
|
||||||
|
|
||||||
|
|
@ -201,21 +217,12 @@ class PrinterOutputDevice(QObject, OutputDevice):
|
||||||
|
|
||||||
## Set the device firmware name
|
## Set the device firmware name
|
||||||
#
|
#
|
||||||
# \param name \type{str} The name of the firmware.
|
# \param name The name of the firmware.
|
||||||
def _setFirmwareName(self, name):
|
def _setFirmwareName(self, name: str) -> None:
|
||||||
self._firmware_name = name
|
self._firmware_name = name
|
||||||
|
|
||||||
## Get the name of device firmware
|
## Get the name of device firmware
|
||||||
#
|
#
|
||||||
# This name can be used to define device type
|
# This name can be used to define device type
|
||||||
def getFirmwareName(self):
|
def getFirmwareName(self) -> Optional[str]:
|
||||||
return self._firmware_name
|
return self._firmware_name
|
||||||
|
|
||||||
|
|
||||||
## The current processing state of the backend.
|
|
||||||
class ConnectionState(IntEnum):
|
|
||||||
closed = 0
|
|
||||||
connecting = 1
|
|
||||||
connected = 2
|
|
||||||
busy = 3
|
|
||||||
error = 4
|
|
||||||
0
cura/ReaderWriters/__init__.py
Normal file
0
cura/ReaderWriters/__init__.py
Normal file
|
|
@ -229,7 +229,7 @@ class ConvexHullDecorator(SceneNodeDecorator):
|
||||||
return offset_hull
|
return offset_hull
|
||||||
|
|
||||||
def _getHeadAndFans(self):
|
def _getHeadAndFans(self):
|
||||||
return Polygon(numpy.array(self._global_stack.getProperty("machine_head_with_fans_polygon", "value"), numpy.float32))
|
return Polygon(numpy.array(self._global_stack.getHeadAndFansCoordinates(), numpy.float32))
|
||||||
|
|
||||||
def _compute2DConvexHeadFull(self):
|
def _compute2DConvexHeadFull(self):
|
||||||
return self._compute2DConvexHull().getMinkowskiHull(self._getHeadAndFans())
|
return self._compute2DConvexHull().getMinkowskiHull(self._getHeadAndFans())
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ class ConvexHullNode(SceneNode):
|
||||||
self._original_parent = parent
|
self._original_parent = parent
|
||||||
|
|
||||||
# Color of the drawn convex hull
|
# Color of the drawn convex hull
|
||||||
if Application.getInstance().hasGui():
|
if not Application.getInstance().getIsHeadLess():
|
||||||
self._color = Color(*Application.getInstance().getTheme().getColor("convex_hull").getRgb())
|
self._color = Color(*Application.getInstance().getTheme().getColor("convex_hull").getRgb())
|
||||||
else:
|
else:
|
||||||
self._color = Color(0, 0, 0)
|
self._color = Color(0, 0, 0)
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ from UM.Signal import Signal
|
||||||
class CuraSceneController(QObject):
|
class CuraSceneController(QObject):
|
||||||
activeBuildPlateChanged = Signal()
|
activeBuildPlateChanged = Signal()
|
||||||
|
|
||||||
def __init__(self, objects_model: ObjectsModel, multi_build_plate_model: MultiBuildPlateModel):
|
def __init__(self, objects_model: ObjectsModel, multi_build_plate_model: MultiBuildPlateModel) -> None:
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
|
||||||
self._objects_model = objects_model
|
self._objects_model = objects_model
|
||||||
|
|
|
||||||
|
|
@ -1,40 +1,47 @@
|
||||||
# Copyright (c) 2018 Ultimaker B.V.
|
# Copyright (c) 2018 Ultimaker B.V.
|
||||||
# Cura is released under the terms of the LGPLv3 or higher.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
from typing import List
|
from typing import cast, Dict, List, Optional
|
||||||
|
|
||||||
from UM.Application import Application
|
from UM.Application import Application
|
||||||
from UM.Math.AxisAlignedBox import AxisAlignedBox
|
from UM.Math.AxisAlignedBox import AxisAlignedBox
|
||||||
|
from UM.Math.Polygon import Polygon #For typing.
|
||||||
from UM.Scene.SceneNode import SceneNode
|
from UM.Scene.SceneNode import SceneNode
|
||||||
|
from UM.Scene.SceneNodeDecorator import SceneNodeDecorator #To cast the deepcopy of every decorator back to SceneNodeDecorator.
|
||||||
|
|
||||||
from cura.Settings.SettingOverrideDecorator import SettingOverrideDecorator
|
import cura.CuraApplication #To get the build plate.
|
||||||
|
from cura.Settings.ExtruderStack import ExtruderStack #For typing.
|
||||||
|
from cura.Settings.SettingOverrideDecorator import SettingOverrideDecorator #For per-object settings.
|
||||||
|
|
||||||
## Scene nodes that are models are only seen when selecting the corresponding build plate
|
## Scene nodes that are models are only seen when selecting the corresponding build plate
|
||||||
# Note that many other nodes can just be UM SceneNode objects.
|
# Note that many other nodes can just be UM SceneNode objects.
|
||||||
class CuraSceneNode(SceneNode):
|
class CuraSceneNode(SceneNode):
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, parent: Optional["SceneNode"] = None, visible: bool = True, name: str = "", no_setting_override: bool = False) -> None:
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(parent = parent, visible = visible, name = name)
|
||||||
if "no_setting_override" not in kwargs:
|
if not no_setting_override:
|
||||||
self.addDecorator(SettingOverrideDecorator()) # now we always have a getActiveExtruderPosition, unless explicitly disabled
|
self.addDecorator(SettingOverrideDecorator()) # now we always have a getActiveExtruderPosition, unless explicitly disabled
|
||||||
self._outside_buildarea = False
|
self._outside_buildarea = False
|
||||||
|
|
||||||
def setOutsideBuildArea(self, new_value):
|
def setOutsideBuildArea(self, new_value: bool) -> None:
|
||||||
self._outside_buildarea = new_value
|
self._outside_buildarea = new_value
|
||||||
|
|
||||||
def isOutsideBuildArea(self):
|
def isOutsideBuildArea(self) -> bool:
|
||||||
return self._outside_buildarea or self.callDecoration("getBuildPlateNumber") < 0
|
return self._outside_buildarea or self.callDecoration("getBuildPlateNumber") < 0
|
||||||
|
|
||||||
def isVisible(self):
|
def isVisible(self) -> bool:
|
||||||
return super().isVisible() and self.callDecoration("getBuildPlateNumber") == Application.getInstance().getMultiBuildPlateModel().activeBuildPlate
|
return super().isVisible() and self.callDecoration("getBuildPlateNumber") == cura.CuraApplication.CuraApplication.getInstance().getMultiBuildPlateModel().activeBuildPlate
|
||||||
|
|
||||||
def isSelectable(self) -> bool:
|
def isSelectable(self) -> bool:
|
||||||
return super().isSelectable() and self.callDecoration("getBuildPlateNumber") == Application.getInstance().getMultiBuildPlateModel().activeBuildPlate
|
return super().isSelectable() and self.callDecoration("getBuildPlateNumber") == cura.CuraApplication.CuraApplication.getInstance().getMultiBuildPlateModel().activeBuildPlate
|
||||||
|
|
||||||
## Get the extruder used to print this node. If there is no active node, then the extruder in position zero is returned
|
## Get the extruder used to print this node. If there is no active node, then the extruder in position zero is returned
|
||||||
# TODO The best way to do it is by adding the setActiveExtruder decorator to every node when is loaded
|
# TODO The best way to do it is by adding the setActiveExtruder decorator to every node when is loaded
|
||||||
def getPrintingExtruder(self):
|
def getPrintingExtruder(self) -> Optional[ExtruderStack]:
|
||||||
global_container_stack = Application.getInstance().getGlobalContainerStack()
|
global_container_stack = Application.getInstance().getGlobalContainerStack()
|
||||||
|
if global_container_stack is None:
|
||||||
|
return None
|
||||||
|
|
||||||
per_mesh_stack = self.callDecoration("getStack")
|
per_mesh_stack = self.callDecoration("getStack")
|
||||||
extruders = list(global_container_stack.extruders.values())
|
extruders = list(global_container_stack.extruders.values())
|
||||||
|
|
||||||
|
|
@ -79,17 +86,17 @@ class CuraSceneNode(SceneNode):
|
||||||
]
|
]
|
||||||
|
|
||||||
## Return if the provided bbox collides with the bbox of this scene node
|
## Return if the provided bbox collides with the bbox of this scene node
|
||||||
def collidesWithBbox(self, check_bbox):
|
def collidesWithBbox(self, check_bbox: AxisAlignedBox) -> bool:
|
||||||
bbox = self.getBoundingBox()
|
bbox = self.getBoundingBox()
|
||||||
|
if bbox is not None:
|
||||||
# Mark the node as outside the build volume if the bounding box test fails.
|
# Mark the node as outside the build volume if the bounding box test fails.
|
||||||
if check_bbox.intersectsBox(bbox) != AxisAlignedBox.IntersectionResult.FullIntersection:
|
if check_bbox.intersectsBox(bbox) != AxisAlignedBox.IntersectionResult.FullIntersection:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
## Return if any area collides with the convex hull of this scene node
|
## Return if any area collides with the convex hull of this scene node
|
||||||
def collidesWithArea(self, areas):
|
def collidesWithArea(self, areas: List[Polygon]) -> bool:
|
||||||
convex_hull = self.callDecoration("getConvexHull")
|
convex_hull = self.callDecoration("getConvexHull")
|
||||||
if convex_hull:
|
if convex_hull:
|
||||||
if not convex_hull.isValid():
|
if not convex_hull.isValid():
|
||||||
|
|
@ -104,8 +111,7 @@ class CuraSceneNode(SceneNode):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
## Override of SceneNode._calculateAABB to exclude non-printing-meshes from bounding box
|
## Override of SceneNode._calculateAABB to exclude non-printing-meshes from bounding box
|
||||||
def _calculateAABB(self):
|
def _calculateAABB(self) -> None:
|
||||||
aabb = None
|
|
||||||
if self._mesh_data:
|
if self._mesh_data:
|
||||||
aabb = self._mesh_data.getExtents(self.getWorldTransformation())
|
aabb = self._mesh_data.getExtents(self.getWorldTransformation())
|
||||||
else: # If there is no mesh_data, use a boundingbox that encompasses the local (0,0,0)
|
else: # If there is no mesh_data, use a boundingbox that encompasses the local (0,0,0)
|
||||||
|
|
@ -123,18 +129,18 @@ class CuraSceneNode(SceneNode):
|
||||||
self._aabb = aabb
|
self._aabb = aabb
|
||||||
|
|
||||||
## Taken from SceneNode, but replaced SceneNode with CuraSceneNode
|
## Taken from SceneNode, but replaced SceneNode with CuraSceneNode
|
||||||
def __deepcopy__(self, memo):
|
def __deepcopy__(self, memo: Dict[int, object]) -> "CuraSceneNode":
|
||||||
copy = CuraSceneNode(no_setting_override = True) # Setting override will be added later
|
copy = CuraSceneNode(no_setting_override = True) # Setting override will be added later
|
||||||
copy.setTransformation(self.getLocalTransformation())
|
copy.setTransformation(self.getLocalTransformation())
|
||||||
copy.setMeshData(self._mesh_data)
|
copy.setMeshData(self._mesh_data)
|
||||||
copy.setVisible(deepcopy(self._visible, memo))
|
copy.setVisible(cast(bool, deepcopy(self._visible, memo)))
|
||||||
copy._selectable = deepcopy(self._selectable, memo)
|
copy._selectable = cast(bool, deepcopy(self._selectable, memo))
|
||||||
copy._name = deepcopy(self._name, memo)
|
copy._name = cast(str, deepcopy(self._name, memo))
|
||||||
for decorator in self._decorators:
|
for decorator in self._decorators:
|
||||||
copy.addDecorator(deepcopy(decorator, memo))
|
copy.addDecorator(cast(SceneNodeDecorator, deepcopy(decorator, memo)))
|
||||||
|
|
||||||
for child in self._children:
|
for child in self._children:
|
||||||
copy.addChild(deepcopy(child, memo))
|
copy.addChild(cast(SceneNode, deepcopy(child, memo)))
|
||||||
self.calculateBoundingBoxMesh()
|
self.calculateBoundingBoxMesh()
|
||||||
return copy
|
return copy
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,32 +1,26 @@
|
||||||
# Copyright (c) 2018 Ultimaker B.V.
|
# Copyright (c) 2018 Ultimaker B.V.
|
||||||
# Cura is released under the terms of the LGPLv3 or higher.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
import os.path
|
import os
|
||||||
import urllib.parse
|
import urllib.parse
|
||||||
import uuid
|
import uuid
|
||||||
from typing import Dict, Union
|
from typing import Any
|
||||||
|
from typing import Dict, Union, Optional
|
||||||
|
|
||||||
from PyQt5.QtCore import QObject, QUrl, QVariant
|
from PyQt5.QtCore import QObject, QUrl, QVariant
|
||||||
from UM.FlameProfiler import pyqtSlot
|
|
||||||
from PyQt5.QtWidgets import QMessageBox
|
from PyQt5.QtWidgets import QMessageBox
|
||||||
|
|
||||||
from UM.PluginRegistry import PluginRegistry
|
from UM.i18n import i18nCatalog
|
||||||
from UM.SaveFile import SaveFile
|
from UM.FlameProfiler import pyqtSlot
|
||||||
from UM.Platform import Platform
|
|
||||||
from UM.MimeTypeDatabase import MimeTypeDatabase
|
|
||||||
|
|
||||||
from UM.Logger import Logger
|
from UM.Logger import Logger
|
||||||
from UM.Application import Application
|
from UM.MimeTypeDatabase import MimeTypeDatabase, MimeTypeNotFoundError
|
||||||
|
from UM.Platform import Platform
|
||||||
|
from UM.SaveFile import SaveFile
|
||||||
|
from UM.Settings.ContainerFormatError import ContainerFormatError
|
||||||
from UM.Settings.ContainerStack import ContainerStack
|
from UM.Settings.ContainerStack import ContainerStack
|
||||||
from UM.Settings.DefinitionContainer import DefinitionContainer
|
from UM.Settings.DefinitionContainer import DefinitionContainer
|
||||||
from UM.Settings.InstanceContainer import InstanceContainer
|
from UM.Settings.InstanceContainer import InstanceContainer
|
||||||
|
|
||||||
from UM.MimeTypeDatabase import MimeTypeNotFoundError
|
|
||||||
from UM.Settings.ContainerFormatError import ContainerFormatError
|
|
||||||
from UM.Settings.ContainerRegistry import ContainerRegistry
|
|
||||||
from cura.Settings.ExtruderManager import ExtruderManager
|
|
||||||
from UM.i18n import i18nCatalog
|
|
||||||
|
|
||||||
catalog = i18nCatalog("cura")
|
catalog = i18nCatalog("cura")
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -36,23 +30,37 @@ catalog = i18nCatalog("cura")
|
||||||
# from within QML. We want to be able to trigger things like removing a container
|
# from within QML. We want to be able to trigger things like removing a container
|
||||||
# when a certain action happens. This can be done through this class.
|
# when a certain action happens. This can be done through this class.
|
||||||
class ContainerManager(QObject):
|
class ContainerManager(QObject):
|
||||||
def __init__(self, parent = None):
|
|
||||||
super().__init__(parent)
|
|
||||||
|
|
||||||
self._application = Application.getInstance()
|
def __init__(self, application):
|
||||||
self._container_registry = ContainerRegistry.getInstance()
|
if ContainerManager.__instance is not None:
|
||||||
|
raise RuntimeError("Try to create singleton '%s' more than once" % self.__class__.__name__)
|
||||||
|
ContainerManager.__instance = self
|
||||||
|
|
||||||
|
super().__init__(parent = application)
|
||||||
|
|
||||||
|
self._application = application
|
||||||
|
self._plugin_registry = self._application.getPluginRegistry()
|
||||||
|
self._container_registry = self._application.getContainerRegistry()
|
||||||
self._machine_manager = self._application.getMachineManager()
|
self._machine_manager = self._application.getMachineManager()
|
||||||
self._material_manager = self._application.getMaterialManager()
|
self._material_manager = self._application.getMaterialManager()
|
||||||
self._container_name_filters = {}
|
self._quality_manager = self._application.getQualityManager()
|
||||||
|
self._container_name_filters = {} # type: Dict[str, Dict[str, Any]]
|
||||||
|
|
||||||
@pyqtSlot(str, str, result=str)
|
@pyqtSlot(str, str, result=str)
|
||||||
def getContainerMetaDataEntry(self, container_id, entry_name):
|
def getContainerMetaDataEntry(self, container_id: str, entry_names: str) -> str:
|
||||||
metadatas = self._container_registry.findContainersMetadata(id = container_id)
|
metadatas = self._container_registry.findContainersMetadata(id = container_id)
|
||||||
if not metadatas:
|
if not metadatas:
|
||||||
Logger.log("w", "Could not get metadata of container %s because it was not found.", container_id)
|
Logger.log("w", "Could not get metadata of container %s because it was not found.", container_id)
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
return str(metadatas[0].get(entry_name, ""))
|
entries = entry_names.split("/")
|
||||||
|
result = metadatas[0]
|
||||||
|
while entries:
|
||||||
|
entry = entries.pop(0)
|
||||||
|
result = result.get(entry, {})
|
||||||
|
if not result:
|
||||||
|
return ""
|
||||||
|
return str(result)
|
||||||
|
|
||||||
## Set a metadata entry of the specified container.
|
## Set a metadata entry of the specified container.
|
||||||
#
|
#
|
||||||
|
|
@ -67,6 +75,7 @@ class ContainerManager(QObject):
|
||||||
#
|
#
|
||||||
# \return True if successful, False if not.
|
# \return True if successful, False if not.
|
||||||
# TODO: This is ONLY used by MaterialView for material containers. Maybe refactor this.
|
# TODO: This is ONLY used by MaterialView for material containers. Maybe refactor this.
|
||||||
|
# Update: In order for QML to use objects and sub objects, those (sub) objects must all be QObject. Is that what we want?
|
||||||
@pyqtSlot("QVariant", str, str)
|
@pyqtSlot("QVariant", str, str)
|
||||||
def setContainerMetaDataEntry(self, container_node, entry_name, entry_value):
|
def setContainerMetaDataEntry(self, container_node, entry_name, entry_value):
|
||||||
root_material_id = container_node.metadata["base_file"]
|
root_material_id = container_node.metadata["base_file"]
|
||||||
|
|
@ -101,63 +110,6 @@ class ContainerManager(QObject):
|
||||||
if sub_item_changed: #If it was only a sub-item that has changed then the setMetaDataEntry won't correctly notice that something changed, and we must manually signal that the metadata changed.
|
if sub_item_changed: #If it was only a sub-item that has changed then the setMetaDataEntry won't correctly notice that something changed, and we must manually signal that the metadata changed.
|
||||||
container.metaDataChanged.emit(container)
|
container.metaDataChanged.emit(container)
|
||||||
|
|
||||||
## Set a setting property of the specified container.
|
|
||||||
#
|
|
||||||
# This will set the specified property of the specified setting of the container
|
|
||||||
# and all containers that share the same base_file (if any). The latter only
|
|
||||||
# happens for material containers.
|
|
||||||
#
|
|
||||||
# \param container_id \type{str} The ID of the container to change.
|
|
||||||
# \param setting_key \type{str} The key of the setting.
|
|
||||||
# \param property_name \type{str} The name of the property, eg "value".
|
|
||||||
# \param property_value \type{str} The new value of the property.
|
|
||||||
#
|
|
||||||
# \return True if successful, False if not.
|
|
||||||
@pyqtSlot(str, str, str, str, result = bool)
|
|
||||||
def setContainerProperty(self, container_id, setting_key, property_name, property_value):
|
|
||||||
if self._container_registry.isReadOnly(container_id):
|
|
||||||
Logger.log("w", "Cannot set properties of read-only container %s.", container_id)
|
|
||||||
return False
|
|
||||||
|
|
||||||
containers = self._container_registry.findContainers(id = container_id)
|
|
||||||
if not containers:
|
|
||||||
Logger.log("w", "Could not set properties of container %s because it was not found.", container_id)
|
|
||||||
return False
|
|
||||||
|
|
||||||
container = containers[0]
|
|
||||||
|
|
||||||
container.setProperty(setting_key, property_name, property_value)
|
|
||||||
|
|
||||||
basefile = container.getMetaDataEntry("base_file", container_id)
|
|
||||||
for sibbling_container in ContainerRegistry.getInstance().findInstanceContainers(base_file = basefile):
|
|
||||||
if sibbling_container != container:
|
|
||||||
sibbling_container.setProperty(setting_key, property_name, property_value)
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
## Get a setting property of the specified container.
|
|
||||||
#
|
|
||||||
# This will get the specified property of the specified setting of the
|
|
||||||
# specified container.
|
|
||||||
#
|
|
||||||
# \param container_id The ID of the container to get the setting property
|
|
||||||
# of.
|
|
||||||
# \param setting_key The key of the setting to get the property of.
|
|
||||||
# \param property_name The property to obtain.
|
|
||||||
# \return The value of the specified property. The type of this property
|
|
||||||
# value depends on the type of the property. For instance, the "value"
|
|
||||||
# property of an integer setting will be a Python int, but the "value"
|
|
||||||
# property of an enum setting will be a Python str.
|
|
||||||
@pyqtSlot(str, str, str, result = QVariant)
|
|
||||||
def getContainerProperty(self, container_id: str, setting_key: str, property_name: str):
|
|
||||||
containers = self._container_registry.findContainers(id = container_id)
|
|
||||||
if not containers:
|
|
||||||
Logger.log("w", "Could not get properties of container %s because it was not found.", container_id)
|
|
||||||
return ""
|
|
||||||
container = containers[0]
|
|
||||||
|
|
||||||
return container.getProperty(setting_key, property_name)
|
|
||||||
|
|
||||||
@pyqtSlot(str, result = str)
|
@pyqtSlot(str, result = str)
|
||||||
def makeUniqueName(self, original_name):
|
def makeUniqueName(self, original_name):
|
||||||
return self._container_registry.uniqueName(original_name)
|
return self._container_registry.uniqueName(original_name)
|
||||||
|
|
@ -207,7 +159,6 @@ class ContainerManager(QObject):
|
||||||
if not file_url:
|
if not file_url:
|
||||||
return {"status": "error", "message": "Invalid path"}
|
return {"status": "error", "message": "Invalid path"}
|
||||||
|
|
||||||
mime_type = None
|
|
||||||
if file_type not in self._container_name_filters:
|
if file_type not in self._container_name_filters:
|
||||||
try:
|
try:
|
||||||
mime_type = MimeTypeDatabase.getMimeTypeForFile(file_url)
|
mime_type = MimeTypeDatabase.getMimeTypeForFile(file_url)
|
||||||
|
|
@ -307,15 +258,25 @@ class ContainerManager(QObject):
|
||||||
# \return \type{bool} True if successful, False if not.
|
# \return \type{bool} True if successful, False if not.
|
||||||
@pyqtSlot(result = bool)
|
@pyqtSlot(result = bool)
|
||||||
def updateQualityChanges(self):
|
def updateQualityChanges(self):
|
||||||
global_stack = Application.getInstance().getGlobalContainerStack()
|
global_stack = self._machine_manager.activeMachine
|
||||||
if not global_stack:
|
if not global_stack:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
self._machine_manager.blurSettings.emit()
|
self._machine_manager.blurSettings.emit()
|
||||||
|
|
||||||
for stack in ExtruderManager.getInstance().getActiveGlobalAndExtruderStacks():
|
current_quality_changes_name = global_stack.qualityChanges.getName()
|
||||||
|
current_quality_type = global_stack.quality.getMetaDataEntry("quality_type")
|
||||||
|
extruder_stacks = list(global_stack.extruders.values())
|
||||||
|
for stack in [global_stack] + extruder_stacks:
|
||||||
# Find the quality_changes container for this stack and merge the contents of the top container into it.
|
# Find the quality_changes container for this stack and merge the contents of the top container into it.
|
||||||
quality_changes = stack.qualityChanges
|
quality_changes = stack.qualityChanges
|
||||||
|
|
||||||
|
if quality_changes.getId() == "empty_quality_changes":
|
||||||
|
quality_changes = self._quality_manager._createQualityChanges(current_quality_type, current_quality_changes_name,
|
||||||
|
global_stack, stack)
|
||||||
|
self._container_registry.addContainer(quality_changes)
|
||||||
|
stack.qualityChanges = quality_changes
|
||||||
|
|
||||||
if not quality_changes or self._container_registry.isReadOnly(quality_changes.getId()):
|
if not quality_changes or self._container_registry.isReadOnly(quality_changes.getId()):
|
||||||
Logger.log("e", "Could not update quality of a nonexistant or read only quality profile in stack %s", stack.getId())
|
Logger.log("e", "Could not update quality of a nonexistant or read only quality profile in stack %s", stack.getId())
|
||||||
continue
|
continue
|
||||||
|
|
@ -334,13 +295,15 @@ class ContainerManager(QObject):
|
||||||
send_emits_containers = []
|
send_emits_containers = []
|
||||||
|
|
||||||
# Go through global and extruder stacks and clear their topmost container (the user settings).
|
# Go through global and extruder stacks and clear their topmost container (the user settings).
|
||||||
for stack in ExtruderManager.getInstance().getActiveGlobalAndExtruderStacks():
|
global_stack = self._machine_manager.activeMachine
|
||||||
|
extruder_stacks = list(global_stack.extruders.values())
|
||||||
|
for stack in [global_stack] + extruder_stacks:
|
||||||
container = stack.userChanges
|
container = stack.userChanges
|
||||||
container.clear()
|
container.clear()
|
||||||
send_emits_containers.append(container)
|
send_emits_containers.append(container)
|
||||||
|
|
||||||
# user changes are possibly added to make the current setup match the current enabled extruders
|
# user changes are possibly added to make the current setup match the current enabled extruders
|
||||||
Application.getInstance().getMachineManager().correctExtruderSettings()
|
self._machine_manager.correctExtruderSettings()
|
||||||
|
|
||||||
for container in send_emits_containers:
|
for container in send_emits_containers:
|
||||||
container.sendPostponedEmits()
|
container.sendPostponedEmits()
|
||||||
|
|
@ -381,21 +344,6 @@ class ContainerManager(QObject):
|
||||||
if container is not None:
|
if container is not None:
|
||||||
container.setMetaDataEntry("GUID", new_guid)
|
container.setMetaDataEntry("GUID", new_guid)
|
||||||
|
|
||||||
## Get the singleton instance for this class.
|
|
||||||
@classmethod
|
|
||||||
def getInstance(cls) -> "ContainerManager":
|
|
||||||
# Note: Explicit use of class name to prevent issues with inheritance.
|
|
||||||
if ContainerManager.__instance is None:
|
|
||||||
ContainerManager.__instance = cls()
|
|
||||||
return ContainerManager.__instance
|
|
||||||
|
|
||||||
__instance = None # type: "ContainerManager"
|
|
||||||
|
|
||||||
# Factory function, used by QML
|
|
||||||
@staticmethod
|
|
||||||
def createContainerManager(engine, js_engine):
|
|
||||||
return ContainerManager.getInstance()
|
|
||||||
|
|
||||||
def _performMerge(self, merge_into, merge, clear_settings = True):
|
def _performMerge(self, merge_into, merge, clear_settings = True):
|
||||||
if merge == merge_into:
|
if merge == merge_into:
|
||||||
return
|
return
|
||||||
|
|
@ -415,7 +363,7 @@ class ContainerManager(QObject):
|
||||||
|
|
||||||
serialize_type = ""
|
serialize_type = ""
|
||||||
try:
|
try:
|
||||||
plugin_metadata = PluginRegistry.getInstance().getMetaData(plugin_id)
|
plugin_metadata = self._plugin_registry.getMetaData(plugin_id)
|
||||||
if plugin_metadata:
|
if plugin_metadata:
|
||||||
serialize_type = plugin_metadata["settings_container"]["type"]
|
serialize_type = plugin_metadata["settings_container"]["type"]
|
||||||
else:
|
else:
|
||||||
|
|
@ -470,3 +418,9 @@ class ContainerManager(QObject):
|
||||||
|
|
||||||
container_list = [n.getContainer() for n in quality_changes_group.getAllNodes() if n.getContainer() is not None]
|
container_list = [n.getContainer() for n in quality_changes_group.getAllNodes() if n.getContainer() is not None]
|
||||||
self._container_registry.exportQualityProfile(container_list, path, file_type)
|
self._container_registry.exportQualityProfile(container_list, path, file_type)
|
||||||
|
|
||||||
|
__instance = None # type: ContainerManager
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def getInstance(cls, *args, **kwargs) -> "ContainerManager":
|
||||||
|
return cls.__instance
|
||||||
|
|
|
||||||
|
|
@ -2,11 +2,10 @@
|
||||||
# Cura is released under the terms of the LGPLv3 or higher.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import os.path
|
|
||||||
import re
|
import re
|
||||||
import configparser
|
import configparser
|
||||||
|
|
||||||
from typing import Optional
|
from typing import cast, Optional
|
||||||
|
|
||||||
from PyQt5.QtWidgets import QMessageBox
|
from PyQt5.QtWidgets import QMessageBox
|
||||||
|
|
||||||
|
|
@ -27,9 +26,9 @@ from UM.Resources import Resources
|
||||||
from . import ExtruderStack
|
from . import ExtruderStack
|
||||||
from . import GlobalStack
|
from . import GlobalStack
|
||||||
|
|
||||||
from cura.CuraApplication import CuraApplication
|
import cura.CuraApplication
|
||||||
from cura.Machines.QualityManager import getMachineDefinitionIDForQualitySearch
|
from cura.Machines.QualityManager import getMachineDefinitionIDForQualitySearch
|
||||||
from cura.ProfileReader import NoProfileException
|
from cura.ReaderWriters.ProfileReader import NoProfileException
|
||||||
|
|
||||||
from UM.i18n import i18nCatalog
|
from UM.i18n import i18nCatalog
|
||||||
catalog = i18nCatalog("cura")
|
catalog = i18nCatalog("cura")
|
||||||
|
|
@ -58,7 +57,7 @@ class CuraContainerRegistry(ContainerRegistry):
|
||||||
|
|
||||||
if isinstance(container, InstanceContainer) and type(container) != type(self.getEmptyInstanceContainer()):
|
if isinstance(container, InstanceContainer) and type(container) != type(self.getEmptyInstanceContainer()):
|
||||||
# Check against setting version of the definition.
|
# Check against setting version of the definition.
|
||||||
required_setting_version = CuraApplication.SettingVersion
|
required_setting_version = cura.CuraApplication.CuraApplication.SettingVersion
|
||||||
actual_setting_version = int(container.getMetaDataEntry("setting_version", default = 0))
|
actual_setting_version = int(container.getMetaDataEntry("setting_version", default = 0))
|
||||||
if required_setting_version != actual_setting_version:
|
if required_setting_version != actual_setting_version:
|
||||||
Logger.log("w", "Instance container {container_id} is outdated. Its setting version is {actual_setting_version} but it should be {required_setting_version}.".format(container_id = container.getId(), actual_setting_version = actual_setting_version, required_setting_version = required_setting_version))
|
Logger.log("w", "Instance container {container_id} is outdated. Its setting version is {actual_setting_version} but it should be {required_setting_version}.".format(container_id = container.getId(), actual_setting_version = actual_setting_version, required_setting_version = required_setting_version))
|
||||||
|
|
@ -191,7 +190,7 @@ class CuraContainerRegistry(ContainerRegistry):
|
||||||
return { "status": "ok", "message": catalog.i18nc("@info:status Don't translate the XML tags <filename> or <message>!", "No custom profile to import in file <filename>{0}</filename>", file_name)}
|
return { "status": "ok", "message": catalog.i18nc("@info:status Don't translate the XML tags <filename> or <message>!", "No custom profile to import in file <filename>{0}</filename>", file_name)}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
# Note that this will fail quickly. That is, if any profile reader throws an exception, it will stop reading. It will only continue reading if the reader returned None.
|
# Note that this will fail quickly. That is, if any profile reader throws an exception, it will stop reading. It will only continue reading if the reader returned None.
|
||||||
Logger.log("e", "Failed to import profile from %s: %s while using profile reader. Got exception %s", file_name,profile_reader.getPluginId(), str(e))
|
Logger.log("e", "Failed to import profile from %s: %s while using profile reader. Got exception %s", file_name, profile_reader.getPluginId(), str(e))
|
||||||
return { "status": "error", "message": catalog.i18nc("@info:status Don't translate the XML tags <filename> or <message>!", "Failed to import profile from <filename>{0}</filename>: <message>{1}</message>", file_name, "\n" + str(e))}
|
return { "status": "error", "message": catalog.i18nc("@info:status Don't translate the XML tags <filename> or <message>!", "Failed to import profile from <filename>{0}</filename>: <message>{1}</message>", file_name, "\n" + str(e))}
|
||||||
|
|
||||||
if profile_or_list:
|
if profile_or_list:
|
||||||
|
|
@ -261,11 +260,11 @@ class CuraContainerRegistry(ContainerRegistry):
|
||||||
profile_id = ContainerRegistry.getInstance().uniqueName(global_stack.getId() + "_extruder_" + str(idx + 1))
|
profile_id = ContainerRegistry.getInstance().uniqueName(global_stack.getId() + "_extruder_" + str(idx + 1))
|
||||||
profile = InstanceContainer(profile_id)
|
profile = InstanceContainer(profile_id)
|
||||||
profile.setName(quality_name)
|
profile.setName(quality_name)
|
||||||
profile.addMetaDataEntry("setting_version", CuraApplication.SettingVersion)
|
profile.setMetaDataEntry("setting_version", cura.CuraApplication.CuraApplication.SettingVersion)
|
||||||
profile.addMetaDataEntry("type", "quality_changes")
|
profile.setMetaDataEntry("type", "quality_changes")
|
||||||
profile.addMetaDataEntry("definition", expected_machine_definition)
|
profile.setMetaDataEntry("definition", expected_machine_definition)
|
||||||
profile.addMetaDataEntry("quality_type", quality_type)
|
profile.setMetaDataEntry("quality_type", quality_type)
|
||||||
profile.addMetaDataEntry("position", "0")
|
profile.setMetaDataEntry("position", "0")
|
||||||
profile.setDirty(True)
|
profile.setDirty(True)
|
||||||
if idx == 0:
|
if idx == 0:
|
||||||
# move all per-extruder settings to the first extruder's quality_changes
|
# move all per-extruder settings to the first extruder's quality_changes
|
||||||
|
|
@ -299,7 +298,7 @@ class CuraContainerRegistry(ContainerRegistry):
|
||||||
extruder_id = machine_extruders[profile_index - 1].definition.getId()
|
extruder_id = machine_extruders[profile_index - 1].definition.getId()
|
||||||
extruder_position = str(profile_index - 1)
|
extruder_position = str(profile_index - 1)
|
||||||
if not profile.getMetaDataEntry("position"):
|
if not profile.getMetaDataEntry("position"):
|
||||||
profile.addMetaDataEntry("position", extruder_position)
|
profile.setMetaDataEntry("position", extruder_position)
|
||||||
else:
|
else:
|
||||||
profile.setMetaDataEntry("position", extruder_position)
|
profile.setMetaDataEntry("position", extruder_position)
|
||||||
profile_id = (extruder_id + "_" + name_seed).lower().replace(" ", "_")
|
profile_id = (extruder_id + "_" + name_seed).lower().replace(" ", "_")
|
||||||
|
|
@ -350,20 +349,22 @@ class CuraContainerRegistry(ContainerRegistry):
|
||||||
if "type" in profile.getMetaData():
|
if "type" in profile.getMetaData():
|
||||||
profile.setMetaDataEntry("type", "quality_changes")
|
profile.setMetaDataEntry("type", "quality_changes")
|
||||||
else:
|
else:
|
||||||
profile.addMetaDataEntry("type", "quality_changes")
|
profile.setMetaDataEntry("type", "quality_changes")
|
||||||
|
|
||||||
quality_type = profile.getMetaDataEntry("quality_type")
|
quality_type = profile.getMetaDataEntry("quality_type")
|
||||||
if not quality_type:
|
if not quality_type:
|
||||||
return catalog.i18nc("@info:status", "Profile is missing a quality type.")
|
return catalog.i18nc("@info:status", "Profile is missing a quality type.")
|
||||||
|
|
||||||
global_stack = Application.getInstance().getGlobalContainerStack()
|
global_stack = Application.getInstance().getGlobalContainerStack()
|
||||||
|
if global_stack is None:
|
||||||
|
return None
|
||||||
definition_id = getMachineDefinitionIDForQualitySearch(global_stack.definition)
|
definition_id = getMachineDefinitionIDForQualitySearch(global_stack.definition)
|
||||||
profile.setDefinition(definition_id)
|
profile.setDefinition(definition_id)
|
||||||
|
|
||||||
# Check to make sure the imported profile actually makes sense in context of the current configuration.
|
# Check to make sure the imported profile actually makes sense in context of the current configuration.
|
||||||
# This prevents issues where importing a "draft" profile for a machine without "draft" qualities would report as
|
# This prevents issues where importing a "draft" profile for a machine without "draft" qualities would report as
|
||||||
# successfully imported but then fail to show up.
|
# successfully imported but then fail to show up.
|
||||||
quality_manager = CuraApplication.getInstance()._quality_manager
|
quality_manager = cura.CuraApplication.CuraApplication.getInstance()._quality_manager
|
||||||
quality_group_dict = quality_manager.getQualityGroupsForMachineDefinition(global_stack)
|
quality_group_dict = quality_manager.getQualityGroupsForMachineDefinition(global_stack)
|
||||||
if quality_type not in quality_group_dict:
|
if quality_type not in quality_group_dict:
|
||||||
return catalog.i18nc("@info:status", "Could not find a quality type {0} for the current configuration.", quality_type)
|
return catalog.i18nc("@info:status", "Could not find a quality type {0} for the current configuration.", quality_type)
|
||||||
|
|
@ -466,7 +467,7 @@ class CuraContainerRegistry(ContainerRegistry):
|
||||||
def addExtruderStackForSingleExtrusionMachine(self, machine, extruder_id, new_global_quality_changes = None, create_new_ids = True):
|
def addExtruderStackForSingleExtrusionMachine(self, machine, extruder_id, new_global_quality_changes = None, create_new_ids = True):
|
||||||
new_extruder_id = extruder_id
|
new_extruder_id = extruder_id
|
||||||
|
|
||||||
application = CuraApplication.getInstance()
|
application = cura.CuraApplication.CuraApplication.getInstance()
|
||||||
|
|
||||||
extruder_definitions = self.findDefinitionContainers(id = new_extruder_id)
|
extruder_definitions = self.findDefinitionContainers(id = new_extruder_id)
|
||||||
if not extruder_definitions:
|
if not extruder_definitions:
|
||||||
|
|
@ -476,19 +477,19 @@ class CuraContainerRegistry(ContainerRegistry):
|
||||||
extruder_definition = extruder_definitions[0]
|
extruder_definition = extruder_definitions[0]
|
||||||
unique_name = self.uniqueName(machine.getName() + " " + new_extruder_id) if create_new_ids else machine.getName() + " " + new_extruder_id
|
unique_name = self.uniqueName(machine.getName() + " " + new_extruder_id) if create_new_ids else machine.getName() + " " + new_extruder_id
|
||||||
|
|
||||||
extruder_stack = ExtruderStack.ExtruderStack(unique_name, parent = machine)
|
extruder_stack = ExtruderStack.ExtruderStack(unique_name)
|
||||||
extruder_stack.setName(extruder_definition.getName())
|
extruder_stack.setName(extruder_definition.getName())
|
||||||
extruder_stack.setDefinition(extruder_definition)
|
extruder_stack.setDefinition(extruder_definition)
|
||||||
extruder_stack.addMetaDataEntry("position", extruder_definition.getMetaDataEntry("position"))
|
extruder_stack.setMetaDataEntry("position", extruder_definition.getMetaDataEntry("position"))
|
||||||
|
|
||||||
# create a new definition_changes container for the extruder stack
|
# create a new definition_changes container for the extruder stack
|
||||||
definition_changes_id = self.uniqueName(extruder_stack.getId() + "_settings") if create_new_ids else extruder_stack.getId() + "_settings"
|
definition_changes_id = self.uniqueName(extruder_stack.getId() + "_settings") if create_new_ids else extruder_stack.getId() + "_settings"
|
||||||
definition_changes_name = definition_changes_id
|
definition_changes_name = definition_changes_id
|
||||||
definition_changes = InstanceContainer(definition_changes_id, parent = application)
|
definition_changes = InstanceContainer(definition_changes_id, parent = application)
|
||||||
definition_changes.setName(definition_changes_name)
|
definition_changes.setName(definition_changes_name)
|
||||||
definition_changes.addMetaDataEntry("setting_version", CuraApplication.SettingVersion)
|
definition_changes.setMetaDataEntry("setting_version", application.SettingVersion)
|
||||||
definition_changes.addMetaDataEntry("type", "definition_changes")
|
definition_changes.setMetaDataEntry("type", "definition_changes")
|
||||||
definition_changes.addMetaDataEntry("definition", extruder_definition.getId())
|
definition_changes.setMetaDataEntry("definition", extruder_definition.getId())
|
||||||
|
|
||||||
# move definition_changes settings if exist
|
# move definition_changes settings if exist
|
||||||
for setting_key in definition_changes.getAllKeys():
|
for setting_key in definition_changes.getAllKeys():
|
||||||
|
|
@ -513,9 +514,9 @@ class CuraContainerRegistry(ContainerRegistry):
|
||||||
user_container_name = user_container_id
|
user_container_name = user_container_id
|
||||||
user_container = InstanceContainer(user_container_id, parent = application)
|
user_container = InstanceContainer(user_container_id, parent = application)
|
||||||
user_container.setName(user_container_name)
|
user_container.setName(user_container_name)
|
||||||
user_container.addMetaDataEntry("type", "user")
|
user_container.setMetaDataEntry("type", "user")
|
||||||
user_container.addMetaDataEntry("machine", machine.getId())
|
user_container.setMetaDataEntry("machine", machine.getId())
|
||||||
user_container.addMetaDataEntry("setting_version", CuraApplication.SettingVersion)
|
user_container.setMetaDataEntry("setting_version", application.SettingVersion)
|
||||||
user_container.setDefinition(machine.definition.getId())
|
user_container.setDefinition(machine.definition.getId())
|
||||||
user_container.setMetaDataEntry("position", extruder_stack.getMetaDataEntry("position"))
|
user_container.setMetaDataEntry("position", extruder_stack.getMetaDataEntry("position"))
|
||||||
|
|
||||||
|
|
@ -579,7 +580,7 @@ class CuraContainerRegistry(ContainerRegistry):
|
||||||
extruder_quality_changes_container = self._findQualityChangesContainerInCuraFolder(machine_quality_changes.getName())
|
extruder_quality_changes_container = self._findQualityChangesContainerInCuraFolder(machine_quality_changes.getName())
|
||||||
if extruder_quality_changes_container:
|
if extruder_quality_changes_container:
|
||||||
quality_changes_id = extruder_quality_changes_container.getId()
|
quality_changes_id = extruder_quality_changes_container.getId()
|
||||||
extruder_quality_changes_container.addMetaDataEntry("position", extruder_definition.getMetaDataEntry("position"))
|
extruder_quality_changes_container.setMetaDataEntry("position", extruder_definition.getMetaDataEntry("position"))
|
||||||
extruder_stack.qualityChanges = self.findInstanceContainers(id = quality_changes_id)[0]
|
extruder_stack.qualityChanges = self.findInstanceContainers(id = quality_changes_id)[0]
|
||||||
else:
|
else:
|
||||||
# if we still cannot find a quality changes container for the extruder, create a new one
|
# if we still cannot find a quality changes container for the extruder, create a new one
|
||||||
|
|
@ -587,10 +588,10 @@ class CuraContainerRegistry(ContainerRegistry):
|
||||||
container_id = self.uniqueName(extruder_stack.getId() + "_qc_" + container_name)
|
container_id = self.uniqueName(extruder_stack.getId() + "_qc_" + container_name)
|
||||||
extruder_quality_changes_container = InstanceContainer(container_id, parent = application)
|
extruder_quality_changes_container = InstanceContainer(container_id, parent = application)
|
||||||
extruder_quality_changes_container.setName(container_name)
|
extruder_quality_changes_container.setName(container_name)
|
||||||
extruder_quality_changes_container.addMetaDataEntry("type", "quality_changes")
|
extruder_quality_changes_container.setMetaDataEntry("type", "quality_changes")
|
||||||
extruder_quality_changes_container.addMetaDataEntry("setting_version", CuraApplication.SettingVersion)
|
extruder_quality_changes_container.setMetaDataEntry("setting_version", application.SettingVersion)
|
||||||
extruder_quality_changes_container.addMetaDataEntry("position", extruder_definition.getMetaDataEntry("position"))
|
extruder_quality_changes_container.setMetaDataEntry("position", extruder_definition.getMetaDataEntry("position"))
|
||||||
extruder_quality_changes_container.addMetaDataEntry("quality_type", machine_quality_changes.getMetaDataEntry("quality_type"))
|
extruder_quality_changes_container.setMetaDataEntry("quality_type", machine_quality_changes.getMetaDataEntry("quality_type"))
|
||||||
extruder_quality_changes_container.setDefinition(machine_quality_changes.getDefinition().getId())
|
extruder_quality_changes_container.setDefinition(machine_quality_changes.getDefinition().getId())
|
||||||
|
|
||||||
self.addContainer(extruder_quality_changes_container)
|
self.addContainer(extruder_quality_changes_container)
|
||||||
|
|
@ -676,7 +677,7 @@ class CuraContainerRegistry(ContainerRegistry):
|
||||||
return extruder_stack
|
return extruder_stack
|
||||||
|
|
||||||
def _findQualityChangesContainerInCuraFolder(self, name):
|
def _findQualityChangesContainerInCuraFolder(self, name):
|
||||||
quality_changes_dir = Resources.getPath(CuraApplication.ResourceTypes.QualityInstanceContainer)
|
quality_changes_dir = Resources.getPath(cura.CuraApplication.CuraApplication.ResourceTypes.QualityChangesInstanceContainer)
|
||||||
|
|
||||||
instance_container = None
|
instance_container = None
|
||||||
|
|
||||||
|
|
@ -732,3 +733,9 @@ class CuraContainerRegistry(ContainerRegistry):
|
||||||
extruder_stack.setNextStack(machines[0])
|
extruder_stack.setNextStack(machines[0])
|
||||||
else:
|
else:
|
||||||
Logger.log("w", "Could not find machine {machine} for extruder {extruder}", machine = extruder_stack.getMetaDataEntry("machine"), extruder = extruder_stack.getId())
|
Logger.log("w", "Could not find machine {machine} for extruder {extruder}", machine = extruder_stack.getMetaDataEntry("machine"), extruder = extruder_stack.getId())
|
||||||
|
|
||||||
|
#Override just for the type.
|
||||||
|
@classmethod
|
||||||
|
@override(ContainerRegistry)
|
||||||
|
def getInstance(cls, *args, **kwargs) -> "CuraContainerRegistry":
|
||||||
|
return cast(CuraContainerRegistry, super().getInstance(*args, **kwargs))
|
||||||
|
|
@ -1,21 +1,19 @@
|
||||||
# Copyright (c) 2017 Ultimaker B.V.
|
# Copyright (c) 2018 Ultimaker B.V.
|
||||||
# Cura is released under the terms of the LGPLv3 or higher.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
import os.path
|
from typing import Any, cast, List, Optional
|
||||||
|
|
||||||
from typing import Any, Optional
|
|
||||||
|
|
||||||
from PyQt5.QtCore import pyqtProperty, pyqtSignal, QObject
|
from PyQt5.QtCore import pyqtProperty, pyqtSignal, QObject
|
||||||
from UM.FlameProfiler import pyqtSlot
|
|
||||||
|
|
||||||
from UM.Application import Application
|
from UM.Application import Application
|
||||||
from UM.Decorators import override
|
from UM.Decorators import override
|
||||||
|
from UM.FlameProfiler import pyqtSlot
|
||||||
from UM.Logger import Logger
|
from UM.Logger import Logger
|
||||||
from UM.Settings.ContainerStack import ContainerStack, InvalidContainerStackError
|
from UM.Settings.ContainerStack import ContainerStack, InvalidContainerStackError
|
||||||
from UM.Settings.InstanceContainer import InstanceContainer
|
from UM.Settings.InstanceContainer import InstanceContainer
|
||||||
from UM.Settings.DefinitionContainer import DefinitionContainer
|
from UM.Settings.DefinitionContainer import DefinitionContainer
|
||||||
from UM.Settings.ContainerRegistry import ContainerRegistry
|
from UM.Settings.ContainerRegistry import ContainerRegistry
|
||||||
from UM.Settings.Interfaces import ContainerInterface, DefinitionContainerInterface
|
from UM.Settings.Interfaces import ContainerInterface, DefinitionContainerInterface
|
||||||
|
from cura.Settings import cura_empty_instance_containers
|
||||||
|
|
||||||
from . import Exceptions
|
from . import Exceptions
|
||||||
|
|
||||||
|
|
@ -39,19 +37,17 @@ from . import Exceptions
|
||||||
# This also means that operations on the stack that modifies the container ordering is prohibited and
|
# This also means that operations on the stack that modifies the container ordering is prohibited and
|
||||||
# will raise an exception.
|
# will raise an exception.
|
||||||
class CuraContainerStack(ContainerStack):
|
class CuraContainerStack(ContainerStack):
|
||||||
def __init__(self, container_id: str, *args, **kwargs):
|
def __init__(self, container_id: str) -> None:
|
||||||
super().__init__(container_id, *args, **kwargs)
|
super().__init__(container_id)
|
||||||
|
|
||||||
self._container_registry = ContainerRegistry.getInstance()
|
self._empty_instance_container = cura_empty_instance_containers.empty_container #type: InstanceContainer
|
||||||
|
|
||||||
self._empty_instance_container = self._container_registry.getEmptyInstanceContainer()
|
self._empty_quality_changes = cura_empty_instance_containers.empty_quality_changes_container #type: InstanceContainer
|
||||||
|
self._empty_quality = cura_empty_instance_containers.empty_quality_container #type: InstanceContainer
|
||||||
|
self._empty_material = cura_empty_instance_containers.empty_material_container #type: InstanceContainer
|
||||||
|
self._empty_variant = cura_empty_instance_containers.empty_variant_container #type: InstanceContainer
|
||||||
|
|
||||||
self._empty_quality_changes = self._container_registry.findInstanceContainers(id = "empty_quality_changes")[0]
|
self._containers = [self._empty_instance_container for i in range(len(_ContainerIndexes.IndexTypeMap))] #type: List[ContainerInterface]
|
||||||
self._empty_quality = self._container_registry.findInstanceContainers(id = "empty_quality")[0]
|
|
||||||
self._empty_material = self._container_registry.findInstanceContainers(id = "empty_material")[0]
|
|
||||||
self._empty_variant = self._container_registry.findInstanceContainers(id = "empty_variant")[0]
|
|
||||||
|
|
||||||
self._containers = [self._empty_instance_container for i in range(len(_ContainerIndexes.IndexTypeMap))]
|
|
||||||
self._containers[_ContainerIndexes.QualityChanges] = self._empty_quality_changes
|
self._containers[_ContainerIndexes.QualityChanges] = self._empty_quality_changes
|
||||||
self._containers[_ContainerIndexes.Quality] = self._empty_quality
|
self._containers[_ContainerIndexes.Quality] = self._empty_quality
|
||||||
self._containers[_ContainerIndexes.Material] = self._empty_material
|
self._containers[_ContainerIndexes.Material] = self._empty_material
|
||||||
|
|
@ -60,7 +56,7 @@ class CuraContainerStack(ContainerStack):
|
||||||
self.containersChanged.connect(self._onContainersChanged)
|
self.containersChanged.connect(self._onContainersChanged)
|
||||||
|
|
||||||
import cura.CuraApplication #Here to prevent circular imports.
|
import cura.CuraApplication #Here to prevent circular imports.
|
||||||
self.addMetaDataEntry("setting_version", cura.CuraApplication.CuraApplication.SettingVersion)
|
self.setMetaDataEntry("setting_version", cura.CuraApplication.CuraApplication.SettingVersion)
|
||||||
|
|
||||||
# This is emitted whenever the containersChanged signal from the ContainerStack base class is emitted.
|
# This is emitted whenever the containersChanged signal from the ContainerStack base class is emitted.
|
||||||
pyqtContainersChanged = pyqtSignal()
|
pyqtContainersChanged = pyqtSignal()
|
||||||
|
|
@ -76,7 +72,7 @@ class CuraContainerStack(ContainerStack):
|
||||||
# \return The user changes container. Should always be a valid container, but can be equal to the empty InstanceContainer.
|
# \return The user changes container. Should always be a valid container, but can be equal to the empty InstanceContainer.
|
||||||
@pyqtProperty(InstanceContainer, fset = setUserChanges, notify = pyqtContainersChanged)
|
@pyqtProperty(InstanceContainer, fset = setUserChanges, notify = pyqtContainersChanged)
|
||||||
def userChanges(self) -> InstanceContainer:
|
def userChanges(self) -> InstanceContainer:
|
||||||
return self._containers[_ContainerIndexes.UserChanges]
|
return cast(InstanceContainer, self._containers[_ContainerIndexes.UserChanges])
|
||||||
|
|
||||||
## Set the quality changes container.
|
## Set the quality changes container.
|
||||||
#
|
#
|
||||||
|
|
@ -89,12 +85,12 @@ class CuraContainerStack(ContainerStack):
|
||||||
# \return The quality changes container. Should always be a valid container, but can be equal to the empty InstanceContainer.
|
# \return The quality changes container. Should always be a valid container, but can be equal to the empty InstanceContainer.
|
||||||
@pyqtProperty(InstanceContainer, fset = setQualityChanges, notify = pyqtContainersChanged)
|
@pyqtProperty(InstanceContainer, fset = setQualityChanges, notify = pyqtContainersChanged)
|
||||||
def qualityChanges(self) -> InstanceContainer:
|
def qualityChanges(self) -> InstanceContainer:
|
||||||
return self._containers[_ContainerIndexes.QualityChanges]
|
return cast(InstanceContainer, self._containers[_ContainerIndexes.QualityChanges])
|
||||||
|
|
||||||
## Set the quality container.
|
## Set the quality container.
|
||||||
#
|
#
|
||||||
# \param new_quality The new quality container. It is expected to have a "type" metadata entry with the value "quality".
|
# \param new_quality The new quality container. It is expected to have a "type" metadata entry with the value "quality".
|
||||||
def setQuality(self, new_quality: InstanceContainer, postpone_emit = False) -> None:
|
def setQuality(self, new_quality: InstanceContainer, postpone_emit: bool = False) -> None:
|
||||||
self.replaceContainer(_ContainerIndexes.Quality, new_quality, postpone_emit = postpone_emit)
|
self.replaceContainer(_ContainerIndexes.Quality, new_quality, postpone_emit = postpone_emit)
|
||||||
|
|
||||||
## Get the quality container.
|
## Get the quality container.
|
||||||
|
|
@ -102,12 +98,12 @@ class CuraContainerStack(ContainerStack):
|
||||||
# \return The quality container. Should always be a valid container, but can be equal to the empty InstanceContainer.
|
# \return The quality container. Should always be a valid container, but can be equal to the empty InstanceContainer.
|
||||||
@pyqtProperty(InstanceContainer, fset = setQuality, notify = pyqtContainersChanged)
|
@pyqtProperty(InstanceContainer, fset = setQuality, notify = pyqtContainersChanged)
|
||||||
def quality(self) -> InstanceContainer:
|
def quality(self) -> InstanceContainer:
|
||||||
return self._containers[_ContainerIndexes.Quality]
|
return cast(InstanceContainer, self._containers[_ContainerIndexes.Quality])
|
||||||
|
|
||||||
## Set the material container.
|
## Set the material container.
|
||||||
#
|
#
|
||||||
# \param new_material The new material container. It is expected to have a "type" metadata entry with the value "material".
|
# \param new_material The new material container. It is expected to have a "type" metadata entry with the value "material".
|
||||||
def setMaterial(self, new_material: InstanceContainer, postpone_emit = False) -> None:
|
def setMaterial(self, new_material: InstanceContainer, postpone_emit: bool = False) -> None:
|
||||||
self.replaceContainer(_ContainerIndexes.Material, new_material, postpone_emit = postpone_emit)
|
self.replaceContainer(_ContainerIndexes.Material, new_material, postpone_emit = postpone_emit)
|
||||||
|
|
||||||
## Get the material container.
|
## Get the material container.
|
||||||
|
|
@ -115,7 +111,7 @@ class CuraContainerStack(ContainerStack):
|
||||||
# \return The material container. Should always be a valid container, but can be equal to the empty InstanceContainer.
|
# \return The material container. Should always be a valid container, but can be equal to the empty InstanceContainer.
|
||||||
@pyqtProperty(InstanceContainer, fset = setMaterial, notify = pyqtContainersChanged)
|
@pyqtProperty(InstanceContainer, fset = setMaterial, notify = pyqtContainersChanged)
|
||||||
def material(self) -> InstanceContainer:
|
def material(self) -> InstanceContainer:
|
||||||
return self._containers[_ContainerIndexes.Material]
|
return cast(InstanceContainer, self._containers[_ContainerIndexes.Material])
|
||||||
|
|
||||||
## Set the variant container.
|
## Set the variant container.
|
||||||
#
|
#
|
||||||
|
|
@ -128,7 +124,7 @@ class CuraContainerStack(ContainerStack):
|
||||||
# \return The variant container. Should always be a valid container, but can be equal to the empty InstanceContainer.
|
# \return The variant container. Should always be a valid container, but can be equal to the empty InstanceContainer.
|
||||||
@pyqtProperty(InstanceContainer, fset = setVariant, notify = pyqtContainersChanged)
|
@pyqtProperty(InstanceContainer, fset = setVariant, notify = pyqtContainersChanged)
|
||||||
def variant(self) -> InstanceContainer:
|
def variant(self) -> InstanceContainer:
|
||||||
return self._containers[_ContainerIndexes.Variant]
|
return cast(InstanceContainer, self._containers[_ContainerIndexes.Variant])
|
||||||
|
|
||||||
## Set the definition changes container.
|
## Set the definition changes container.
|
||||||
#
|
#
|
||||||
|
|
@ -141,7 +137,7 @@ class CuraContainerStack(ContainerStack):
|
||||||
# \return The definition changes container. Should always be a valid container, but can be equal to the empty InstanceContainer.
|
# \return The definition changes container. Should always be a valid container, but can be equal to the empty InstanceContainer.
|
||||||
@pyqtProperty(InstanceContainer, fset = setDefinitionChanges, notify = pyqtContainersChanged)
|
@pyqtProperty(InstanceContainer, fset = setDefinitionChanges, notify = pyqtContainersChanged)
|
||||||
def definitionChanges(self) -> InstanceContainer:
|
def definitionChanges(self) -> InstanceContainer:
|
||||||
return self._containers[_ContainerIndexes.DefinitionChanges]
|
return cast(InstanceContainer, self._containers[_ContainerIndexes.DefinitionChanges])
|
||||||
|
|
||||||
## Set the definition container.
|
## Set the definition container.
|
||||||
#
|
#
|
||||||
|
|
@ -154,7 +150,7 @@ class CuraContainerStack(ContainerStack):
|
||||||
# \return The definition container. Should always be a valid container, but can be equal to the empty InstanceContainer.
|
# \return The definition container. Should always be a valid container, but can be equal to the empty InstanceContainer.
|
||||||
@pyqtProperty(QObject, fset = setDefinition, notify = pyqtContainersChanged)
|
@pyqtProperty(QObject, fset = setDefinition, notify = pyqtContainersChanged)
|
||||||
def definition(self) -> DefinitionContainer:
|
def definition(self) -> DefinitionContainer:
|
||||||
return self._containers[_ContainerIndexes.Definition]
|
return cast(DefinitionContainer, self._containers[_ContainerIndexes.Definition])
|
||||||
|
|
||||||
@override(ContainerStack)
|
@override(ContainerStack)
|
||||||
def getBottom(self) -> "DefinitionContainer":
|
def getBottom(self) -> "DefinitionContainer":
|
||||||
|
|
@ -189,13 +185,9 @@ class CuraContainerStack(ContainerStack):
|
||||||
# \param key The key of the setting to set.
|
# \param key The key of the setting to set.
|
||||||
# \param property_name The name of the property to set.
|
# \param property_name The name of the property to set.
|
||||||
# \param new_value The new value to set the property to.
|
# \param new_value The new value to set the property to.
|
||||||
# \param target_container The type of the container to set the property of. Defaults to "user".
|
def setProperty(self, key: str, property_name: str, property_value: Any, container: "ContainerInterface" = None, set_from_cache: bool = False) -> None:
|
||||||
def setProperty(self, key: str, property_name: str, new_value: Any, target_container: str = "user") -> None:
|
container_index = _ContainerIndexes.UserChanges
|
||||||
container_index = _ContainerIndexes.TypeIndexMap.get(target_container, -1)
|
self._containers[container_index].setProperty(key, property_name, property_value, container, set_from_cache)
|
||||||
if container_index != -1:
|
|
||||||
self._containers[container_index].setProperty(key, property_name, new_value)
|
|
||||||
else:
|
|
||||||
raise IndexError("Invalid target container {type}".format(type = target_container))
|
|
||||||
|
|
||||||
## Overridden from ContainerStack
|
## Overridden from ContainerStack
|
||||||
#
|
#
|
||||||
|
|
@ -251,8 +243,9 @@ class CuraContainerStack(ContainerStack):
|
||||||
#
|
#
|
||||||
# \throws InvalidContainerStackError Raised when no definition can be found for the stack.
|
# \throws InvalidContainerStackError Raised when no definition can be found for the stack.
|
||||||
@override(ContainerStack)
|
@override(ContainerStack)
|
||||||
def deserialize(self, contents: str, file_name: Optional[str] = None) -> None:
|
def deserialize(self, serialized: str, file_name: Optional[str] = None) -> str:
|
||||||
super().deserialize(contents, file_name)
|
# update the serialized data first
|
||||||
|
serialized = super().deserialize(serialized, file_name)
|
||||||
|
|
||||||
new_containers = self._containers.copy()
|
new_containers = self._containers.copy()
|
||||||
while len(new_containers) < len(_ContainerIndexes.IndexTypeMap):
|
while len(new_containers) < len(_ContainerIndexes.IndexTypeMap):
|
||||||
|
|
@ -260,10 +253,11 @@ class CuraContainerStack(ContainerStack):
|
||||||
|
|
||||||
# Validate and ensure the list of containers matches with what we expect
|
# Validate and ensure the list of containers matches with what we expect
|
||||||
for index, type_name in _ContainerIndexes.IndexTypeMap.items():
|
for index, type_name in _ContainerIndexes.IndexTypeMap.items():
|
||||||
|
container = None
|
||||||
try:
|
try:
|
||||||
container = new_containers[index]
|
container = new_containers[index]
|
||||||
except IndexError:
|
except IndexError:
|
||||||
container = None
|
pass
|
||||||
|
|
||||||
if type_name == "definition":
|
if type_name == "definition":
|
||||||
if not container or not isinstance(container, DefinitionContainer):
|
if not container or not isinstance(container, DefinitionContainer):
|
||||||
|
|
@ -283,6 +277,16 @@ class CuraContainerStack(ContainerStack):
|
||||||
|
|
||||||
self._containers = new_containers
|
self._containers = new_containers
|
||||||
|
|
||||||
|
# CURA-5281
|
||||||
|
# Some stacks can have empty definition_changes containers which will cause problems.
|
||||||
|
# Make sure that all stacks here have non-empty definition_changes containers.
|
||||||
|
if isinstance(new_containers[_ContainerIndexes.DefinitionChanges], type(self._empty_instance_container)):
|
||||||
|
from cura.Settings.CuraStackBuilder import CuraStackBuilder
|
||||||
|
CuraStackBuilder.createDefinitionChangesContainer(self, self.getId() + "_settings")
|
||||||
|
|
||||||
|
## TODO; Deserialize the containers.
|
||||||
|
return serialized
|
||||||
|
|
||||||
## protected:
|
## protected:
|
||||||
|
|
||||||
# Helper to make sure we emit a PyQt signal on container changes.
|
# Helper to make sure we emit a PyQt signal on container changes.
|
||||||
|
|
@ -303,15 +307,15 @@ class CuraContainerStack(ContainerStack):
|
||||||
#
|
#
|
||||||
# \return The ID of the definition container to use when searching for instance containers.
|
# \return The ID of the definition container to use when searching for instance containers.
|
||||||
@classmethod
|
@classmethod
|
||||||
def _findInstanceContainerDefinitionId(cls, machine_definition: DefinitionContainer) -> str:
|
def _findInstanceContainerDefinitionId(cls, machine_definition: DefinitionContainerInterface) -> str:
|
||||||
quality_definition = machine_definition.getMetaDataEntry("quality_definition")
|
quality_definition = machine_definition.getMetaDataEntry("quality_definition")
|
||||||
if not quality_definition:
|
if not quality_definition:
|
||||||
return machine_definition.id
|
return machine_definition.id #type: ignore
|
||||||
|
|
||||||
definitions = ContainerRegistry.getInstance().findDefinitionContainers(id = quality_definition)
|
definitions = ContainerRegistry.getInstance().findDefinitionContainers(id = quality_definition)
|
||||||
if not definitions:
|
if not definitions:
|
||||||
Logger.log("w", "Unable to find parent definition {parent} for machine {machine}", parent = quality_definition, machine = machine_definition.id)
|
Logger.log("w", "Unable to find parent definition {parent} for machine {machine}", parent = quality_definition, machine = machine_definition.id) #type: ignore
|
||||||
return machine_definition.id
|
return machine_definition.id #type: ignore
|
||||||
|
|
||||||
return cls._findInstanceContainerDefinitionId(definitions[0])
|
return cls._findInstanceContainerDefinitionId(definitions[0])
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,9 +7,8 @@ from UM.ConfigurationErrorMessage import ConfigurationErrorMessage
|
||||||
from UM.Logger import Logger
|
from UM.Logger import Logger
|
||||||
from UM.Settings.Interfaces import DefinitionContainerInterface
|
from UM.Settings.Interfaces import DefinitionContainerInterface
|
||||||
from UM.Settings.InstanceContainer import InstanceContainer
|
from UM.Settings.InstanceContainer import InstanceContainer
|
||||||
from UM.Settings.ContainerRegistry import ContainerRegistry
|
|
||||||
|
|
||||||
from cura.Machines.VariantManager import VariantType
|
from cura.Machines.VariantType import VariantType
|
||||||
from .GlobalStack import GlobalStack
|
from .GlobalStack import GlobalStack
|
||||||
from .ExtruderStack import ExtruderStack
|
from .ExtruderStack import ExtruderStack
|
||||||
|
|
||||||
|
|
@ -29,7 +28,7 @@ class CuraStackBuilder:
|
||||||
variant_manager = application.getVariantManager()
|
variant_manager = application.getVariantManager()
|
||||||
material_manager = application.getMaterialManager()
|
material_manager = application.getMaterialManager()
|
||||||
quality_manager = application.getQualityManager()
|
quality_manager = application.getQualityManager()
|
||||||
registry = ContainerRegistry.getInstance()
|
registry = application.getContainerRegistry()
|
||||||
|
|
||||||
definitions = registry.findDefinitionContainers(id = definition_id)
|
definitions = registry.findDefinitionContainers(id = definition_id)
|
||||||
if not definitions:
|
if not definitions:
|
||||||
|
|
@ -73,12 +72,6 @@ class CuraStackBuilder:
|
||||||
)
|
)
|
||||||
new_global_stack.setName(generated_name)
|
new_global_stack.setName(generated_name)
|
||||||
|
|
||||||
# get material container for extruders
|
|
||||||
material_container = application.empty_material_container
|
|
||||||
material_node = material_manager.getDefaultMaterial(new_global_stack, extruder_variant_name)
|
|
||||||
if material_node and material_node.getContainer():
|
|
||||||
material_container = material_node.getContainer()
|
|
||||||
|
|
||||||
# Create ExtruderStacks
|
# Create ExtruderStacks
|
||||||
extruder_dict = machine_definition.getMetaDataEntry("machine_extruder_trains")
|
extruder_dict = machine_definition.getMetaDataEntry("machine_extruder_trains")
|
||||||
|
|
||||||
|
|
@ -91,6 +84,12 @@ class CuraStackBuilder:
|
||||||
ConfigurationErrorMessage.getInstance().addFaultyContainers(extruder_definition_id)
|
ConfigurationErrorMessage.getInstance().addFaultyContainers(extruder_definition_id)
|
||||||
return None #Don't return any container stack then, not the rest of the extruders either.
|
return None #Don't return any container stack then, not the rest of the extruders either.
|
||||||
|
|
||||||
|
# get material container for extruders
|
||||||
|
material_container = application.empty_material_container
|
||||||
|
material_node = material_manager.getDefaultMaterial(new_global_stack, position, extruder_variant_name, extruder_definition = extruder_definition)
|
||||||
|
if material_node and material_node.getContainer():
|
||||||
|
material_container = material_node.getContainer()
|
||||||
|
|
||||||
new_extruder_id = registry.uniqueName(extruder_definition_id)
|
new_extruder_id = registry.uniqueName(extruder_definition_id)
|
||||||
new_extruder = cls.createExtruderStack(
|
new_extruder = cls.createExtruderStack(
|
||||||
new_extruder_id,
|
new_extruder_id,
|
||||||
|
|
@ -99,8 +98,7 @@ class CuraStackBuilder:
|
||||||
position = position,
|
position = position,
|
||||||
variant_container = extruder_variant_container,
|
variant_container = extruder_variant_container,
|
||||||
material_container = material_container,
|
material_container = material_container,
|
||||||
quality_container = application.empty_quality_container,
|
quality_container = application.empty_quality_container
|
||||||
global_stack = new_global_stack,
|
|
||||||
)
|
)
|
||||||
new_extruder.setNextStack(new_global_stack)
|
new_extruder.setNextStack(new_global_stack)
|
||||||
new_global_stack.addExtruder(new_extruder)
|
new_global_stack.addExtruder(new_extruder)
|
||||||
|
|
@ -110,16 +108,27 @@ class CuraStackBuilder:
|
||||||
|
|
||||||
preferred_quality_type = machine_definition.getMetaDataEntry("preferred_quality_type")
|
preferred_quality_type = machine_definition.getMetaDataEntry("preferred_quality_type")
|
||||||
quality_group_dict = quality_manager.getQualityGroups(new_global_stack)
|
quality_group_dict = quality_manager.getQualityGroups(new_global_stack)
|
||||||
quality_group = quality_group_dict.get(preferred_quality_type)
|
if not quality_group_dict:
|
||||||
|
# There is no available quality group, set all quality containers to empty.
|
||||||
new_global_stack.quality = quality_group.node_for_global.getContainer()
|
|
||||||
if not new_global_stack.quality:
|
|
||||||
new_global_stack.quality = application.empty_quality_container
|
new_global_stack.quality = application.empty_quality_container
|
||||||
for position, extruder_stack in new_global_stack.extruders.items():
|
for extruder_stack in new_global_stack.extruders.values():
|
||||||
if position in quality_group.nodes_for_extruders and quality_group.nodes_for_extruders[position].getContainer():
|
|
||||||
extruder_stack.quality = quality_group.nodes_for_extruders[position].getContainer()
|
|
||||||
else:
|
|
||||||
extruder_stack.quality = application.empty_quality_container
|
extruder_stack.quality = application.empty_quality_container
|
||||||
|
else:
|
||||||
|
# Set the quality containers to the preferred quality type if available, otherwise use the first quality
|
||||||
|
# type that's available.
|
||||||
|
if preferred_quality_type not in quality_group_dict:
|
||||||
|
Logger.log("w", "The preferred quality {quality_type} doesn't exist for this set-up. Choosing a random one.".format(quality_type = preferred_quality_type))
|
||||||
|
preferred_quality_type = next(iter(quality_group_dict))
|
||||||
|
quality_group = quality_group_dict.get(preferred_quality_type)
|
||||||
|
|
||||||
|
new_global_stack.quality = quality_group.node_for_global.getContainer()
|
||||||
|
if not new_global_stack.quality:
|
||||||
|
new_global_stack.quality = application.empty_quality_container
|
||||||
|
for position, extruder_stack in new_global_stack.extruders.items():
|
||||||
|
if position in quality_group.nodes_for_extruders and quality_group.nodes_for_extruders[position].getContainer():
|
||||||
|
extruder_stack.quality = quality_group.nodes_for_extruders[position].getContainer()
|
||||||
|
else:
|
||||||
|
extruder_stack.quality = application.empty_quality_container
|
||||||
|
|
||||||
# Register the global stack after the extruder stacks are created. This prevents the registry from adding another
|
# Register the global stack after the extruder stacks are created. This prevents the registry from adding another
|
||||||
# extruder stack because the global stack didn't have one yet (which is enforced since Cura 3.1).
|
# extruder stack because the global stack didn't have one yet (which is enforced since Cura 3.1).
|
||||||
|
|
@ -139,15 +148,16 @@ class CuraStackBuilder:
|
||||||
@classmethod
|
@classmethod
|
||||||
def createExtruderStack(cls, new_stack_id: str, extruder_definition: DefinitionContainerInterface, machine_definition_id: str,
|
def createExtruderStack(cls, new_stack_id: str, extruder_definition: DefinitionContainerInterface, machine_definition_id: str,
|
||||||
position: int,
|
position: int,
|
||||||
variant_container, material_container, quality_container, global_stack) -> ExtruderStack:
|
variant_container, material_container, quality_container) -> ExtruderStack:
|
||||||
from cura.CuraApplication import CuraApplication
|
from cura.CuraApplication import CuraApplication
|
||||||
application = CuraApplication.getInstance()
|
application = CuraApplication.getInstance()
|
||||||
|
registry = application.getContainerRegistry()
|
||||||
|
|
||||||
stack = ExtruderStack(new_stack_id, parent = global_stack)
|
stack = ExtruderStack(new_stack_id)
|
||||||
stack.setName(extruder_definition.getName())
|
stack.setName(extruder_definition.getName())
|
||||||
stack.setDefinition(extruder_definition)
|
stack.setDefinition(extruder_definition)
|
||||||
|
|
||||||
stack.addMetaDataEntry("position", position)
|
stack.setMetaDataEntry("position", position)
|
||||||
|
|
||||||
user_container = cls.createUserChangesContainer(new_stack_id + "_user", machine_definition_id, new_stack_id,
|
user_container = cls.createUserChangesContainer(new_stack_id + "_user", machine_definition_id, new_stack_id,
|
||||||
is_global_stack = False)
|
is_global_stack = False)
|
||||||
|
|
@ -162,7 +172,7 @@ class CuraStackBuilder:
|
||||||
# Only add the created containers to the registry after we have set all the other
|
# Only add the created containers to the registry after we have set all the other
|
||||||
# properties. This makes the create operation more transactional, since any problems
|
# properties. This makes the create operation more transactional, since any problems
|
||||||
# setting properties will not result in incomplete containers being added.
|
# setting properties will not result in incomplete containers being added.
|
||||||
ContainerRegistry.getInstance().addContainer(user_container)
|
registry.addContainer(user_container)
|
||||||
|
|
||||||
return stack
|
return stack
|
||||||
|
|
||||||
|
|
@ -178,6 +188,7 @@ class CuraStackBuilder:
|
||||||
variant_container, material_container, quality_container) -> GlobalStack:
|
variant_container, material_container, quality_container) -> GlobalStack:
|
||||||
from cura.CuraApplication import CuraApplication
|
from cura.CuraApplication import CuraApplication
|
||||||
application = CuraApplication.getInstance()
|
application = CuraApplication.getInstance()
|
||||||
|
registry = application.getContainerRegistry()
|
||||||
|
|
||||||
stack = GlobalStack(new_stack_id)
|
stack = GlobalStack(new_stack_id)
|
||||||
stack.setDefinition(definition)
|
stack.setDefinition(definition)
|
||||||
|
|
@ -193,7 +204,7 @@ class CuraStackBuilder:
|
||||||
stack.qualityChanges = application.empty_quality_changes_container
|
stack.qualityChanges = application.empty_quality_changes_container
|
||||||
stack.userChanges = user_container
|
stack.userChanges = user_container
|
||||||
|
|
||||||
ContainerRegistry.getInstance().addContainer(user_container)
|
registry.addContainer(user_container)
|
||||||
|
|
||||||
return stack
|
return stack
|
||||||
|
|
||||||
|
|
@ -201,31 +212,35 @@ class CuraStackBuilder:
|
||||||
def createUserChangesContainer(cls, container_name: str, definition_id: str, stack_id: str,
|
def createUserChangesContainer(cls, container_name: str, definition_id: str, stack_id: str,
|
||||||
is_global_stack: bool) -> "InstanceContainer":
|
is_global_stack: bool) -> "InstanceContainer":
|
||||||
from cura.CuraApplication import CuraApplication
|
from cura.CuraApplication import CuraApplication
|
||||||
|
application = CuraApplication.getInstance()
|
||||||
|
registry = application.getContainerRegistry()
|
||||||
|
|
||||||
unique_container_name = ContainerRegistry.getInstance().uniqueName(container_name)
|
unique_container_name = registry.uniqueName(container_name)
|
||||||
|
|
||||||
container = InstanceContainer(unique_container_name)
|
container = InstanceContainer(unique_container_name)
|
||||||
container.setDefinition(definition_id)
|
container.setDefinition(definition_id)
|
||||||
container.addMetaDataEntry("type", "user")
|
container.setMetaDataEntry("type", "user")
|
||||||
container.addMetaDataEntry("setting_version", CuraApplication.SettingVersion)
|
container.setMetaDataEntry("setting_version", CuraApplication.SettingVersion)
|
||||||
|
|
||||||
metadata_key_to_add = "machine" if is_global_stack else "extruder"
|
metadata_key_to_add = "machine" if is_global_stack else "extruder"
|
||||||
container.addMetaDataEntry(metadata_key_to_add, stack_id)
|
container.setMetaDataEntry(metadata_key_to_add, stack_id)
|
||||||
|
|
||||||
return container
|
return container
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def createDefinitionChangesContainer(cls, container_stack, container_name):
|
def createDefinitionChangesContainer(cls, container_stack, container_name):
|
||||||
from cura.CuraApplication import CuraApplication
|
from cura.CuraApplication import CuraApplication
|
||||||
|
application = CuraApplication.getInstance()
|
||||||
|
registry = application.getContainerRegistry()
|
||||||
|
|
||||||
unique_container_name = ContainerRegistry.getInstance().uniqueName(container_name)
|
unique_container_name = registry.uniqueName(container_name)
|
||||||
|
|
||||||
definition_changes_container = InstanceContainer(unique_container_name)
|
definition_changes_container = InstanceContainer(unique_container_name)
|
||||||
definition_changes_container.setDefinition(container_stack.getBottom().getId())
|
definition_changes_container.setDefinition(container_stack.getBottom().getId())
|
||||||
definition_changes_container.addMetaDataEntry("type", "definition_changes")
|
definition_changes_container.setMetaDataEntry("type", "definition_changes")
|
||||||
definition_changes_container.addMetaDataEntry("setting_version", CuraApplication.SettingVersion)
|
definition_changes_container.setMetaDataEntry("setting_version", CuraApplication.SettingVersion)
|
||||||
|
|
||||||
ContainerRegistry.getInstance().addContainer(definition_changes_container)
|
registry.addContainer(definition_changes_container)
|
||||||
container_stack.definitionChanges = definition_changes_container
|
container_stack.definitionChanges = definition_changes_container
|
||||||
|
|
||||||
return definition_changes_container
|
return definition_changes_container
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
# Copyright (c) 2017 Ultimaker B.V.
|
# Copyright (c) 2018 Ultimaker B.V.
|
||||||
# Cura is released under the terms of the LGPLv3 or higher.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
from PyQt5.QtCore import pyqtSignal, pyqtProperty, QObject, QVariant # For communicating data and events to Qt.
|
from PyQt5.QtCore import pyqtSignal, pyqtProperty, QObject, QVariant # For communicating data and events to Qt.
|
||||||
from UM.FlameProfiler import pyqtSlot
|
from UM.FlameProfiler import pyqtSlot
|
||||||
|
|
||||||
from UM.Application import Application # To get the global container stack to find the current machine.
|
import cura.CuraApplication #To get the global container stack to find the current machine.
|
||||||
from UM.Logger import Logger
|
from UM.Logger import Logger
|
||||||
from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
|
from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
|
||||||
from UM.Scene.SceneNode import SceneNode
|
from UM.Scene.SceneNode import SceneNode
|
||||||
|
|
@ -15,7 +15,8 @@ from UM.Settings.SettingFunction import SettingFunction
|
||||||
from UM.Settings.SettingInstance import SettingInstance
|
from UM.Settings.SettingInstance import SettingInstance
|
||||||
from UM.Settings.ContainerStack import ContainerStack
|
from UM.Settings.ContainerStack import ContainerStack
|
||||||
from UM.Settings.PropertyEvaluationContext import PropertyEvaluationContext
|
from UM.Settings.PropertyEvaluationContext import PropertyEvaluationContext
|
||||||
from typing import Optional, List, TYPE_CHECKING, Union
|
|
||||||
|
from typing import Optional, List, TYPE_CHECKING, Union, Dict
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from cura.Settings.ExtruderStack import ExtruderStack
|
from cura.Settings.ExtruderStack import ExtruderStack
|
||||||
|
|
@ -29,16 +30,19 @@ class ExtruderManager(QObject):
|
||||||
|
|
||||||
## Registers listeners and such to listen to changes to the extruders.
|
## Registers listeners and such to listen to changes to the extruders.
|
||||||
def __init__(self, parent = None):
|
def __init__(self, parent = None):
|
||||||
|
if ExtruderManager.__instance is not None:
|
||||||
|
raise RuntimeError("Try to create singleton '%s' more than once" % self.__class__.__name__)
|
||||||
|
ExtruderManager.__instance = self
|
||||||
|
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
|
|
||||||
self._application = Application.getInstance()
|
self._application = cura.CuraApplication.CuraApplication.getInstance()
|
||||||
|
|
||||||
self._extruder_trains = {} # Per machine, a dictionary of extruder container stack IDs. Only for separately defined extruders.
|
self._extruder_trains = {} # Per machine, a dictionary of extruder container stack IDs. Only for separately defined extruders.
|
||||||
self._active_extruder_index = -1 # Indicates the index of the active extruder stack. -1 means no active extruder stack
|
self._active_extruder_index = -1 # Indicates the index of the active extruder stack. -1 means no active extruder stack
|
||||||
self._selected_object_extruders = []
|
self._selected_object_extruders = []
|
||||||
self._addCurrentMachineExtruders()
|
self._addCurrentMachineExtruders()
|
||||||
|
|
||||||
#Application.getInstance().globalContainerStackChanged.connect(self._globalContainerStackChanged)
|
|
||||||
Selection.selectionChanged.connect(self.resetSelectedObjectExtruders)
|
Selection.selectionChanged.connect(self.resetSelectedObjectExtruders)
|
||||||
|
|
||||||
## Signal to notify other components when the list of extruders for a machine definition changes.
|
## Signal to notify other components when the list of extruders for a machine definition changes.
|
||||||
|
|
@ -55,64 +59,47 @@ class ExtruderManager(QObject):
|
||||||
# \return The unique ID of the currently active extruder stack.
|
# \return The unique ID of the currently active extruder stack.
|
||||||
@pyqtProperty(str, notify = activeExtruderChanged)
|
@pyqtProperty(str, notify = activeExtruderChanged)
|
||||||
def activeExtruderStackId(self) -> Optional[str]:
|
def activeExtruderStackId(self) -> Optional[str]:
|
||||||
if not Application.getInstance().getGlobalContainerStack():
|
if not self._application.getGlobalContainerStack():
|
||||||
return None # No active machine, so no active extruder.
|
return None # No active machine, so no active extruder.
|
||||||
try:
|
try:
|
||||||
return self._extruder_trains[Application.getInstance().getGlobalContainerStack().getId()][str(self._active_extruder_index)].getId()
|
return self._extruder_trains[self._application.getGlobalContainerStack().getId()][str(self._active_extruder_index)].getId()
|
||||||
except KeyError: # Extruder index could be -1 if the global tab is selected, or the entry doesn't exist if the machine definition is wrong.
|
except KeyError: # Extruder index could be -1 if the global tab is selected, or the entry doesn't exist if the machine definition is wrong.
|
||||||
return None
|
return None
|
||||||
|
|
||||||
## Return extruder count according to extruder trains.
|
## Return extruder count according to extruder trains.
|
||||||
@pyqtProperty(int, notify = extrudersChanged)
|
@pyqtProperty(int, notify = extrudersChanged)
|
||||||
def extruderCount(self):
|
def extruderCount(self):
|
||||||
if not Application.getInstance().getGlobalContainerStack():
|
if not self._application.getGlobalContainerStack():
|
||||||
return 0 # No active machine, so no extruders.
|
return 0 # No active machine, so no extruders.
|
||||||
try:
|
try:
|
||||||
return len(self._extruder_trains[Application.getInstance().getGlobalContainerStack().getId()])
|
return len(self._extruder_trains[self._application.getGlobalContainerStack().getId()])
|
||||||
except KeyError:
|
except KeyError:
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
## Gets a dict with the extruder stack ids with the extruder number as the key.
|
## Gets a dict with the extruder stack ids with the extruder number as the key.
|
||||||
@pyqtProperty("QVariantMap", notify = extrudersChanged)
|
@pyqtProperty("QVariantMap", notify = extrudersChanged)
|
||||||
def extruderIds(self):
|
def extruderIds(self) -> Dict[str, str]:
|
||||||
extruder_stack_ids = {}
|
extruder_stack_ids = {}
|
||||||
|
|
||||||
global_stack_id = Application.getInstance().getGlobalContainerStack().getId()
|
global_container_stack = self._application.getGlobalContainerStack()
|
||||||
|
if global_container_stack:
|
||||||
|
global_stack_id = global_container_stack.getId()
|
||||||
|
|
||||||
if global_stack_id in self._extruder_trains:
|
if global_stack_id in self._extruder_trains:
|
||||||
for position in self._extruder_trains[global_stack_id]:
|
for position in self._extruder_trains[global_stack_id]:
|
||||||
extruder_stack_ids[position] = self._extruder_trains[global_stack_id][position].getId()
|
extruder_stack_ids[position] = self._extruder_trains[global_stack_id][position].getId()
|
||||||
|
|
||||||
return extruder_stack_ids
|
return extruder_stack_ids
|
||||||
|
|
||||||
@pyqtSlot(str, result = str)
|
@pyqtSlot(str, result = str)
|
||||||
def getQualityChangesIdByExtruderStackId(self, extruder_stack_id: str) -> str:
|
def getQualityChangesIdByExtruderStackId(self, extruder_stack_id: str) -> str:
|
||||||
for position in self._extruder_trains[Application.getInstance().getGlobalContainerStack().getId()]:
|
global_container_stack = self._application.getGlobalContainerStack()
|
||||||
extruder = self._extruder_trains[Application.getInstance().getGlobalContainerStack().getId()][position]
|
if global_container_stack is not None:
|
||||||
if extruder.getId() == extruder_stack_id:
|
for position in self._extruder_trains[global_container_stack.getId()]:
|
||||||
return extruder.qualityChanges.getId()
|
extruder = self._extruder_trains[global_container_stack.getId()][position]
|
||||||
|
if extruder.getId() == extruder_stack_id:
|
||||||
## The instance of the singleton pattern.
|
return extruder.qualityChanges.getId()
|
||||||
#
|
return ""
|
||||||
# It's None if the extruder manager hasn't been created yet.
|
|
||||||
__instance = None
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def createExtruderManager():
|
|
||||||
return ExtruderManager().getInstance()
|
|
||||||
|
|
||||||
## Gets an instance of the extruder manager, or creates one if no instance
|
|
||||||
# exists yet.
|
|
||||||
#
|
|
||||||
# This is an implementation of singleton. If an extruder manager already
|
|
||||||
# exists, it is re-used.
|
|
||||||
#
|
|
||||||
# \return The extruder manager.
|
|
||||||
@classmethod
|
|
||||||
def getInstance(cls) -> "ExtruderManager":
|
|
||||||
if not cls.__instance:
|
|
||||||
cls.__instance = ExtruderManager()
|
|
||||||
return cls.__instance
|
|
||||||
|
|
||||||
## Changes the active extruder by index.
|
## Changes the active extruder by index.
|
||||||
#
|
#
|
||||||
|
|
@ -149,7 +136,7 @@ class ExtruderManager(QObject):
|
||||||
selected_nodes = []
|
selected_nodes = []
|
||||||
for node in Selection.getAllSelectedObjects():
|
for node in Selection.getAllSelectedObjects():
|
||||||
if node.callDecoration("isGroup"):
|
if node.callDecoration("isGroup"):
|
||||||
for grouped_node in BreadthFirstIterator(node):
|
for grouped_node in BreadthFirstIterator(node): #type: ignore #Ignore type error because iter() should get called automatically by Python syntax.
|
||||||
if grouped_node.callDecoration("isGroup"):
|
if grouped_node.callDecoration("isGroup"):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
|
@ -158,7 +145,7 @@ class ExtruderManager(QObject):
|
||||||
selected_nodes.append(node)
|
selected_nodes.append(node)
|
||||||
|
|
||||||
# Then, figure out which nodes are used by those selected nodes.
|
# Then, figure out which nodes are used by those selected nodes.
|
||||||
global_stack = Application.getInstance().getGlobalContainerStack()
|
global_stack = self._application.getGlobalContainerStack()
|
||||||
current_extruder_trains = self._extruder_trains.get(global_stack.getId())
|
current_extruder_trains = self._extruder_trains.get(global_stack.getId())
|
||||||
for node in selected_nodes:
|
for node in selected_nodes:
|
||||||
extruder = node.callDecoration("getActiveExtruder")
|
extruder = node.callDecoration("getActiveExtruder")
|
||||||
|
|
@ -181,7 +168,7 @@ class ExtruderManager(QObject):
|
||||||
|
|
||||||
@pyqtSlot(result = QObject)
|
@pyqtSlot(result = QObject)
|
||||||
def getActiveExtruderStack(self) -> Optional["ExtruderStack"]:
|
def getActiveExtruderStack(self) -> Optional["ExtruderStack"]:
|
||||||
global_container_stack = Application.getInstance().getGlobalContainerStack()
|
global_container_stack = self._application.getGlobalContainerStack()
|
||||||
|
|
||||||
if global_container_stack:
|
if global_container_stack:
|
||||||
if global_container_stack.getId() in self._extruder_trains:
|
if global_container_stack.getId() in self._extruder_trains:
|
||||||
|
|
@ -192,7 +179,7 @@ class ExtruderManager(QObject):
|
||||||
|
|
||||||
## Get an extruder stack by index
|
## Get an extruder stack by index
|
||||||
def getExtruderStack(self, index) -> Optional["ExtruderStack"]:
|
def getExtruderStack(self, index) -> Optional["ExtruderStack"]:
|
||||||
global_container_stack = Application.getInstance().getGlobalContainerStack()
|
global_container_stack = self._application.getGlobalContainerStack()
|
||||||
if global_container_stack:
|
if global_container_stack:
|
||||||
if global_container_stack.getId() in self._extruder_trains:
|
if global_container_stack.getId() in self._extruder_trains:
|
||||||
if str(index) in self._extruder_trains[global_container_stack.getId()]:
|
if str(index) in self._extruder_trains[global_container_stack.getId()]:
|
||||||
|
|
@ -203,7 +190,9 @@ class ExtruderManager(QObject):
|
||||||
def getExtruderStacks(self) -> List["ExtruderStack"]:
|
def getExtruderStacks(self) -> List["ExtruderStack"]:
|
||||||
result = []
|
result = []
|
||||||
for i in range(self.extruderCount):
|
for i in range(self.extruderCount):
|
||||||
result.append(self.getExtruderStack(i))
|
stack = self.getExtruderStack(i)
|
||||||
|
if stack:
|
||||||
|
result.append(stack)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def registerExtruder(self, extruder_train, machine_id):
|
def registerExtruder(self, extruder_train, machine_id):
|
||||||
|
|
@ -269,14 +258,14 @@ class ExtruderManager(QObject):
|
||||||
support_bottom_enabled = False
|
support_bottom_enabled = False
|
||||||
support_roof_enabled = False
|
support_roof_enabled = False
|
||||||
|
|
||||||
scene_root = Application.getInstance().getController().getScene().getRoot()
|
scene_root = self._application.getController().getScene().getRoot()
|
||||||
|
|
||||||
# If no extruders are registered in the extruder manager yet, return an empty array
|
# If no extruders are registered in the extruder manager yet, return an empty array
|
||||||
if len(self.extruderIds) == 0:
|
if len(self.extruderIds) == 0:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
# Get the extruders of all printable meshes in the scene
|
# Get the extruders of all printable meshes in the scene
|
||||||
meshes = [node for node in DepthFirstIterator(scene_root) if isinstance(node, SceneNode) and node.isSelectable()]
|
meshes = [node for node in DepthFirstIterator(scene_root) if isinstance(node, SceneNode) and node.isSelectable()] #type: ignore #Ignore type error because iter() should get called automatically by Python syntax.
|
||||||
for mesh in meshes:
|
for mesh in meshes:
|
||||||
extruder_stack_id = mesh.callDecoration("getActiveExtruder")
|
extruder_stack_id = mesh.callDecoration("getActiveExtruder")
|
||||||
if not extruder_stack_id:
|
if not extruder_stack_id:
|
||||||
|
|
@ -318,10 +307,10 @@ class ExtruderManager(QObject):
|
||||||
|
|
||||||
# The platform adhesion extruder. Not used if using none.
|
# The platform adhesion extruder. Not used if using none.
|
||||||
if global_stack.getProperty("adhesion_type", "value") != "none":
|
if global_stack.getProperty("adhesion_type", "value") != "none":
|
||||||
extruder_nr = str(global_stack.getProperty("adhesion_extruder_nr", "value"))
|
extruder_str_nr = str(global_stack.getProperty("adhesion_extruder_nr", "value"))
|
||||||
if extruder_nr == "-1":
|
if extruder_str_nr == "-1":
|
||||||
extruder_nr = Application.getInstance().getMachineManager().defaultExtruderPosition
|
extruder_str_nr = self._application.getMachineManager().defaultExtruderPosition
|
||||||
used_extruder_stack_ids.add(self.extruderIds[extruder_nr])
|
used_extruder_stack_ids.add(self.extruderIds[extruder_str_nr])
|
||||||
|
|
||||||
try:
|
try:
|
||||||
return [container_registry.findContainerStacks(id = stack_id)[0] for stack_id in used_extruder_stack_ids]
|
return [container_registry.findContainerStacks(id = stack_id)[0] for stack_id in used_extruder_stack_ids]
|
||||||
|
|
@ -352,7 +341,7 @@ class ExtruderManager(QObject):
|
||||||
# The first element is the global container stack, followed by any extruder stacks.
|
# The first element is the global container stack, followed by any extruder stacks.
|
||||||
# \return \type{List[ContainerStack]}
|
# \return \type{List[ContainerStack]}
|
||||||
def getActiveGlobalAndExtruderStacks(self) -> Optional[List[Union["ExtruderStack", "GlobalStack"]]]:
|
def getActiveGlobalAndExtruderStacks(self) -> Optional[List[Union["ExtruderStack", "GlobalStack"]]]:
|
||||||
global_stack = Application.getInstance().getGlobalContainerStack()
|
global_stack = self._application.getGlobalContainerStack()
|
||||||
if not global_stack:
|
if not global_stack:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
@ -364,7 +353,7 @@ class ExtruderManager(QObject):
|
||||||
#
|
#
|
||||||
# \return \type{List[ContainerStack]} a list of
|
# \return \type{List[ContainerStack]} a list of
|
||||||
def getActiveExtruderStacks(self) -> List["ExtruderStack"]:
|
def getActiveExtruderStacks(self) -> List["ExtruderStack"]:
|
||||||
global_stack = Application.getInstance().getGlobalContainerStack()
|
global_stack = self._application.getGlobalContainerStack()
|
||||||
if not global_stack:
|
if not global_stack:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
|
@ -402,84 +391,30 @@ class ExtruderManager(QObject):
|
||||||
|
|
||||||
# Register the extruder trains by position
|
# Register the extruder trains by position
|
||||||
for extruder_train in extruder_trains:
|
for extruder_train in extruder_trains:
|
||||||
self._extruder_trains[global_stack_id][extruder_train.getMetaDataEntry("position")] = extruder_train
|
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. ???
|
# regardless of what the next stack is, we have to set it again, because of signal routing. ???
|
||||||
extruder_train.setParent(global_stack)
|
extruder_train.setParent(global_stack)
|
||||||
extruder_train.setNextStack(global_stack)
|
extruder_train.setNextStack(global_stack)
|
||||||
extruders_changed = True
|
extruders_changed = True
|
||||||
|
|
||||||
self._fixMaterialDiameterAndNozzleSize(global_stack, extruder_trains)
|
self._fixSingleExtrusionMachineExtruderDefinition(global_stack)
|
||||||
if extruders_changed:
|
if extruders_changed:
|
||||||
self.extrudersChanged.emit(global_stack_id)
|
self.extrudersChanged.emit(global_stack_id)
|
||||||
self.setActiveExtruderIndex(0)
|
self.setActiveExtruderIndex(0)
|
||||||
|
|
||||||
#
|
# After 3.4, all single-extrusion machines have their own extruder definition files instead of reusing
|
||||||
# This function tries to fix the problem with per-extruder-settable nozzle size and material diameter problems
|
# "fdmextruder". We need to check a machine here so its extruder definition is correct according to this.
|
||||||
# in early versions (3.0 - 3.2.1).
|
def _fixSingleExtrusionMachineExtruderDefinition(self, global_stack):
|
||||||
#
|
expected_extruder_definition_0_id = global_stack.getMetaDataEntry("machine_extruder_trains")["0"]
|
||||||
# In earlier versions, "nozzle size" and "material diameter" are only applicable to the complete machine, so all
|
extruder_stack_0 = global_stack.extruders["0"]
|
||||||
# extruders share the same values. In this case, "nozzle size" and "material diameter" are saved in the
|
if extruder_stack_0.definition.getId() != expected_extruder_definition_0_id:
|
||||||
# GlobalStack's DefinitionChanges container.
|
Logger.log("e", "Single extruder printer [{printer}] expected extruder [{expected}], but got [{got}]. I'm making it [{expected}].".format(
|
||||||
#
|
printer = global_stack.getId(), expected = expected_extruder_definition_0_id, got = extruder_stack_0.definition.getId()))
|
||||||
# Later, we could have different "nozzle size" for each extruder, but "material diameter" could only be set for
|
container_registry = ContainerRegistry.getInstance()
|
||||||
# the entire machine. In this case, "nozzle size" should be saved in each ExtruderStack's DefinitionChanges, but
|
extruder_definition = container_registry.findDefinitionContainers(id = expected_extruder_definition_0_id)[0]
|
||||||
# "material diameter" still remains in the GlobalStack's DefinitionChanges.
|
extruder_stack_0.definition = extruder_definition
|
||||||
#
|
|
||||||
# Lateer, both "nozzle size" and "material diameter" are settable per-extruder, and both settings should be saved
|
|
||||||
# in the ExtruderStack's DefinitionChanges.
|
|
||||||
#
|
|
||||||
# There were some bugs in upgrade so the data weren't saved correct as described above. This function tries fix
|
|
||||||
# this.
|
|
||||||
#
|
|
||||||
# One more thing is about material diameter and single-extrusion machines. Most single-extrusion machines don't
|
|
||||||
# specifically define their extruder definition, so they reuse "fdmextruder", but for those machines, they may
|
|
||||||
# define "material diameter = 1.75" in their machine definition, but in "fdmextruder", it's still "2.85". This
|
|
||||||
# causes a problem with incorrect default values.
|
|
||||||
#
|
|
||||||
# This is also fixed here in this way: If no "material diameter" is specified, it will look for the default value
|
|
||||||
# in both the Extruder's definition and the Global's definition. If 2 values don't match, we will use the value
|
|
||||||
# from the Global definition by setting it in the Extruder's DefinitionChanges container.
|
|
||||||
#
|
|
||||||
def _fixMaterialDiameterAndNozzleSize(self, global_stack, extruder_stack_list):
|
|
||||||
keys_to_copy = ["material_diameter", "machine_nozzle_size"] # these will be copied over to all extruders
|
|
||||||
|
|
||||||
extruder_positions_to_update = set()
|
|
||||||
for extruder_stack in extruder_stack_list:
|
|
||||||
for key in keys_to_copy:
|
|
||||||
# Only copy the value when this extruder doesn't have the value.
|
|
||||||
if extruder_stack.definitionChanges.hasProperty(key, "value"):
|
|
||||||
continue
|
|
||||||
|
|
||||||
setting_value_in_global_def_changes = global_stack.definitionChanges.getProperty(key, "value")
|
|
||||||
setting_value_in_global_def = global_stack.definition.getProperty(key, "value")
|
|
||||||
setting_value = setting_value_in_global_def
|
|
||||||
if setting_value_in_global_def_changes is not None:
|
|
||||||
setting_value = setting_value_in_global_def_changes
|
|
||||||
if setting_value == extruder_stack.definition.getProperty(key, "value"):
|
|
||||||
continue
|
|
||||||
|
|
||||||
setting_definition = global_stack.getSettingDefinition(key)
|
|
||||||
new_instance = SettingInstance(setting_definition, extruder_stack.definitionChanges)
|
|
||||||
new_instance.setProperty("value", setting_value)
|
|
||||||
new_instance.resetState() # Ensure that the state is not seen as a user state.
|
|
||||||
extruder_stack.definitionChanges.addInstance(new_instance)
|
|
||||||
extruder_stack.definitionChanges.setDirty(True)
|
|
||||||
|
|
||||||
# Make sure the material diameter is up to date for the extruder stack.
|
|
||||||
if key == "material_diameter":
|
|
||||||
position = int(extruder_stack.getMetaDataEntry("position"))
|
|
||||||
extruder_positions_to_update.add(position)
|
|
||||||
|
|
||||||
# We have to remove those settings here because we know that those values have been copied to all
|
|
||||||
# the extruders at this point.
|
|
||||||
for key in keys_to_copy:
|
|
||||||
if global_stack.definitionChanges.hasProperty(key, "value"):
|
|
||||||
global_stack.definitionChanges.removeInstance(key, postpone_emit = True)
|
|
||||||
|
|
||||||
# Update material diameter for extruders
|
|
||||||
for position in extruder_positions_to_update:
|
|
||||||
self.updateMaterialForDiameter(position, global_stack = global_stack)
|
|
||||||
|
|
||||||
## Get all extruder values for a certain setting.
|
## Get all extruder values for a certain setting.
|
||||||
#
|
#
|
||||||
|
|
@ -491,7 +426,7 @@ class ExtruderManager(QObject):
|
||||||
# If no extruder has the value, the list will contain the global value.
|
# If no extruder has the value, the list will contain the global value.
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def getExtruderValues(key):
|
def getExtruderValues(key):
|
||||||
global_stack = Application.getInstance().getGlobalContainerStack()
|
global_stack = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack()
|
||||||
|
|
||||||
result = []
|
result = []
|
||||||
for extruder in ExtruderManager.getInstance().getMachineExtruders(global_stack.getId()):
|
for extruder in ExtruderManager.getInstance().getMachineExtruders(global_stack.getId()):
|
||||||
|
|
@ -526,7 +461,7 @@ class ExtruderManager(QObject):
|
||||||
# If no extruder has the value, the list will contain the global value.
|
# If no extruder has the value, the list will contain the global value.
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def getDefaultExtruderValues(key):
|
def getDefaultExtruderValues(key):
|
||||||
global_stack = Application.getInstance().getGlobalContainerStack()
|
global_stack = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack()
|
||||||
context = PropertyEvaluationContext(global_stack)
|
context = PropertyEvaluationContext(global_stack)
|
||||||
context.context["evaluate_from_container_index"] = 1 # skip the user settings container
|
context.context["evaluate_from_container_index"] = 1 # skip the user settings container
|
||||||
context.context["override_operators"] = {
|
context.context["override_operators"] = {
|
||||||
|
|
@ -556,6 +491,11 @@ class ExtruderManager(QObject):
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
## Return the default extruder position from the machine manager
|
||||||
|
@staticmethod
|
||||||
|
def getDefaultExtruderPosition() -> str:
|
||||||
|
return cura.CuraApplication.CuraApplication.getInstance().getMachineManager().defaultExtruderPosition
|
||||||
|
|
||||||
## Get all extruder values for a certain setting.
|
## Get all extruder values for a certain setting.
|
||||||
#
|
#
|
||||||
# This is exposed to qml for display purposes
|
# This is exposed to qml for display purposes
|
||||||
|
|
@ -567,96 +507,6 @@ class ExtruderManager(QObject):
|
||||||
def getInstanceExtruderValues(self, key):
|
def getInstanceExtruderValues(self, key):
|
||||||
return ExtruderManager.getExtruderValues(key)
|
return ExtruderManager.getExtruderValues(key)
|
||||||
|
|
||||||
## Updates the material container to a material that matches the material diameter set for the printer
|
|
||||||
def updateMaterialForDiameter(self, extruder_position: int, global_stack = None):
|
|
||||||
if not global_stack:
|
|
||||||
global_stack = Application.getInstance().getGlobalContainerStack()
|
|
||||||
if not global_stack:
|
|
||||||
return
|
|
||||||
|
|
||||||
if not global_stack.getMetaDataEntry("has_materials", False):
|
|
||||||
return
|
|
||||||
|
|
||||||
extruder_stack = global_stack.extruders[str(extruder_position)]
|
|
||||||
|
|
||||||
material_diameter = extruder_stack.material.getProperty("material_diameter", "value")
|
|
||||||
if not material_diameter:
|
|
||||||
# in case of "empty" material
|
|
||||||
material_diameter = 0
|
|
||||||
|
|
||||||
material_approximate_diameter = str(round(material_diameter))
|
|
||||||
material_diameter = extruder_stack.definitionChanges.getProperty("material_diameter", "value")
|
|
||||||
setting_provider = extruder_stack
|
|
||||||
if not material_diameter:
|
|
||||||
if extruder_stack.definition.hasProperty("material_diameter", "value"):
|
|
||||||
material_diameter = extruder_stack.definition.getProperty("material_diameter", "value")
|
|
||||||
else:
|
|
||||||
material_diameter = global_stack.definition.getProperty("material_diameter", "value")
|
|
||||||
setting_provider = global_stack
|
|
||||||
|
|
||||||
if isinstance(material_diameter, SettingFunction):
|
|
||||||
material_diameter = material_diameter(setting_provider)
|
|
||||||
|
|
||||||
machine_approximate_diameter = str(round(material_diameter))
|
|
||||||
|
|
||||||
if material_approximate_diameter != machine_approximate_diameter:
|
|
||||||
Logger.log("i", "The the currently active material(s) do not match the diameter set for the printer. Finding alternatives.")
|
|
||||||
|
|
||||||
if global_stack.getMetaDataEntry("has_machine_materials", False):
|
|
||||||
materials_definition = global_stack.definition.getId()
|
|
||||||
has_material_variants = global_stack.getMetaDataEntry("has_variants", False)
|
|
||||||
else:
|
|
||||||
materials_definition = "fdmprinter"
|
|
||||||
has_material_variants = False
|
|
||||||
|
|
||||||
old_material = extruder_stack.material
|
|
||||||
search_criteria = {
|
|
||||||
"type": "material",
|
|
||||||
"approximate_diameter": machine_approximate_diameter,
|
|
||||||
"material": old_material.getMetaDataEntry("material", "value"),
|
|
||||||
"brand": old_material.getMetaDataEntry("brand", "value"),
|
|
||||||
"supplier": old_material.getMetaDataEntry("supplier", "value"),
|
|
||||||
"color_name": old_material.getMetaDataEntry("color_name", "value"),
|
|
||||||
"definition": materials_definition
|
|
||||||
}
|
|
||||||
if has_material_variants:
|
|
||||||
search_criteria["variant"] = extruder_stack.variant.getId()
|
|
||||||
|
|
||||||
container_registry = Application.getInstance().getContainerRegistry()
|
|
||||||
empty_material = container_registry.findInstanceContainers(id = "empty_material")[0]
|
|
||||||
|
|
||||||
if old_material == empty_material:
|
|
||||||
search_criteria.pop("material", None)
|
|
||||||
search_criteria.pop("supplier", None)
|
|
||||||
search_criteria.pop("brand", None)
|
|
||||||
search_criteria.pop("definition", None)
|
|
||||||
search_criteria["id"] = extruder_stack.getMetaDataEntry("preferred_material")
|
|
||||||
|
|
||||||
materials = container_registry.findInstanceContainers(**search_criteria)
|
|
||||||
if not materials:
|
|
||||||
# Same material with new diameter is not found, search for generic version of the same material type
|
|
||||||
search_criteria.pop("supplier", None)
|
|
||||||
search_criteria.pop("brand", None)
|
|
||||||
search_criteria["color_name"] = "Generic"
|
|
||||||
materials = container_registry.findInstanceContainers(**search_criteria)
|
|
||||||
if not materials:
|
|
||||||
# Generic material with new diameter is not found, search for preferred material
|
|
||||||
search_criteria.pop("color_name", None)
|
|
||||||
search_criteria.pop("material", None)
|
|
||||||
search_criteria["id"] = extruder_stack.getMetaDataEntry("preferred_material")
|
|
||||||
materials = container_registry.findInstanceContainers(**search_criteria)
|
|
||||||
if not materials:
|
|
||||||
# Preferred material with new diameter is not found, search for any material
|
|
||||||
search_criteria.pop("id", None)
|
|
||||||
materials = container_registry.findInstanceContainers(**search_criteria)
|
|
||||||
if not materials:
|
|
||||||
# Just use empty material as a final fallback
|
|
||||||
materials = [empty_material]
|
|
||||||
|
|
||||||
Logger.log("i", "Selecting new material: %s", materials[0].getId())
|
|
||||||
|
|
||||||
extruder_stack.material = materials[0]
|
|
||||||
|
|
||||||
## Get the value for a setting from a specific extruder.
|
## Get the value for a setting from a specific extruder.
|
||||||
#
|
#
|
||||||
# This is exposed to SettingFunction to use in value functions.
|
# This is exposed to SettingFunction to use in value functions.
|
||||||
|
|
@ -669,7 +519,7 @@ class ExtruderManager(QObject):
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def getExtruderValue(extruder_index, key):
|
def getExtruderValue(extruder_index, key):
|
||||||
if extruder_index == -1:
|
if extruder_index == -1:
|
||||||
extruder_index = int(Application.getInstance().getMachineManager().defaultExtruderPosition)
|
extruder_index = int(cura.CuraApplication.CuraApplication.getInstance().getMachineManager().defaultExtruderPosition)
|
||||||
extruder = ExtruderManager.getInstance().getExtruderStack(extruder_index)
|
extruder = ExtruderManager.getInstance().getExtruderStack(extruder_index)
|
||||||
|
|
||||||
if extruder:
|
if extruder:
|
||||||
|
|
@ -678,7 +528,7 @@ class ExtruderManager(QObject):
|
||||||
value = value(extruder)
|
value = value(extruder)
|
||||||
else:
|
else:
|
||||||
# Just a value from global.
|
# Just a value from global.
|
||||||
value = Application.getInstance().getGlobalContainerStack().getProperty(key, "value")
|
value = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack().getProperty(key, "value")
|
||||||
|
|
||||||
return value
|
return value
|
||||||
|
|
||||||
|
|
@ -707,7 +557,7 @@ class ExtruderManager(QObject):
|
||||||
if isinstance(value, SettingFunction):
|
if isinstance(value, SettingFunction):
|
||||||
value = value(extruder, context = context)
|
value = value(extruder, context = context)
|
||||||
else: # Just a value from global.
|
else: # Just a value from global.
|
||||||
value = Application.getInstance().getGlobalContainerStack().getProperty(key, "value", context = context)
|
value = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack().getProperty(key, "value", context = context)
|
||||||
|
|
||||||
return value
|
return value
|
||||||
|
|
||||||
|
|
@ -720,7 +570,7 @@ class ExtruderManager(QObject):
|
||||||
# \return The effective value
|
# \return The effective value
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def getResolveOrValue(key):
|
def getResolveOrValue(key):
|
||||||
global_stack = Application.getInstance().getGlobalContainerStack()
|
global_stack = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack()
|
||||||
resolved_value = global_stack.getProperty(key, "value")
|
resolved_value = global_stack.getProperty(key, "value")
|
||||||
|
|
||||||
return resolved_value
|
return resolved_value
|
||||||
|
|
@ -734,7 +584,7 @@ class ExtruderManager(QObject):
|
||||||
# \return The effective value
|
# \return The effective value
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def getDefaultResolveOrValue(key):
|
def getDefaultResolveOrValue(key):
|
||||||
global_stack = Application.getInstance().getGlobalContainerStack()
|
global_stack = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack()
|
||||||
context = PropertyEvaluationContext(global_stack)
|
context = PropertyEvaluationContext(global_stack)
|
||||||
context.context["evaluate_from_container_index"] = 1 # skip the user settings container
|
context.context["evaluate_from_container_index"] = 1 # skip the user settings container
|
||||||
context.context["override_operators"] = {
|
context.context["override_operators"] = {
|
||||||
|
|
@ -746,3 +596,9 @@ class ExtruderManager(QObject):
|
||||||
resolved_value = global_stack.getProperty(key, "value", context = context)
|
resolved_value = global_stack.getProperty(key, "value", context = context)
|
||||||
|
|
||||||
return resolved_value
|
return resolved_value
|
||||||
|
|
||||||
|
__instance = None # type: ExtruderManager
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def getInstance(cls, *args, **kwargs) -> "ExtruderManager":
|
||||||
|
return cls.__instance
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,10 @@
|
||||||
# Copyright (c) 2017 Ultimaker B.V.
|
# Copyright (c) 2018 Ultimaker B.V.
|
||||||
# Cura is released under the terms of the LGPLv3 or higher.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
from typing import Any, TYPE_CHECKING, Optional
|
from typing import Any, Dict, TYPE_CHECKING, Optional
|
||||||
|
|
||||||
from PyQt5.QtCore import pyqtProperty, pyqtSignal
|
from PyQt5.QtCore import pyqtProperty, pyqtSignal
|
||||||
|
|
||||||
from UM.Application import Application
|
|
||||||
from UM.Decorators import override
|
from UM.Decorators import override
|
||||||
from UM.MimeTypeDatabase import MimeType, MimeTypeDatabase
|
from UM.MimeTypeDatabase import MimeType, MimeTypeDatabase
|
||||||
from UM.Settings.ContainerStack import ContainerStack
|
from UM.Settings.ContainerStack import ContainerStack
|
||||||
|
|
@ -13,6 +12,8 @@ from UM.Settings.ContainerRegistry import ContainerRegistry
|
||||||
from UM.Settings.Interfaces import ContainerInterface, PropertyEvaluationContext
|
from UM.Settings.Interfaces import ContainerInterface, PropertyEvaluationContext
|
||||||
from UM.Util import parseBool
|
from UM.Util import parseBool
|
||||||
|
|
||||||
|
import cura.CuraApplication
|
||||||
|
|
||||||
from . import Exceptions
|
from . import Exceptions
|
||||||
from .CuraContainerStack import CuraContainerStack, _ContainerIndexes
|
from .CuraContainerStack import CuraContainerStack, _ContainerIndexes
|
||||||
from .ExtruderManager import ExtruderManager
|
from .ExtruderManager import ExtruderManager
|
||||||
|
|
@ -25,10 +26,10 @@ if TYPE_CHECKING:
|
||||||
#
|
#
|
||||||
#
|
#
|
||||||
class ExtruderStack(CuraContainerStack):
|
class ExtruderStack(CuraContainerStack):
|
||||||
def __init__(self, container_id: str, *args, **kwargs):
|
def __init__(self, container_id: str) -> None:
|
||||||
super().__init__(container_id, *args, **kwargs)
|
super().__init__(container_id)
|
||||||
|
|
||||||
self.addMetaDataEntry("type", "extruder_train") # For backward compatibility
|
self.setMetaDataEntry("type", "extruder_train") # For backward compatibility
|
||||||
|
|
||||||
self.propertiesChanged.connect(self._onPropertiesChanged)
|
self.propertiesChanged.connect(self._onPropertiesChanged)
|
||||||
|
|
||||||
|
|
@ -38,10 +39,10 @@ class ExtruderStack(CuraContainerStack):
|
||||||
#
|
#
|
||||||
# This will set the next stack and ensure that we register this stack as an extruder.
|
# This will set the next stack and ensure that we register this stack as an extruder.
|
||||||
@override(ContainerStack)
|
@override(ContainerStack)
|
||||||
def setNextStack(self, stack: CuraContainerStack) -> None:
|
def setNextStack(self, stack: CuraContainerStack, connect_signals: bool = True) -> None:
|
||||||
super().setNextStack(stack)
|
super().setNextStack(stack)
|
||||||
stack.addExtruder(self)
|
stack.addExtruder(self)
|
||||||
self.addMetaDataEntry("machine", stack.id)
|
self.setMetaDataEntry("machine", stack.id)
|
||||||
|
|
||||||
# For backward compatibility: Register the extruder with the Extruder Manager
|
# For backward compatibility: Register the extruder with the Extruder Manager
|
||||||
ExtruderManager.getInstance().registerExtruder(self, stack.id)
|
ExtruderManager.getInstance().registerExtruder(self, stack.id)
|
||||||
|
|
@ -50,14 +51,14 @@ class ExtruderStack(CuraContainerStack):
|
||||||
def getNextStack(self) -> Optional["GlobalStack"]:
|
def getNextStack(self) -> Optional["GlobalStack"]:
|
||||||
return super().getNextStack()
|
return super().getNextStack()
|
||||||
|
|
||||||
def setEnabled(self, enabled):
|
def setEnabled(self, enabled: bool) -> None:
|
||||||
if "enabled" not in self._metadata:
|
if "enabled" not in self._metadata:
|
||||||
self.addMetaDataEntry("enabled", "True")
|
self.setMetaDataEntry("enabled", "True")
|
||||||
self.setMetaDataEntry("enabled", str(enabled))
|
self.setMetaDataEntry("enabled", str(enabled))
|
||||||
self.enabledChanged.emit()
|
self.enabledChanged.emit()
|
||||||
|
|
||||||
@pyqtProperty(bool, notify = enabledChanged)
|
@pyqtProperty(bool, notify = enabledChanged)
|
||||||
def isEnabled(self):
|
def isEnabled(self) -> bool:
|
||||||
return parseBool(self.getMetaDataEntry("enabled", "True"))
|
return parseBool(self.getMetaDataEntry("enabled", "True"))
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
|
@ -113,7 +114,7 @@ class ExtruderStack(CuraContainerStack):
|
||||||
limit_to_extruder = super().getProperty(key, "limit_to_extruder", context)
|
limit_to_extruder = super().getProperty(key, "limit_to_extruder", context)
|
||||||
if limit_to_extruder is not None:
|
if limit_to_extruder is not None:
|
||||||
if limit_to_extruder == -1:
|
if limit_to_extruder == -1:
|
||||||
limit_to_extruder = int(Application.getInstance().getMachineManager().defaultExtruderPosition)
|
limit_to_extruder = int(cura.CuraApplication.CuraApplication.getInstance().getMachineManager().defaultExtruderPosition)
|
||||||
limit_to_extruder = str(limit_to_extruder)
|
limit_to_extruder = str(limit_to_extruder)
|
||||||
if (limit_to_extruder is not None and limit_to_extruder != "-1") and self.getMetaDataEntry("position") != str(limit_to_extruder):
|
if (limit_to_extruder is not None and limit_to_extruder != "-1") and self.getMetaDataEntry("position") != str(limit_to_extruder):
|
||||||
if str(limit_to_extruder) in self.getNextStack().extruders:
|
if str(limit_to_extruder) in self.getNextStack().extruders:
|
||||||
|
|
@ -137,12 +138,9 @@ class ExtruderStack(CuraContainerStack):
|
||||||
def deserialize(self, contents: str, file_name: Optional[str] = None) -> None:
|
def deserialize(self, contents: str, file_name: Optional[str] = None) -> None:
|
||||||
super().deserialize(contents, file_name)
|
super().deserialize(contents, file_name)
|
||||||
if "enabled" not in self.getMetaData():
|
if "enabled" not in self.getMetaData():
|
||||||
self.addMetaDataEntry("enabled", "True")
|
self.setMetaDataEntry("enabled", "True")
|
||||||
stacks = ContainerRegistry.getInstance().findContainerStacks(id=self.getMetaDataEntry("machine", ""))
|
|
||||||
if stacks:
|
|
||||||
self.setNextStack(stacks[0])
|
|
||||||
|
|
||||||
def _onPropertiesChanged(self, key, properties):
|
def _onPropertiesChanged(self, key: str, properties: Dict[str, Any]) -> None:
|
||||||
# When there is a setting that is not settable per extruder that depends on a value from a setting that is,
|
# When there is a setting that is not settable per extruder that depends on a value from a setting that is,
|
||||||
# we do not always get properly informed that we should re-evaluate the setting. So make sure to indicate
|
# we do not always get properly informed that we should re-evaluate the setting. So make sure to indicate
|
||||||
# something changed for those settings.
|
# something changed for those settings.
|
||||||
|
|
|
||||||
|
|
@ -1,32 +1,33 @@
|
||||||
# Copyright (c) 2017 Ultimaker B.V.
|
# Copyright (c) 2018 Ultimaker B.V.
|
||||||
# Cura is released under the terms of the LGPLv3 or higher.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
import threading
|
import threading
|
||||||
from typing import Any, Dict, Optional
|
from typing import Any, Dict, Optional, Set, TYPE_CHECKING
|
||||||
|
|
||||||
from PyQt5.QtCore import pyqtProperty
|
from PyQt5.QtCore import pyqtProperty
|
||||||
|
|
||||||
from UM.Application import Application
|
|
||||||
from UM.Decorators import override
|
from UM.Decorators import override
|
||||||
|
|
||||||
from UM.MimeTypeDatabase import MimeType, MimeTypeDatabase
|
from UM.MimeTypeDatabase import MimeType, MimeTypeDatabase
|
||||||
from UM.Settings.ContainerStack import ContainerStack
|
from UM.Settings.ContainerStack import ContainerStack
|
||||||
from UM.Settings.SettingInstance import InstanceState
|
from UM.Settings.SettingInstance import InstanceState
|
||||||
from UM.Settings.ContainerRegistry import ContainerRegistry
|
from UM.Settings.ContainerRegistry import ContainerRegistry
|
||||||
from UM.Settings.Interfaces import PropertyEvaluationContext
|
from UM.Settings.Interfaces import PropertyEvaluationContext
|
||||||
from UM.Logger import Logger
|
from UM.Logger import Logger
|
||||||
|
import cura.CuraApplication
|
||||||
|
|
||||||
from . import Exceptions
|
from . import Exceptions
|
||||||
from .CuraContainerStack import CuraContainerStack
|
from .CuraContainerStack import CuraContainerStack
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from cura.Settings.ExtruderStack import ExtruderStack
|
||||||
|
|
||||||
## Represents the Global or Machine stack and its related containers.
|
## Represents the Global or Machine stack and its related containers.
|
||||||
#
|
#
|
||||||
class GlobalStack(CuraContainerStack):
|
class GlobalStack(CuraContainerStack):
|
||||||
def __init__(self, container_id: str, *args, **kwargs):
|
def __init__(self, container_id: str) -> None:
|
||||||
super().__init__(container_id, *args, **kwargs)
|
super().__init__(container_id)
|
||||||
|
|
||||||
self.addMetaDataEntry("type", "machine") # For backward compatibility
|
self.setMetaDataEntry("type", "machine") # For backward compatibility
|
||||||
|
|
||||||
self._extruders = {} # type: Dict[str, "ExtruderStack"]
|
self._extruders = {} # type: Dict[str, "ExtruderStack"]
|
||||||
|
|
||||||
|
|
@ -34,7 +35,7 @@ class GlobalStack(CuraContainerStack):
|
||||||
# and if so, to bypass the resolve to prevent an infinite recursion that would occur
|
# and if so, to bypass the resolve to prevent an infinite recursion that would occur
|
||||||
# if the resolve function tried to access the same property it is a resolve for.
|
# if the resolve function tried to access the same property it is a resolve for.
|
||||||
# Per thread we have our own resolving_settings, or strange things sometimes occur.
|
# Per thread we have our own resolving_settings, or strange things sometimes occur.
|
||||||
self._resolving_settings = defaultdict(set) # keys are thread names
|
self._resolving_settings = defaultdict(set) #type: Dict[str, Set[str]] # keys are thread names
|
||||||
|
|
||||||
## Get the list of extruders of this stack.
|
## Get the list of extruders of this stack.
|
||||||
#
|
#
|
||||||
|
|
@ -54,6 +55,12 @@ class GlobalStack(CuraContainerStack):
|
||||||
return "machine_stack"
|
return "machine_stack"
|
||||||
return configuration_type
|
return configuration_type
|
||||||
|
|
||||||
|
def getBuildplateName(self) -> Optional[str]:
|
||||||
|
name = None
|
||||||
|
if self.variant.getId() != "empty_variant":
|
||||||
|
name = self.variant.getName()
|
||||||
|
return name
|
||||||
|
|
||||||
## Add an extruder to the list of extruders of this stack.
|
## Add an extruder to the list of extruders of this stack.
|
||||||
#
|
#
|
||||||
# \param extruder The extruder to add.
|
# \param extruder The extruder to add.
|
||||||
|
|
@ -94,6 +101,10 @@ class GlobalStack(CuraContainerStack):
|
||||||
context.pushContainer(self)
|
context.pushContainer(self)
|
||||||
|
|
||||||
# Handle the "resolve" property.
|
# Handle the "resolve" property.
|
||||||
|
#TODO: Why the hell does this involve threading?
|
||||||
|
# Answer: Because if multiple threads start resolving properties that have the same underlying properties that's
|
||||||
|
# related, without taking a note of which thread a resolve paths belongs to, they can bump into each other and
|
||||||
|
# generate unexpected behaviours.
|
||||||
if self._shouldResolve(key, property_name, context):
|
if self._shouldResolve(key, property_name, context):
|
||||||
current_thread = threading.current_thread()
|
current_thread = threading.current_thread()
|
||||||
self._resolving_settings[current_thread.name].add(key)
|
self._resolving_settings[current_thread.name].add(key)
|
||||||
|
|
@ -106,7 +117,7 @@ class GlobalStack(CuraContainerStack):
|
||||||
limit_to_extruder = super().getProperty(key, "limit_to_extruder", context)
|
limit_to_extruder = super().getProperty(key, "limit_to_extruder", context)
|
||||||
if limit_to_extruder is not None:
|
if limit_to_extruder is not None:
|
||||||
if limit_to_extruder == -1:
|
if limit_to_extruder == -1:
|
||||||
limit_to_extruder = int(Application.getInstance().getMachineManager().defaultExtruderPosition)
|
limit_to_extruder = int(cura.CuraApplication.CuraApplication.getInstance().getMachineManager().defaultExtruderPosition)
|
||||||
limit_to_extruder = str(limit_to_extruder)
|
limit_to_extruder = str(limit_to_extruder)
|
||||||
if limit_to_extruder is not None and limit_to_extruder != "-1" and limit_to_extruder in self._extruders:
|
if limit_to_extruder is not None and limit_to_extruder != "-1" and limit_to_extruder in self._extruders:
|
||||||
if super().getProperty(key, "settable_per_extruder", context):
|
if super().getProperty(key, "settable_per_extruder", context):
|
||||||
|
|
@ -125,7 +136,7 @@ class GlobalStack(CuraContainerStack):
|
||||||
#
|
#
|
||||||
# This will simply raise an exception since the Global stack cannot have a next stack.
|
# This will simply raise an exception since the Global stack cannot have a next stack.
|
||||||
@override(ContainerStack)
|
@override(ContainerStack)
|
||||||
def setNextStack(self, next_stack: ContainerStack) -> None:
|
def setNextStack(self, stack: CuraContainerStack, connect_signals: bool = True) -> None:
|
||||||
raise Exceptions.InvalidOperationError("Global stack cannot have a next stack!")
|
raise Exceptions.InvalidOperationError("Global stack cannot have a next stack!")
|
||||||
|
|
||||||
# protected:
|
# protected:
|
||||||
|
|
@ -153,6 +164,26 @@ class GlobalStack(CuraContainerStack):
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
## Perform some sanity checks on the global stack
|
||||||
|
# Sanity check for extruders; they must have positions 0 and up to machine_extruder_count - 1
|
||||||
|
def isValid(self) -> bool:
|
||||||
|
container_registry = ContainerRegistry.getInstance()
|
||||||
|
extruder_trains = container_registry.findContainerStacks(type = "extruder_train", machine = self.getId())
|
||||||
|
|
||||||
|
machine_extruder_count = self.getProperty("machine_extruder_count", "value")
|
||||||
|
extruder_check_position = set()
|
||||||
|
for extruder_train in extruder_trains:
|
||||||
|
extruder_position = extruder_train.getMetaDataEntry("position")
|
||||||
|
extruder_check_position.add(extruder_position)
|
||||||
|
|
||||||
|
for check_position in range(machine_extruder_count):
|
||||||
|
if str(check_position) not in extruder_check_position:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
def getHeadAndFansCoordinates(self):
|
||||||
|
return self.getProperty("machine_head_with_fans_polygon", "value")
|
||||||
|
|
||||||
|
|
||||||
## private:
|
## private:
|
||||||
global_stack_mime = MimeType(
|
global_stack_mime = MimeType(
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,3 +1,6 @@
|
||||||
|
# Copyright (c) 2018 Ultimaker B.V.
|
||||||
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
from typing import Any, Optional
|
from typing import Any, Optional
|
||||||
|
|
||||||
from UM.Application import Application
|
from UM.Application import Application
|
||||||
|
|
@ -9,7 +12,6 @@ from .CuraContainerStack import CuraContainerStack
|
||||||
|
|
||||||
|
|
||||||
class PerObjectContainerStack(CuraContainerStack):
|
class PerObjectContainerStack(CuraContainerStack):
|
||||||
|
|
||||||
@override(CuraContainerStack)
|
@override(CuraContainerStack)
|
||||||
def getProperty(self, key: str, property_name: str, context: Optional[PropertyEvaluationContext] = None) -> Any:
|
def getProperty(self, key: str, property_name: str, context: Optional[PropertyEvaluationContext] = None) -> Any:
|
||||||
if context is None:
|
if context is None:
|
||||||
|
|
@ -17,10 +19,12 @@ class PerObjectContainerStack(CuraContainerStack):
|
||||||
context.pushContainer(self)
|
context.pushContainer(self)
|
||||||
|
|
||||||
global_stack = Application.getInstance().getGlobalContainerStack()
|
global_stack = Application.getInstance().getGlobalContainerStack()
|
||||||
|
if not global_stack:
|
||||||
|
return None
|
||||||
|
|
||||||
# Return the user defined value if present, otherwise, evaluate the value according to the default routine.
|
# Return the user defined value if present, otherwise, evaluate the value according to the default routine.
|
||||||
if self.getContainer(0).hasProperty(key, property_name):
|
if self.getContainer(0).hasProperty(key, property_name):
|
||||||
if self.getContainer(0)._instances[key].state == InstanceState.User:
|
if self.getContainer(0).getProperty(key, "state") == InstanceState.User:
|
||||||
result = super().getProperty(key, property_name, context)
|
result = super().getProperty(key, property_name, context)
|
||||||
context.popContainer()
|
context.popContainer()
|
||||||
return result
|
return result
|
||||||
|
|
@ -53,13 +57,13 @@ class PerObjectContainerStack(CuraContainerStack):
|
||||||
return result
|
return result
|
||||||
|
|
||||||
@override(CuraContainerStack)
|
@override(CuraContainerStack)
|
||||||
def setNextStack(self, stack: CuraContainerStack):
|
def setNextStack(self, stack: CuraContainerStack) -> None:
|
||||||
super().setNextStack(stack)
|
super().setNextStack(stack)
|
||||||
|
|
||||||
# trigger signal to re-evaluate all default settings
|
# trigger signal to re-evaluate all default settings
|
||||||
for key, instance in self.getContainer(0)._instances.items():
|
for key in self.getContainer(0).getAllKeys():
|
||||||
# only evaluate default settings
|
# only evaluate default settings
|
||||||
if instance.state != InstanceState.Default:
|
if self.getContainer(0).getProperty(key, "state") != InstanceState.Default:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
self._collectPropertyChanges(key, "value")
|
self._collectPropertyChanges(key, "value")
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
# Copyright (c) 2017 Ultimaker B.V.
|
# Copyright (c) 2017 Ultimaker B.V.
|
||||||
# Cura is released under the terms of the LGPLv3 or higher.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
from typing import List
|
||||||
|
|
||||||
from PyQt5.QtCore import QObject, QTimer, pyqtProperty, pyqtSignal
|
from PyQt5.QtCore import QObject, QTimer, pyqtProperty, pyqtSignal
|
||||||
from UM.FlameProfiler import pyqtSlot
|
from UM.FlameProfiler import pyqtSlot
|
||||||
|
|
@ -13,6 +14,7 @@ from UM.Logger import Logger
|
||||||
# speed settings. If all the children of print_speed have a single value override, changing the speed won't
|
# speed settings. If all the children of print_speed have a single value override, changing the speed won't
|
||||||
# actually do anything, as only the 'leaf' settings are used by the engine.
|
# actually do anything, as only the 'leaf' settings are used by the engine.
|
||||||
from UM.Settings.ContainerStack import ContainerStack
|
from UM.Settings.ContainerStack import ContainerStack
|
||||||
|
from UM.Settings.Interfaces import ContainerInterface
|
||||||
from UM.Settings.SettingFunction import SettingFunction
|
from UM.Settings.SettingFunction import SettingFunction
|
||||||
from UM.Settings.SettingInstance import InstanceState
|
from UM.Settings.SettingInstance import InstanceState
|
||||||
|
|
||||||
|
|
@ -82,8 +84,9 @@ class SettingInheritanceManager(QObject):
|
||||||
|
|
||||||
def _onActiveExtruderChanged(self):
|
def _onActiveExtruderChanged(self):
|
||||||
new_active_stack = ExtruderManager.getInstance().getActiveExtruderStack()
|
new_active_stack = ExtruderManager.getInstance().getActiveExtruderStack()
|
||||||
# if not new_active_stack:
|
if not new_active_stack:
|
||||||
# new_active_stack = self._global_container_stack
|
self._active_container_stack = None
|
||||||
|
return
|
||||||
|
|
||||||
if new_active_stack != self._active_container_stack: # Check if changed
|
if new_active_stack != self._active_container_stack: # Check if changed
|
||||||
if self._active_container_stack: # Disconnect signal from old container (if any)
|
if self._active_container_stack: # Disconnect signal from old container (if any)
|
||||||
|
|
@ -154,7 +157,9 @@ class SettingInheritanceManager(QObject):
|
||||||
has_setting_function = False
|
has_setting_function = False
|
||||||
if not stack:
|
if not stack:
|
||||||
stack = self._active_container_stack
|
stack = self._active_container_stack
|
||||||
containers = []
|
if not stack: #No active container stack yet!
|
||||||
|
return False
|
||||||
|
containers = [] # type: List[ContainerInterface]
|
||||||
|
|
||||||
## Check if the setting has a user state. If not, it is never overwritten.
|
## Check if the setting has a user state. If not, it is never overwritten.
|
||||||
has_user_state = stack.getProperty(key, "state") == InstanceState.User
|
has_user_state = stack.getProperty(key, "state") == InstanceState.User
|
||||||
|
|
@ -166,7 +171,8 @@ class SettingInheritanceManager(QObject):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
## Also check if the top container is not a setting function (this happens if the inheritance is restored).
|
## Also check if the top container is not a setting function (this happens if the inheritance is restored).
|
||||||
if isinstance(stack.getTop().getProperty(key, "value"), SettingFunction):
|
user_container = stack.getTop()
|
||||||
|
if user_container and isinstance(user_container.getProperty(key, "value"), SettingFunction):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
## Mash all containers for all the stacks together.
|
## Mash all containers for all the stacks together.
|
||||||
|
|
|
||||||
|
|
@ -30,17 +30,19 @@ class SettingOverrideDecorator(SceneNodeDecorator):
|
||||||
# Note that Support Mesh is not in here because it actually generates
|
# Note that Support Mesh is not in here because it actually generates
|
||||||
# g-code in the volume of the mesh.
|
# g-code in the volume of the mesh.
|
||||||
_non_printing_mesh_settings = {"anti_overhang_mesh", "infill_mesh", "cutting_mesh"}
|
_non_printing_mesh_settings = {"anti_overhang_mesh", "infill_mesh", "cutting_mesh"}
|
||||||
|
_non_thumbnail_visible_settings = {"anti_overhang_mesh", "infill_mesh", "cutting_mesh", "support_mesh"}
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self._stack = PerObjectContainerStack(container_id = "per_object_stack_" + str(id(self)))
|
self._stack = PerObjectContainerStack(container_id = "per_object_stack_" + str(id(self)))
|
||||||
self._stack.setDirty(False) # This stack does not need to be saved.
|
self._stack.setDirty(False) # This stack does not need to be saved.
|
||||||
user_container = InstanceContainer(container_id = self._generateUniqueName())
|
user_container = InstanceContainer(container_id = self._generateUniqueName())
|
||||||
user_container.addMetaDataEntry("type", "user")
|
user_container.setMetaDataEntry("type", "user")
|
||||||
self._stack.userChanges = user_container
|
self._stack.userChanges = user_container
|
||||||
self._extruder_stack = ExtruderManager.getInstance().getExtruderStack(0).getId()
|
self._extruder_stack = ExtruderManager.getInstance().getExtruderStack(0).getId()
|
||||||
|
|
||||||
self._is_non_printing_mesh = False
|
self._is_non_printing_mesh = False
|
||||||
|
self._is_non_thumbnail_visible_mesh = False
|
||||||
|
|
||||||
self._stack.propertyChanged.connect(self._onSettingChanged)
|
self._stack.propertyChanged.connect(self._onSettingChanged)
|
||||||
|
|
||||||
|
|
@ -61,7 +63,7 @@ class SettingOverrideDecorator(SceneNodeDecorator):
|
||||||
instance_container = copy.deepcopy(self._stack.getContainer(0), memo)
|
instance_container = copy.deepcopy(self._stack.getContainer(0), memo)
|
||||||
|
|
||||||
# A unique name must be added, or replaceContainer will not replace it
|
# A unique name must be added, or replaceContainer will not replace it
|
||||||
instance_container.setMetaDataEntry("id", self._generateUniqueName)
|
instance_container.setMetaDataEntry("id", self._generateUniqueName())
|
||||||
|
|
||||||
## Set the copied instance as the first (and only) instance container of the stack.
|
## Set the copied instance as the first (and only) instance container of the stack.
|
||||||
deep_copy._stack.replaceContainer(0, instance_container)
|
deep_copy._stack.replaceContainer(0, instance_container)
|
||||||
|
|
@ -72,6 +74,7 @@ class SettingOverrideDecorator(SceneNodeDecorator):
|
||||||
# use value from the stack because there can be a delay in signal triggering and "_is_non_printing_mesh"
|
# use value from the stack because there can be a delay in signal triggering and "_is_non_printing_mesh"
|
||||||
# has not been updated yet.
|
# has not been updated yet.
|
||||||
deep_copy._is_non_printing_mesh = self.evaluateIsNonPrintingMesh()
|
deep_copy._is_non_printing_mesh = self.evaluateIsNonPrintingMesh()
|
||||||
|
deep_copy._is_non_thumbnail_visible_mesh = self.evaluateIsNonThumbnailVisibleMesh()
|
||||||
|
|
||||||
return deep_copy
|
return deep_copy
|
||||||
|
|
||||||
|
|
@ -102,10 +105,17 @@ class SettingOverrideDecorator(SceneNodeDecorator):
|
||||||
def evaluateIsNonPrintingMesh(self):
|
def evaluateIsNonPrintingMesh(self):
|
||||||
return any(bool(self._stack.getProperty(setting, "value")) for setting in self._non_printing_mesh_settings)
|
return any(bool(self._stack.getProperty(setting, "value")) for setting in self._non_printing_mesh_settings)
|
||||||
|
|
||||||
|
def isNonThumbnailVisibleMesh(self):
|
||||||
|
return self._is_non_thumbnail_visible_mesh
|
||||||
|
|
||||||
|
def evaluateIsNonThumbnailVisibleMesh(self):
|
||||||
|
return any(bool(self._stack.getProperty(setting, "value")) for setting in self._non_thumbnail_visible_settings)
|
||||||
|
|
||||||
def _onSettingChanged(self, instance, property_name): # Reminder: 'property' is a built-in function
|
def _onSettingChanged(self, instance, property_name): # Reminder: 'property' is a built-in function
|
||||||
if property_name == "value":
|
if property_name == "value":
|
||||||
# Trigger slice/need slicing if the value has changed.
|
# Trigger slice/need slicing if the value has changed.
|
||||||
self._is_non_printing_mesh = self.evaluateIsNonPrintingMesh()
|
self._is_non_printing_mesh = self.evaluateIsNonPrintingMesh()
|
||||||
|
self._is_non_thumbnail_visible_mesh = self.evaluateIsNonThumbnailVisibleMesh()
|
||||||
|
|
||||||
Application.getInstance().getBackend().needsSlicing()
|
Application.getInstance().getBackend().needsSlicing()
|
||||||
Application.getInstance().getBackend().tickle()
|
Application.getInstance().getBackend().tickle()
|
||||||
|
|
|
||||||
41
cura/Settings/SidebarCustomMenuItemsModel.py
Normal file
41
cura/Settings/SidebarCustomMenuItemsModel.py
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
# Copyright (c) 2018 Ultimaker B.V.
|
||||||
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from UM.Qt.ListModel import ListModel
|
||||||
|
from PyQt5.QtCore import pyqtSlot, Qt
|
||||||
|
|
||||||
|
|
||||||
|
class SidebarCustomMenuItemsModel(ListModel):
|
||||||
|
name_role = Qt.UserRole + 1
|
||||||
|
actions_role = Qt.UserRole + 2
|
||||||
|
menu_item_role = Qt.UserRole + 3
|
||||||
|
menu_item_icon_name_role = Qt.UserRole + 5
|
||||||
|
|
||||||
|
def __init__(self, parent=None):
|
||||||
|
super().__init__(parent)
|
||||||
|
self.addRoleName(self.name_role, "name")
|
||||||
|
self.addRoleName(self.actions_role, "actions")
|
||||||
|
self.addRoleName(self.menu_item_role, "menu_item")
|
||||||
|
self.addRoleName(self.menu_item_icon_name_role, "iconName")
|
||||||
|
self._updateExtensionList()
|
||||||
|
|
||||||
|
def _updateExtensionList(self)-> None:
|
||||||
|
from cura.CuraApplication import CuraApplication
|
||||||
|
for menu_item in CuraApplication.getInstance().getSidebarCustomMenuItems():
|
||||||
|
|
||||||
|
self.appendItem({
|
||||||
|
"name": menu_item["name"],
|
||||||
|
"icon_name": menu_item["icon_name"],
|
||||||
|
"actions": menu_item["actions"],
|
||||||
|
"menu_item": menu_item["menu_item"]
|
||||||
|
})
|
||||||
|
|
||||||
|
@pyqtSlot(str, "QVariantList", "QVariantMap")
|
||||||
|
def callMenuItemMethod(self, menu_item_name: str, menu_item_actions: list, kwargs: Any) -> None:
|
||||||
|
for item in self._items:
|
||||||
|
if menu_item_name == item["name"]:
|
||||||
|
for method in menu_item_actions:
|
||||||
|
getattr(item["menu_item"], method)(kwargs)
|
||||||
|
break
|
||||||
|
|
@ -39,12 +39,12 @@ class SimpleModeSettingsManager(QObject):
|
||||||
global_stack = self._machine_manager.activeMachine
|
global_stack = self._machine_manager.activeMachine
|
||||||
|
|
||||||
# check user settings in the global stack
|
# check user settings in the global stack
|
||||||
user_setting_keys.update(set(global_stack.userChanges.getAllKeys()))
|
user_setting_keys.update(global_stack.userChanges.getAllKeys())
|
||||||
|
|
||||||
# check user settings in the extruder stacks
|
# check user settings in the extruder stacks
|
||||||
if global_stack.extruders:
|
if global_stack.extruders:
|
||||||
for extruder_stack in global_stack.extruders.values():
|
for extruder_stack in global_stack.extruders.values():
|
||||||
user_setting_keys.update(set(extruder_stack.userChanges.getAllKeys()))
|
user_setting_keys.update(extruder_stack.userChanges.getAllKeys())
|
||||||
|
|
||||||
# remove settings that are visible in recommended (we don't show the reset button for those)
|
# remove settings that are visible in recommended (we don't show the reset button for those)
|
||||||
for skip_key in self.__ignored_custom_setting_keys:
|
for skip_key in self.__ignored_custom_setting_keys:
|
||||||
|
|
@ -70,12 +70,12 @@ class SimpleModeSettingsManager(QObject):
|
||||||
global_stack = self._machine_manager.activeMachine
|
global_stack = self._machine_manager.activeMachine
|
||||||
|
|
||||||
# check quality changes settings in the global stack
|
# check quality changes settings in the global stack
|
||||||
quality_changes_keys.update(set(global_stack.qualityChanges.getAllKeys()))
|
quality_changes_keys.update(global_stack.qualityChanges.getAllKeys())
|
||||||
|
|
||||||
# check quality changes settings in the extruder stacks
|
# check quality changes settings in the extruder stacks
|
||||||
if global_stack.extruders:
|
if global_stack.extruders:
|
||||||
for extruder_stack in global_stack.extruders.values():
|
for extruder_stack in global_stack.extruders.values():
|
||||||
quality_changes_keys.update(set(extruder_stack.qualityChanges.getAllKeys()))
|
quality_changes_keys.update(extruder_stack.qualityChanges.getAllKeys())
|
||||||
|
|
||||||
# check if the qualityChanges container is not empty (meaning it is a user created profile)
|
# check if the qualityChanges container is not empty (meaning it is a user created profile)
|
||||||
has_quality_changes = len(quality_changes_keys) > 0
|
has_quality_changes = len(quality_changes_keys) > 0
|
||||||
|
|
|
||||||
56
cura/Settings/cura_empty_instance_containers.py
Normal file
56
cura/Settings/cura_empty_instance_containers.py
Normal file
|
|
@ -0,0 +1,56 @@
|
||||||
|
# Copyright (c) 2018 Ultimaker B.V.
|
||||||
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
|
import copy
|
||||||
|
|
||||||
|
from UM.Settings.constant_instance_containers import EMPTY_CONTAINER_ID, empty_container
|
||||||
|
|
||||||
|
|
||||||
|
# Empty definition changes
|
||||||
|
EMPTY_DEFINITION_CHANGES_CONTAINER_ID = "empty_definition_changes"
|
||||||
|
empty_definition_changes_container = copy.deepcopy(empty_container)
|
||||||
|
empty_definition_changes_container.setMetaDataEntry("id", EMPTY_DEFINITION_CHANGES_CONTAINER_ID)
|
||||||
|
empty_definition_changes_container.setMetaDataEntry("type", "definition_changes")
|
||||||
|
|
||||||
|
# Empty variant
|
||||||
|
EMPTY_VARIANT_CONTAINER_ID = "empty_variant"
|
||||||
|
empty_variant_container = copy.deepcopy(empty_container)
|
||||||
|
empty_variant_container.setMetaDataEntry("id", EMPTY_VARIANT_CONTAINER_ID)
|
||||||
|
empty_variant_container.setMetaDataEntry("type", "variant")
|
||||||
|
|
||||||
|
# Empty material
|
||||||
|
EMPTY_MATERIAL_CONTAINER_ID = "empty_material"
|
||||||
|
empty_material_container = copy.deepcopy(empty_container)
|
||||||
|
empty_material_container.setMetaDataEntry("id", EMPTY_MATERIAL_CONTAINER_ID)
|
||||||
|
empty_material_container.setMetaDataEntry("type", "material")
|
||||||
|
|
||||||
|
# Empty quality
|
||||||
|
EMPTY_QUALITY_CONTAINER_ID = "empty_quality"
|
||||||
|
empty_quality_container = copy.deepcopy(empty_container)
|
||||||
|
empty_quality_container.setMetaDataEntry("id", EMPTY_QUALITY_CONTAINER_ID)
|
||||||
|
empty_quality_container.setName("Not Supported")
|
||||||
|
empty_quality_container.setMetaDataEntry("quality_type", "not_supported")
|
||||||
|
empty_quality_container.setMetaDataEntry("type", "quality")
|
||||||
|
empty_quality_container.setMetaDataEntry("supported", False)
|
||||||
|
|
||||||
|
# Empty quality changes
|
||||||
|
EMPTY_QUALITY_CHANGES_CONTAINER_ID = "empty_quality_changes"
|
||||||
|
empty_quality_changes_container = copy.deepcopy(empty_container)
|
||||||
|
empty_quality_changes_container.setMetaDataEntry("id", EMPTY_QUALITY_CHANGES_CONTAINER_ID)
|
||||||
|
empty_quality_changes_container.setMetaDataEntry("type", "quality_changes")
|
||||||
|
empty_quality_changes_container.setMetaDataEntry("quality_type", "not_supported")
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = ["EMPTY_CONTAINER_ID",
|
||||||
|
"empty_container", # For convenience
|
||||||
|
"EMPTY_DEFINITION_CHANGES_CONTAINER_ID",
|
||||||
|
"empty_definition_changes_container",
|
||||||
|
"EMPTY_VARIANT_CONTAINER_ID",
|
||||||
|
"empty_variant_container",
|
||||||
|
"EMPTY_MATERIAL_CONTAINER_ID",
|
||||||
|
"empty_material_container",
|
||||||
|
"EMPTY_QUALITY_CHANGES_CONTAINER_ID",
|
||||||
|
"empty_quality_changes_container",
|
||||||
|
"EMPTY_QUALITY_CONTAINER_ID",
|
||||||
|
"empty_quality_container"
|
||||||
|
]
|
||||||
110
cura/SingleInstance.py
Normal file
110
cura/SingleInstance.py
Normal file
|
|
@ -0,0 +1,110 @@
|
||||||
|
# Copyright (c) 2018 Ultimaker B.V.
|
||||||
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
from typing import List, Optional
|
||||||
|
|
||||||
|
from PyQt5.QtNetwork import QLocalServer, QLocalSocket
|
||||||
|
|
||||||
|
from UM.Qt.QtApplication import QtApplication #For typing.
|
||||||
|
from UM.Logger import Logger
|
||||||
|
|
||||||
|
|
||||||
|
class SingleInstance:
|
||||||
|
def __init__(self, application: QtApplication, files_to_open: Optional[List[str]]) -> None:
|
||||||
|
self._application = application
|
||||||
|
self._files_to_open = files_to_open
|
||||||
|
|
||||||
|
self._single_instance_server = None
|
||||||
|
|
||||||
|
# Starts a client that checks for a single instance server and sends the files that need to opened if the server
|
||||||
|
# exists. Returns True if the single instance server is found, otherwise False.
|
||||||
|
def startClient(self) -> bool:
|
||||||
|
Logger.log("i", "Checking for the presence of an ready running Cura instance.")
|
||||||
|
single_instance_socket = QLocalSocket(self._application)
|
||||||
|
Logger.log("d", "Full single instance server name: %s", single_instance_socket.fullServerName())
|
||||||
|
single_instance_socket.connectToServer("ultimaker-cura")
|
||||||
|
single_instance_socket.waitForConnected(msecs = 3000) # wait for 3 seconds
|
||||||
|
|
||||||
|
if single_instance_socket.state() != QLocalSocket.ConnectedState:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# We only send the files that need to be opened.
|
||||||
|
if not self._files_to_open:
|
||||||
|
Logger.log("i", "No file need to be opened, do nothing.")
|
||||||
|
return True
|
||||||
|
|
||||||
|
if single_instance_socket.state() == QLocalSocket.ConnectedState:
|
||||||
|
Logger.log("i", "Connection has been made to the single-instance Cura socket.")
|
||||||
|
|
||||||
|
# Protocol is one line of JSON terminated with a carriage return.
|
||||||
|
# "command" field is required and holds the name of the command to execute.
|
||||||
|
# Other fields depend on the command.
|
||||||
|
|
||||||
|
payload = {"command": "clear-all"}
|
||||||
|
single_instance_socket.write(bytes(json.dumps(payload) + "\n", encoding = "ascii"))
|
||||||
|
|
||||||
|
payload = {"command": "focus"}
|
||||||
|
single_instance_socket.write(bytes(json.dumps(payload) + "\n", encoding = "ascii"))
|
||||||
|
|
||||||
|
for filename in self._files_to_open:
|
||||||
|
payload = {"command": "open", "filePath": os.path.abspath(filename)}
|
||||||
|
single_instance_socket.write(bytes(json.dumps(payload) + "\n", encoding = "ascii"))
|
||||||
|
|
||||||
|
payload = {"command": "close-connection"}
|
||||||
|
single_instance_socket.write(bytes(json.dumps(payload) + "\n", encoding = "ascii"))
|
||||||
|
|
||||||
|
single_instance_socket.flush()
|
||||||
|
single_instance_socket.waitForDisconnected()
|
||||||
|
return True
|
||||||
|
|
||||||
|
def startServer(self) -> None:
|
||||||
|
self._single_instance_server = QLocalServer()
|
||||||
|
if self._single_instance_server:
|
||||||
|
self._single_instance_server.newConnection.connect(self._onClientConnected)
|
||||||
|
self._single_instance_server.listen("ultimaker-cura")
|
||||||
|
else:
|
||||||
|
Logger.log("e", "Single instance server was not created.")
|
||||||
|
|
||||||
|
def _onClientConnected(self) -> None:
|
||||||
|
Logger.log("i", "New connection recevied on our single-instance server")
|
||||||
|
connection = None #type: Optional[QLocalSocket]
|
||||||
|
if self._single_instance_server:
|
||||||
|
connection = self._single_instance_server.nextPendingConnection()
|
||||||
|
|
||||||
|
if connection is not None:
|
||||||
|
connection.readyRead.connect(lambda c = connection: self.__readCommands(c))
|
||||||
|
|
||||||
|
def __readCommands(self, connection: QLocalSocket) -> None:
|
||||||
|
line = connection.readLine()
|
||||||
|
while len(line) != 0: # There is also a .canReadLine()
|
||||||
|
try:
|
||||||
|
payload = json.loads(str(line, encoding = "ascii").strip())
|
||||||
|
command = payload["command"]
|
||||||
|
|
||||||
|
# Command: Remove all models from the build plate.
|
||||||
|
if command == "clear-all":
|
||||||
|
self._application.callLater(lambda: self._application.deleteAll())
|
||||||
|
|
||||||
|
# Command: Load a model file
|
||||||
|
elif command == "open":
|
||||||
|
self._application.callLater(lambda f = payload["filePath"]: self._application._openFile(f))
|
||||||
|
|
||||||
|
# Command: Activate the window and bring it to the top.
|
||||||
|
elif command == "focus":
|
||||||
|
# Operating systems these days prevent windows from moving around by themselves.
|
||||||
|
# 'alert' or flashing the icon in the taskbar is the best thing we do now.
|
||||||
|
main_window = self._application.getMainWindow()
|
||||||
|
if main_window is not None:
|
||||||
|
self._application.callLater(lambda: main_window.alert(0)) # type: ignore # I don't know why MyPy complains here
|
||||||
|
|
||||||
|
# Command: Close the socket connection. We're done.
|
||||||
|
elif command == "close-connection":
|
||||||
|
connection.close()
|
||||||
|
|
||||||
|
else:
|
||||||
|
Logger.log("w", "Received an unrecognized command " + str(command))
|
||||||
|
except json.decoder.JSONDecodeError as ex:
|
||||||
|
Logger.log("w", "Unable to parse JSON command '%s': %s", line, repr(ex))
|
||||||
|
line = connection.readLine()
|
||||||
|
|
@ -6,13 +6,10 @@ from PyQt5 import QtCore
|
||||||
from PyQt5.QtGui import QImage
|
from PyQt5.QtGui import QImage
|
||||||
|
|
||||||
from cura.PreviewPass import PreviewPass
|
from cura.PreviewPass import PreviewPass
|
||||||
from cura.Scene import ConvexHullNode
|
|
||||||
|
|
||||||
from UM.Application import Application
|
from UM.Application import Application
|
||||||
from UM.Math.AxisAlignedBox import AxisAlignedBox
|
|
||||||
from UM.Math.Matrix import Matrix
|
from UM.Math.Matrix import Matrix
|
||||||
from UM.Math.Vector import Vector
|
from UM.Math.Vector import Vector
|
||||||
from UM.Mesh.MeshData import transformVertices
|
|
||||||
from UM.Scene.Camera import Camera
|
from UM.Scene.Camera import Camera
|
||||||
from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
|
from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
|
||||||
|
|
||||||
|
|
@ -51,7 +48,7 @@ class Snapshot:
|
||||||
# determine zoom and look at
|
# determine zoom and look at
|
||||||
bbox = None
|
bbox = None
|
||||||
for node in DepthFirstIterator(root):
|
for node in DepthFirstIterator(root):
|
||||||
if node.callDecoration("isSliceable") and node.getMeshData() and node.isVisible():
|
if node.callDecoration("isSliceable") and node.getMeshData() and node.isVisible() and not node.callDecoration("isNonThumbnailVisibleMesh"):
|
||||||
if bbox is None:
|
if bbox is None:
|
||||||
bbox = node.getBoundingBox()
|
bbox = node.getBoundingBox()
|
||||||
else:
|
else:
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,10 @@
|
||||||
# Copyright (c) 2017 Ultimaker B.V.
|
# Copyright (c) 2017 Ultimaker B.V.
|
||||||
# Cura is released under the terms of the LGPLv3 or higher.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
from PyQt5.QtCore import pyqtProperty, QUrl, QObject
|
from PyQt5.QtCore import pyqtProperty, QUrl
|
||||||
|
|
||||||
from UM.Stage import Stage
|
from UM.Stage import Stage
|
||||||
|
|
||||||
|
|
||||||
class CuraStage(Stage):
|
class CuraStage(Stage):
|
||||||
|
|
||||||
def __init__(self, parent = None):
|
def __init__(self, parent = None):
|
||||||
|
|
|
||||||
69
cura/TaskManagement/OnExitCallbackManager.py
Normal file
69
cura/TaskManagement/OnExitCallbackManager.py
Normal file
|
|
@ -0,0 +1,69 @@
|
||||||
|
# Copyright (c) 2018 Ultimaker B.V.
|
||||||
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
|
from typing import TYPE_CHECKING, Callable, List
|
||||||
|
|
||||||
|
from UM.Logger import Logger
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from cura.CuraApplication import CuraApplication
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# This class manages a all registered upon-exit checks that need to be perform when the application tries to exit.
|
||||||
|
# For example, to show a confirmation dialog when there is USB printing in progress, etc. All callbacks will be called
|
||||||
|
# in the order of when they got registered. If all callbacks "passes", that is, for example, if the user clicks "yes"
|
||||||
|
# on the exit confirmation dialog or nothing that's blocking the exit, then the application will quit after that.
|
||||||
|
#
|
||||||
|
class OnExitCallbackManager:
|
||||||
|
|
||||||
|
def __init__(self, application: "CuraApplication") -> None:
|
||||||
|
self._application = application
|
||||||
|
self._on_exit_callback_list = list() # type: List[Callable]
|
||||||
|
self._current_callback_idx = 0
|
||||||
|
self._is_all_checks_passed = False
|
||||||
|
|
||||||
|
def addCallback(self, callback: Callable) -> None:
|
||||||
|
self._on_exit_callback_list.append(callback)
|
||||||
|
Logger.log("d", "on-app-exit callback [%s] added.", callback)
|
||||||
|
|
||||||
|
# Reset the current state so the next time it will call all the callbacks again.
|
||||||
|
def resetCurrentState(self) -> None:
|
||||||
|
self._current_callback_idx = 0
|
||||||
|
self._is_all_checks_passed = False
|
||||||
|
|
||||||
|
def getIsAllChecksPassed(self) -> bool:
|
||||||
|
return self._is_all_checks_passed
|
||||||
|
|
||||||
|
# Trigger the next callback if available. If not, it means that all callbacks have "passed", which means we should
|
||||||
|
# not block the application to quit, and it will call the application to actually quit.
|
||||||
|
def triggerNextCallback(self) -> None:
|
||||||
|
# Get the next callback and schedule that if
|
||||||
|
this_callback = None
|
||||||
|
if self._current_callback_idx < len(self._on_exit_callback_list):
|
||||||
|
this_callback = self._on_exit_callback_list[self._current_callback_idx]
|
||||||
|
self._current_callback_idx += 1
|
||||||
|
|
||||||
|
if this_callback is not None:
|
||||||
|
Logger.log("d", "Scheduled the next on-app-exit callback [%s]", this_callback)
|
||||||
|
self._application.callLater(this_callback)
|
||||||
|
else:
|
||||||
|
Logger.log("d", "No more on-app-exit callbacks to process. Tell the app to exit.")
|
||||||
|
|
||||||
|
self._is_all_checks_passed = True
|
||||||
|
|
||||||
|
# Tell the application to exit
|
||||||
|
self._application.callLater(self._application.closeApplication)
|
||||||
|
|
||||||
|
# This is the callback function which an on-exit callback should call when it finishes, it should provide the
|
||||||
|
# "should_proceed" flag indicating whether this check has "passed", or in other words, whether quiting the
|
||||||
|
# application should be blocked. If the last on-exit callback doesn't block the quiting, it will call the next
|
||||||
|
# registered on-exit callback if available.
|
||||||
|
def onCurrentCallbackFinished(self, should_proceed: bool = True) -> None:
|
||||||
|
if not should_proceed:
|
||||||
|
Logger.log("d", "on-app-exit callback finished and we should not proceed.")
|
||||||
|
# Reset the state
|
||||||
|
self.resetCurrentState()
|
||||||
|
return
|
||||||
|
|
||||||
|
self.triggerNextCallback()
|
||||||
0
cura/TaskManagement/__init__.py
Normal file
0
cura/TaskManagement/__init__.py
Normal file
57
cura_app.py
57
cura_app.py
|
|
@ -1,9 +1,10 @@
|
||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
# Copyright (c) 2015 Ultimaker B.V.
|
# Copyright (c) 2018 Ultimaker B.V.
|
||||||
# Cura is released under the terms of the LGPLv3 or higher.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
|
import faulthandler
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
|
|
@ -11,14 +12,14 @@ from UM.Platform import Platform
|
||||||
|
|
||||||
parser = argparse.ArgumentParser(prog = "cura",
|
parser = argparse.ArgumentParser(prog = "cura",
|
||||||
add_help = False)
|
add_help = False)
|
||||||
parser.add_argument('--debug',
|
parser.add_argument("--debug",
|
||||||
action='store_true',
|
action="store_true",
|
||||||
default = False,
|
default = False,
|
||||||
help = "Turn on the debug mode by setting this option."
|
help = "Turn on the debug mode by setting this option."
|
||||||
)
|
)
|
||||||
parser.add_argument('--trigger-early-crash',
|
parser.add_argument("--trigger-early-crash",
|
||||||
dest = 'trigger_early_crash',
|
dest = "trigger_early_crash",
|
||||||
action = 'store_true',
|
action = "store_true",
|
||||||
default = False,
|
default = False,
|
||||||
help = "FOR TESTING ONLY. Trigger an early crash to show the crash dialog."
|
help = "FOR TESTING ONLY. Trigger an early crash to show the crash dialog."
|
||||||
)
|
)
|
||||||
|
|
@ -27,7 +28,7 @@ known_args = vars(parser.parse_known_args()[0])
|
||||||
if not known_args["debug"]:
|
if not known_args["debug"]:
|
||||||
def get_cura_dir_path():
|
def get_cura_dir_path():
|
||||||
if Platform.isWindows():
|
if Platform.isWindows():
|
||||||
return os.path.expanduser("~/AppData/Roaming/cura/")
|
return os.path.expanduser("~/AppData/Roaming/cura")
|
||||||
elif Platform.isLinux():
|
elif Platform.isLinux():
|
||||||
return os.path.expanduser("~/.local/share/cura")
|
return os.path.expanduser("~/.local/share/cura")
|
||||||
elif Platform.isOSX():
|
elif Platform.isOSX():
|
||||||
|
|
@ -39,18 +40,19 @@ if not known_args["debug"]:
|
||||||
sys.stdout = open(os.path.join(dirpath, "stdout.log"), "w", encoding = "utf-8")
|
sys.stdout = open(os.path.join(dirpath, "stdout.log"), "w", encoding = "utf-8")
|
||||||
sys.stderr = open(os.path.join(dirpath, "stderr.log"), "w", encoding = "utf-8")
|
sys.stderr = open(os.path.join(dirpath, "stderr.log"), "w", encoding = "utf-8")
|
||||||
|
|
||||||
import platform
|
|
||||||
import faulthandler
|
|
||||||
|
|
||||||
#WORKAROUND: GITHUB-88 GITHUB-385 GITHUB-612
|
# WORKAROUND: GITHUB-88 GITHUB-385 GITHUB-612
|
||||||
if Platform.isLinux(): # Needed for platform.linux_distribution, which is not available on Windows and OSX
|
if Platform.isLinux(): # Needed for platform.linux_distribution, which is not available on Windows and OSX
|
||||||
# For Ubuntu: https://bugs.launchpad.net/ubuntu/+source/python-qt4/+bug/941826
|
# For Ubuntu: https://bugs.launchpad.net/ubuntu/+source/python-qt4/+bug/941826
|
||||||
linux_distro_name = platform.linux_distribution()[0].lower()
|
# The workaround is only needed on Ubuntu+NVidia drivers. Other drivers are not affected, but fine with this fix.
|
||||||
# TODO: Needs a "if X11_GFX == 'nvidia'" here. The workaround is only needed on Ubuntu+NVidia drivers. Other drivers are not affected, but fine with this fix.
|
try:
|
||||||
import ctypes
|
import ctypes
|
||||||
from ctypes.util import find_library
|
from ctypes.util import find_library
|
||||||
libGL = find_library("GL")
|
libGL = find_library("GL")
|
||||||
ctypes.CDLL(libGL, ctypes.RTLD_GLOBAL)
|
ctypes.CDLL(libGL, ctypes.RTLD_GLOBAL)
|
||||||
|
except:
|
||||||
|
# GLES-only systems (e.g. ARM Mali) do not have libGL, ignore error
|
||||||
|
pass
|
||||||
|
|
||||||
# When frozen, i.e. installer version, don't let PYTHONPATH mess up the search path for DLLs.
|
# When frozen, i.e. installer version, don't let PYTHONPATH mess up the search path for DLLs.
|
||||||
if Platform.isWindows() and hasattr(sys, "frozen"):
|
if Platform.isWindows() and hasattr(sys, "frozen"):
|
||||||
|
|
@ -75,6 +77,7 @@ if "PYTHONPATH" in os.environ.keys(): # If PYTHONPATH is u
|
||||||
sys.path.remove(PATH_real)
|
sys.path.remove(PATH_real)
|
||||||
sys.path.insert(1, PATH_real) # Insert it at 1 after os.curdir, which is 0.
|
sys.path.insert(1, PATH_real) # Insert it at 1 after os.curdir, which is 0.
|
||||||
|
|
||||||
|
|
||||||
def exceptHook(hook_type, value, traceback):
|
def exceptHook(hook_type, value, traceback):
|
||||||
from cura.CrashHandler import CrashHandler
|
from cura.CrashHandler import CrashHandler
|
||||||
from cura.CuraApplication import CuraApplication
|
from cura.CuraApplication import CuraApplication
|
||||||
|
|
@ -117,25 +120,19 @@ def exceptHook(hook_type, value, traceback):
|
||||||
_crash_handler.early_crash_dialog.show()
|
_crash_handler.early_crash_dialog.show()
|
||||||
sys.exit(application.exec_())
|
sys.exit(application.exec_())
|
||||||
|
|
||||||
if not known_args["debug"]:
|
|
||||||
sys.excepthook = exceptHook
|
# Set exception hook to use the crash dialog handler
|
||||||
|
sys.excepthook = exceptHook
|
||||||
|
# Enable dumping traceback for all threads
|
||||||
|
faulthandler.enable(all_threads = True)
|
||||||
|
|
||||||
# Workaround for a race condition on certain systems where there
|
# Workaround for a race condition on certain systems where there
|
||||||
# is a race condition between Arcus and PyQt. Importing Arcus
|
# is a race condition between Arcus and PyQt. Importing Arcus
|
||||||
# first seems to prevent Sip from going into a state where it
|
# first seems to prevent Sip from going into a state where it
|
||||||
# tries to create PyQt objects on a non-main thread.
|
# tries to create PyQt objects on a non-main thread.
|
||||||
import Arcus #@UnusedImport
|
import Arcus #@UnusedImport
|
||||||
import cura.CuraApplication
|
import Savitar #@UnusedImport
|
||||||
import cura.Settings.CuraContainerRegistry
|
from cura.CuraApplication import CuraApplication
|
||||||
|
|
||||||
faulthandler.enable()
|
app = CuraApplication()
|
||||||
|
|
||||||
# Force an instance of CuraContainerRegistry to be created and reused later.
|
|
||||||
cura.Settings.CuraContainerRegistry.CuraContainerRegistry.getInstance()
|
|
||||||
|
|
||||||
# This pre-start up check is needed to determine if we should start the application at all.
|
|
||||||
if not cura.CuraApplication.CuraApplication.preStartUp(parser = parser, parsed_command_line = known_args):
|
|
||||||
sys.exit(0)
|
|
||||||
|
|
||||||
app = cura.CuraApplication.CuraApplication.getInstance(parser = parser, parsed_command_line = known_args)
|
|
||||||
app.run()
|
app.run()
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
# Copyright (c) 2018 Ultimaker B.V.
|
# Copyright (c) 2018 Ultimaker B.V.
|
||||||
# Cura is released under the terms of the LGPLv3 or higher.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
|
from typing import Optional
|
||||||
import os.path
|
import os.path
|
||||||
import zipfile
|
import zipfile
|
||||||
|
|
||||||
|
|
@ -15,6 +16,7 @@ from UM.Math.Vector import Vector
|
||||||
from UM.Mesh.MeshBuilder import MeshBuilder
|
from UM.Mesh.MeshBuilder import MeshBuilder
|
||||||
from UM.Mesh.MeshReader import MeshReader
|
from UM.Mesh.MeshReader import MeshReader
|
||||||
from UM.Scene.GroupDecorator import GroupDecorator
|
from UM.Scene.GroupDecorator import GroupDecorator
|
||||||
|
from UM.MimeTypeDatabase import MimeTypeDatabase, MimeType
|
||||||
|
|
||||||
from cura.Settings.ExtruderManager import ExtruderManager
|
from cura.Settings.ExtruderManager import ExtruderManager
|
||||||
from cura.Scene.CuraSceneNode import CuraSceneNode
|
from cura.Scene.CuraSceneNode import CuraSceneNode
|
||||||
|
|
@ -25,6 +27,7 @@ from cura.Machines.QualityManager import getMachineDefinitionIDForQualitySearch
|
||||||
|
|
||||||
MYPY = False
|
MYPY = False
|
||||||
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if not MYPY:
|
if not MYPY:
|
||||||
import xml.etree.cElementTree as ET
|
import xml.etree.cElementTree as ET
|
||||||
|
|
@ -32,10 +35,20 @@ except ImportError:
|
||||||
Logger.log("w", "Unable to load cElementTree, switching to slower version")
|
Logger.log("w", "Unable to load cElementTree, switching to slower version")
|
||||||
import xml.etree.ElementTree as ET
|
import xml.etree.ElementTree as ET
|
||||||
|
|
||||||
|
|
||||||
## Base implementation for reading 3MF files. Has no support for textures. Only loads meshes!
|
## Base implementation for reading 3MF files. Has no support for textures. Only loads meshes!
|
||||||
class ThreeMFReader(MeshReader):
|
class ThreeMFReader(MeshReader):
|
||||||
def __init__(self):
|
def __init__(self) -> None:
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
|
||||||
|
MimeTypeDatabase.addMimeType(
|
||||||
|
MimeType(
|
||||||
|
name = "application/vnd.ms-package.3dmanufacturing-3dmodel+xml",
|
||||||
|
comment="3MF",
|
||||||
|
suffixes=["3mf"]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
self._supported_extensions = [".3mf"]
|
self._supported_extensions = [".3mf"]
|
||||||
self._root = None
|
self._root = None
|
||||||
self._base_name = ""
|
self._base_name = ""
|
||||||
|
|
@ -46,7 +59,7 @@ class ThreeMFReader(MeshReader):
|
||||||
if transformation == "":
|
if transformation == "":
|
||||||
return Matrix()
|
return Matrix()
|
||||||
|
|
||||||
splitted_transformation = transformation.split()
|
split_transformation = transformation.split()
|
||||||
## Transformation is saved as:
|
## Transformation is saved as:
|
||||||
## M00 M01 M02 0.0
|
## M00 M01 M02 0.0
|
||||||
## M10 M11 M12 0.0
|
## M10 M11 M12 0.0
|
||||||
|
|
@ -55,20 +68,20 @@ class ThreeMFReader(MeshReader):
|
||||||
## We switch the row & cols as that is how everyone else uses matrices!
|
## We switch the row & cols as that is how everyone else uses matrices!
|
||||||
temp_mat = Matrix()
|
temp_mat = Matrix()
|
||||||
# Rotation & Scale
|
# Rotation & Scale
|
||||||
temp_mat._data[0, 0] = splitted_transformation[0]
|
temp_mat._data[0, 0] = split_transformation[0]
|
||||||
temp_mat._data[1, 0] = splitted_transformation[1]
|
temp_mat._data[1, 0] = split_transformation[1]
|
||||||
temp_mat._data[2, 0] = splitted_transformation[2]
|
temp_mat._data[2, 0] = split_transformation[2]
|
||||||
temp_mat._data[0, 1] = splitted_transformation[3]
|
temp_mat._data[0, 1] = split_transformation[3]
|
||||||
temp_mat._data[1, 1] = splitted_transformation[4]
|
temp_mat._data[1, 1] = split_transformation[4]
|
||||||
temp_mat._data[2, 1] = splitted_transformation[5]
|
temp_mat._data[2, 1] = split_transformation[5]
|
||||||
temp_mat._data[0, 2] = splitted_transformation[6]
|
temp_mat._data[0, 2] = split_transformation[6]
|
||||||
temp_mat._data[1, 2] = splitted_transformation[7]
|
temp_mat._data[1, 2] = split_transformation[7]
|
||||||
temp_mat._data[2, 2] = splitted_transformation[8]
|
temp_mat._data[2, 2] = split_transformation[8]
|
||||||
|
|
||||||
# Translation
|
# Translation
|
||||||
temp_mat._data[0, 3] = splitted_transformation[9]
|
temp_mat._data[0, 3] = split_transformation[9]
|
||||||
temp_mat._data[1, 3] = splitted_transformation[10]
|
temp_mat._data[1, 3] = split_transformation[10]
|
||||||
temp_mat._data[2, 3] = splitted_transformation[11]
|
temp_mat._data[2, 3] = split_transformation[11]
|
||||||
|
|
||||||
return temp_mat
|
return temp_mat
|
||||||
|
|
||||||
|
|
@ -148,7 +161,7 @@ class ThreeMFReader(MeshReader):
|
||||||
um_node.addDecorator(sliceable_decorator)
|
um_node.addDecorator(sliceable_decorator)
|
||||||
return um_node
|
return um_node
|
||||||
|
|
||||||
def read(self, file_name):
|
def _read(self, file_name):
|
||||||
result = []
|
result = []
|
||||||
self._object_count = 0 # Used to name objects as there is no node name yet.
|
self._object_count = 0 # Used to name objects as there is no node name yet.
|
||||||
# The base object of 3mf is a zipped archive.
|
# The base object of 3mf is a zipped archive.
|
||||||
|
|
@ -186,9 +199,9 @@ class ThreeMFReader(MeshReader):
|
||||||
# Second step: 3MF defines the left corner of the machine as center, whereas cura uses the center of the
|
# Second step: 3MF defines the left corner of the machine as center, whereas cura uses the center of the
|
||||||
# build volume.
|
# build volume.
|
||||||
if global_container_stack:
|
if global_container_stack:
|
||||||
translation_vector = Vector(x=-global_container_stack.getProperty("machine_width", "value") / 2,
|
translation_vector = Vector(x = -global_container_stack.getProperty("machine_width", "value") / 2,
|
||||||
y=-global_container_stack.getProperty("machine_depth", "value") / 2,
|
y = -global_container_stack.getProperty("machine_depth", "value") / 2,
|
||||||
z=0)
|
z = 0)
|
||||||
translation_matrix = Matrix()
|
translation_matrix = Matrix()
|
||||||
translation_matrix.setByTranslation(translation_vector)
|
translation_matrix.setByTranslation(translation_vector)
|
||||||
transformation_matrix.multiply(translation_matrix)
|
transformation_matrix.multiply(translation_matrix)
|
||||||
|
|
@ -224,23 +237,20 @@ class ThreeMFReader(MeshReader):
|
||||||
# * inch
|
# * inch
|
||||||
# * foot
|
# * foot
|
||||||
# * meter
|
# * meter
|
||||||
def _getScaleFromUnit(self, unit):
|
def _getScaleFromUnit(self, unit: Optional[str]) -> Vector:
|
||||||
|
conversion_to_mm = {
|
||||||
|
"micron": 0.001,
|
||||||
|
"millimeter": 1,
|
||||||
|
"centimeter": 10,
|
||||||
|
"meter": 1000,
|
||||||
|
"inch": 25.4,
|
||||||
|
"foot": 304.8
|
||||||
|
}
|
||||||
if unit is None:
|
if unit is None:
|
||||||
unit = "millimeter"
|
unit = "millimeter"
|
||||||
if unit == "micron":
|
elif unit not in conversion_to_mm:
|
||||||
scale = 0.001
|
Logger.log("w", "Unrecognised unit {unit} used. Assuming mm instead.".format(unit = unit))
|
||||||
elif unit == "millimeter":
|
unit = "millimeter"
|
||||||
scale = 1
|
|
||||||
elif unit == "centimeter":
|
|
||||||
scale = 10
|
|
||||||
elif unit == "inch":
|
|
||||||
scale = 25.4
|
|
||||||
elif unit == "foot":
|
|
||||||
scale = 304.8
|
|
||||||
elif unit == "meter":
|
|
||||||
scale = 1000
|
|
||||||
else:
|
|
||||||
Logger.log("w", "Unrecognised unit %s used. Assuming mm instead", unit)
|
|
||||||
scale = 1
|
|
||||||
|
|
||||||
return Vector(scale, scale, scale)
|
scale = conversion_to_mm[unit]
|
||||||
|
return Vector(scale, scale, scale)
|
||||||
|
|
@ -4,9 +4,7 @@
|
||||||
from configparser import ConfigParser
|
from configparser import ConfigParser
|
||||||
import zipfile
|
import zipfile
|
||||||
import os
|
import os
|
||||||
import threading
|
from typing import Dict, List, Tuple, cast
|
||||||
from typing import List, Tuple
|
|
||||||
|
|
||||||
|
|
||||||
import xml.etree.ElementTree as ET
|
import xml.etree.ElementTree as ET
|
||||||
|
|
||||||
|
|
@ -14,6 +12,7 @@ from UM.Workspace.WorkspaceReader import WorkspaceReader
|
||||||
from UM.Application import Application
|
from UM.Application import Application
|
||||||
|
|
||||||
from UM.Logger import Logger
|
from UM.Logger import Logger
|
||||||
|
from UM.Message import Message
|
||||||
from UM.i18n import i18nCatalog
|
from UM.i18n import i18nCatalog
|
||||||
from UM.Signal import postponeSignals, CompressTechnique
|
from UM.Signal import postponeSignals, CompressTechnique
|
||||||
from UM.Settings.ContainerFormatError import ContainerFormatError
|
from UM.Settings.ContainerFormatError import ContainerFormatError
|
||||||
|
|
@ -21,10 +20,11 @@ from UM.Settings.ContainerStack import ContainerStack
|
||||||
from UM.Settings.DefinitionContainer import DefinitionContainer
|
from UM.Settings.DefinitionContainer import DefinitionContainer
|
||||||
from UM.Settings.InstanceContainer import InstanceContainer
|
from UM.Settings.InstanceContainer import InstanceContainer
|
||||||
from UM.Settings.ContainerRegistry import ContainerRegistry
|
from UM.Settings.ContainerRegistry import ContainerRegistry
|
||||||
from UM.MimeTypeDatabase import MimeTypeDatabase
|
from UM.MimeTypeDatabase import MimeTypeDatabase, MimeType
|
||||||
from UM.Job import Job
|
from UM.Job import Job
|
||||||
from UM.Preferences import Preferences
|
from UM.Preferences import Preferences
|
||||||
|
|
||||||
|
from cura.Machines.VariantType import VariantType
|
||||||
from cura.Settings.CuraStackBuilder import CuraStackBuilder
|
from cura.Settings.CuraStackBuilder import CuraStackBuilder
|
||||||
from cura.Settings.ExtruderStack import ExtruderStack
|
from cura.Settings.ExtruderStack import ExtruderStack
|
||||||
from cura.Settings.GlobalStack import GlobalStack
|
from cura.Settings.GlobalStack import GlobalStack
|
||||||
|
|
@ -38,7 +38,7 @@ i18n_catalog = i18nCatalog("cura")
|
||||||
|
|
||||||
|
|
||||||
class ContainerInfo:
|
class ContainerInfo:
|
||||||
def __init__(self, file_name: str, serialized: str, parser: ConfigParser):
|
def __init__(self, file_name: str, serialized: str, parser: ConfigParser) -> None:
|
||||||
self.file_name = file_name
|
self.file_name = file_name
|
||||||
self.serialized = serialized
|
self.serialized = serialized
|
||||||
self.parser = parser
|
self.parser = parser
|
||||||
|
|
@ -47,14 +47,14 @@ class ContainerInfo:
|
||||||
|
|
||||||
|
|
||||||
class QualityChangesInfo:
|
class QualityChangesInfo:
|
||||||
def __init__(self):
|
def __init__(self) -> None:
|
||||||
self.name = None
|
self.name = None
|
||||||
self.global_info = None
|
self.global_info = None
|
||||||
self.extruder_info_dict = {}
|
self.extruder_info_dict = {} # type: Dict[str, ContainerInfo]
|
||||||
|
|
||||||
|
|
||||||
class MachineInfo:
|
class MachineInfo:
|
||||||
def __init__(self):
|
def __init__(self) -> None:
|
||||||
self.container_id = None
|
self.container_id = None
|
||||||
self.name = None
|
self.name = None
|
||||||
self.definition_id = None
|
self.definition_id = None
|
||||||
|
|
@ -66,11 +66,11 @@ class MachineInfo:
|
||||||
self.definition_changes_info = None
|
self.definition_changes_info = None
|
||||||
self.user_changes_info = None
|
self.user_changes_info = None
|
||||||
|
|
||||||
self.extruder_info_dict = {}
|
self.extruder_info_dict = {} # type: Dict[str, ExtruderInfo]
|
||||||
|
|
||||||
|
|
||||||
class ExtruderInfo:
|
class ExtruderInfo:
|
||||||
def __init__(self):
|
def __init__(self) -> None:
|
||||||
self.position = None
|
self.position = None
|
||||||
self.enabled = True
|
self.enabled = True
|
||||||
self.variant_info = None
|
self.variant_info = None
|
||||||
|
|
@ -82,20 +82,29 @@ class ExtruderInfo:
|
||||||
|
|
||||||
## Base implementation for reading 3MF workspace files.
|
## Base implementation for reading 3MF workspace files.
|
||||||
class ThreeMFWorkspaceReader(WorkspaceReader):
|
class ThreeMFWorkspaceReader(WorkspaceReader):
|
||||||
def __init__(self):
|
def __init__(self) -> None:
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
|
||||||
|
MimeTypeDatabase.addMimeType(
|
||||||
|
MimeType(
|
||||||
|
name="application/x-curaproject+xml",
|
||||||
|
comment="Cura Project File",
|
||||||
|
suffixes=["curaproject.3mf"]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
self._supported_extensions = [".3mf"]
|
self._supported_extensions = [".3mf"]
|
||||||
self._dialog = WorkspaceDialog()
|
self._dialog = WorkspaceDialog()
|
||||||
self._3mf_mesh_reader = None
|
self._3mf_mesh_reader = None
|
||||||
self._container_registry = ContainerRegistry.getInstance()
|
self._container_registry = ContainerRegistry.getInstance()
|
||||||
|
|
||||||
# suffixes registered with the MineTypes don't start with a dot '.'
|
# suffixes registered with the MimeTypes don't start with a dot '.'
|
||||||
self._definition_container_suffix = "." + ContainerRegistry.getMimeTypeForContainer(DefinitionContainer).preferredSuffix
|
self._definition_container_suffix = "." + cast(MimeType, ContainerRegistry.getMimeTypeForContainer(DefinitionContainer)).preferredSuffix
|
||||||
self._material_container_suffix = None # We have to wait until all other plugins are loaded before we can set it
|
self._material_container_suffix = None # We have to wait until all other plugins are loaded before we can set it
|
||||||
self._instance_container_suffix = "." + ContainerRegistry.getMimeTypeForContainer(InstanceContainer).preferredSuffix
|
self._instance_container_suffix = "." + cast(MimeType, ContainerRegistry.getMimeTypeForContainer(InstanceContainer)).preferredSuffix
|
||||||
self._container_stack_suffix = "." + ContainerRegistry.getMimeTypeForContainer(ContainerStack).preferredSuffix
|
self._container_stack_suffix = "." + cast(MimeType, ContainerRegistry.getMimeTypeForContainer(ContainerStack)).preferredSuffix
|
||||||
self._extruder_stack_suffix = "." + ContainerRegistry.getMimeTypeForContainer(ExtruderStack).preferredSuffix
|
self._extruder_stack_suffix = "." + cast(MimeType, ContainerRegistry.getMimeTypeForContainer(ExtruderStack)).preferredSuffix
|
||||||
self._global_stack_suffix = "." + ContainerRegistry.getMimeTypeForContainer(GlobalStack).preferredSuffix
|
self._global_stack_suffix = "." + cast(MimeType, ContainerRegistry.getMimeTypeForContainer(GlobalStack)).preferredSuffix
|
||||||
|
|
||||||
# Certain instance container types are ignored because we make the assumption that only we make those types
|
# Certain instance container types are ignored because we make the assumption that only we make those types
|
||||||
# of containers. They are:
|
# of containers. They are:
|
||||||
|
|
@ -103,28 +112,26 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
|
||||||
# - variant
|
# - variant
|
||||||
self._ignored_instance_container_types = {"quality", "variant"}
|
self._ignored_instance_container_types = {"quality", "variant"}
|
||||||
|
|
||||||
self._resolve_strategies = {}
|
self._resolve_strategies = {} # type: Dict[str, str]
|
||||||
|
|
||||||
self._id_mapping = {}
|
self._id_mapping = {} # type: Dict[str, str]
|
||||||
|
|
||||||
# In Cura 2.5 and 2.6, the empty profiles used to have those long names
|
# In Cura 2.5 and 2.6, the empty profiles used to have those long names
|
||||||
self._old_empty_profile_id_dict = {"empty_%s" % k: "empty" for k in ["material", "variant"]}
|
self._old_empty_profile_id_dict = {"empty_%s" % k: "empty" for k in ["material", "variant"]}
|
||||||
|
|
||||||
self._is_same_machine_type = False
|
self._is_same_machine_type = False
|
||||||
self._old_new_materials = {}
|
self._old_new_materials = {} # type: Dict[str, str]
|
||||||
self._materials_to_select = {}
|
|
||||||
self._machine_info = None
|
self._machine_info = None
|
||||||
|
|
||||||
def _clearState(self):
|
def _clearState(self):
|
||||||
self._is_same_machine_type = False
|
self._is_same_machine_type = False
|
||||||
self._id_mapping = {}
|
self._id_mapping = {}
|
||||||
self._old_new_materials = {}
|
self._old_new_materials = {}
|
||||||
self._materials_to_select = {}
|
|
||||||
self._machine_info = None
|
self._machine_info = None
|
||||||
|
|
||||||
## Get a unique name based on the old_id. This is different from directly calling the registry in that it caches results.
|
## Get a unique name based on the old_id. This is different from directly calling the registry in that it caches results.
|
||||||
# This has nothing to do with speed, but with getting consistent new naming for instances & objects.
|
# This has nothing to do with speed, but with getting consistent new naming for instances & objects.
|
||||||
def getNewId(self, old_id):
|
def getNewId(self, old_id: str):
|
||||||
if old_id not in self._id_mapping:
|
if old_id not in self._id_mapping:
|
||||||
self._id_mapping[old_id] = self._container_registry.uniqueName(old_id)
|
self._id_mapping[old_id] = self._container_registry.uniqueName(old_id)
|
||||||
return self._id_mapping[old_id]
|
return self._id_mapping[old_id]
|
||||||
|
|
@ -456,12 +463,26 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
|
||||||
num_visible_settings = len(visible_settings_string.split(";"))
|
num_visible_settings = len(visible_settings_string.split(";"))
|
||||||
active_mode = temp_preferences.getValue("cura/active_mode")
|
active_mode = temp_preferences.getValue("cura/active_mode")
|
||||||
if not active_mode:
|
if not active_mode:
|
||||||
active_mode = Preferences.getInstance().getValue("cura/active_mode")
|
active_mode = Application.getInstance().getPreferences().getValue("cura/active_mode")
|
||||||
except KeyError:
|
except KeyError:
|
||||||
# If there is no preferences file, it's not a workspace, so notify user of failure.
|
# If there is no preferences file, it's not a workspace, so notify user of failure.
|
||||||
Logger.log("w", "File %s is not a valid workspace.", file_name)
|
Logger.log("w", "File %s is not a valid workspace.", file_name)
|
||||||
return WorkspaceReader.PreReadResult.failed
|
return WorkspaceReader.PreReadResult.failed
|
||||||
|
|
||||||
|
# Check if the machine definition exists. If not, indicate failure because we do not import definition files.
|
||||||
|
def_results = self._container_registry.findDefinitionContainersMetadata(id = machine_definition_id)
|
||||||
|
if not def_results:
|
||||||
|
message = Message(i18n_catalog.i18nc("@info:status Don't translate the XML tags <filename> or <message>!",
|
||||||
|
"Project file <filename>{0}</filename> contains an unknown machine type"
|
||||||
|
" <message>{1}</message>. Cannot import the machine."
|
||||||
|
" Models will be imported instead.", file_name, machine_definition_id),
|
||||||
|
title = i18n_catalog.i18nc("@info:title", "Open Project File"))
|
||||||
|
message.show()
|
||||||
|
|
||||||
|
Logger.log("i", "Could unknown machine definition %s in project file %s, cannot import it.",
|
||||||
|
self._machine_info.definition_id, file_name)
|
||||||
|
return WorkspaceReader.PreReadResult.failed
|
||||||
|
|
||||||
# In case we use preRead() to check if a file is a valid project file, we don't want to show a dialog.
|
# In case we use preRead() to check if a file is a valid project file, we don't want to show a dialog.
|
||||||
if not show_dialog:
|
if not show_dialog:
|
||||||
return WorkspaceReader.PreReadResult.accepted
|
return WorkspaceReader.PreReadResult.accepted
|
||||||
|
|
@ -575,7 +596,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
|
||||||
temp_preferences.deserialize(serialized)
|
temp_preferences.deserialize(serialized)
|
||||||
|
|
||||||
# Copy a number of settings from the temp preferences to the global
|
# Copy a number of settings from the temp preferences to the global
|
||||||
global_preferences = Preferences.getInstance()
|
global_preferences = application.getInstance().getPreferences()
|
||||||
|
|
||||||
visible_settings = temp_preferences.getValue("general/visible_settings")
|
visible_settings = temp_preferences.getValue("general/visible_settings")
|
||||||
if visible_settings is None:
|
if visible_settings is None:
|
||||||
|
|
@ -598,9 +619,10 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
|
||||||
machine_name = self._container_registry.uniqueName(self._machine_info.name)
|
machine_name = self._container_registry.uniqueName(self._machine_info.name)
|
||||||
|
|
||||||
global_stack = CuraStackBuilder.createMachine(machine_name, self._machine_info.definition_id)
|
global_stack = CuraStackBuilder.createMachine(machine_name, self._machine_info.definition_id)
|
||||||
extruder_stack_dict = global_stack.extruders
|
if global_stack: #Only switch if creating the machine was successful.
|
||||||
|
extruder_stack_dict = global_stack.extruders
|
||||||
|
|
||||||
self._container_registry.addContainer(global_stack)
|
self._container_registry.addContainer(global_stack)
|
||||||
else:
|
else:
|
||||||
# Find the machine
|
# Find the machine
|
||||||
global_stack = self._container_registry.findContainerStacks(name = self._machine_info.name, type = "machine")[0]
|
global_stack = self._container_registry.findContainerStacks(name = self._machine_info.name, type = "machine")[0]
|
||||||
|
|
@ -608,6 +630,11 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
|
||||||
type = "extruder_train")
|
type = "extruder_train")
|
||||||
extruder_stack_dict = {stack.getMetaDataEntry("position"): stack for stack in extruder_stacks}
|
extruder_stack_dict = {stack.getMetaDataEntry("position"): stack for stack in extruder_stacks}
|
||||||
|
|
||||||
|
# Make sure that those extruders have the global stack as the next stack or later some value evaluation
|
||||||
|
# will fail.
|
||||||
|
for stack in extruder_stacks:
|
||||||
|
stack.setNextStack(global_stack, connect_signals = False)
|
||||||
|
|
||||||
Logger.log("d", "Workspace loading is checking definitions...")
|
Logger.log("d", "Workspace loading is checking definitions...")
|
||||||
# Get all the definition files & check if they exist. If not, add them.
|
# Get all the definition files & check if they exist. If not, add them.
|
||||||
definition_container_files = [name for name in cura_file_names if name.endswith(self._definition_container_suffix)]
|
definition_container_files = [name for name in cura_file_names if name.endswith(self._definition_container_suffix)]
|
||||||
|
|
@ -647,7 +674,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
|
||||||
else:
|
else:
|
||||||
material_container = materials[0]
|
material_container = materials[0]
|
||||||
old_material_root_id = material_container.getMetaDataEntry("base_file")
|
old_material_root_id = material_container.getMetaDataEntry("base_file")
|
||||||
if not self._container_registry.isReadOnly(old_material_root_id): # Only create new materials if they are not read only.
|
if old_material_root_id is not None and not self._container_registry.isReadOnly(old_material_root_id): # Only create new materials if they are not read only.
|
||||||
to_deserialize_material = True
|
to_deserialize_material = True
|
||||||
|
|
||||||
if self._resolve_strategies["material"] == "override":
|
if self._resolve_strategies["material"] == "override":
|
||||||
|
|
@ -868,7 +895,6 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
|
||||||
parser = self._machine_info.variant_info.parser
|
parser = self._machine_info.variant_info.parser
|
||||||
variant_name = parser["general"]["name"]
|
variant_name = parser["general"]["name"]
|
||||||
|
|
||||||
from cura.Machines.VariantManager import VariantType
|
|
||||||
variant_type = VariantType.BUILD_PLATE
|
variant_type = VariantType.BUILD_PLATE
|
||||||
|
|
||||||
node = variant_manager.getVariantNode(global_stack.definition.getId(), variant_name, variant_type)
|
node = variant_manager.getVariantNode(global_stack.definition.getId(), variant_name, variant_type)
|
||||||
|
|
@ -884,7 +910,6 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
|
||||||
parser = extruder_info.variant_info.parser
|
parser = extruder_info.variant_info.parser
|
||||||
|
|
||||||
variant_name = parser["general"]["name"]
|
variant_name = parser["general"]["name"]
|
||||||
from cura.Machines.VariantManager import VariantType
|
|
||||||
variant_type = VariantType.NOZZLE
|
variant_type = VariantType.NOZZLE
|
||||||
|
|
||||||
node = variant_manager.getVariantNode(global_stack.definition.getId(), variant_name, variant_type)
|
node = variant_manager.getVariantNode(global_stack.definition.getId(), variant_name, variant_type)
|
||||||
|
|
@ -908,12 +933,16 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
|
||||||
root_material_id = extruder_info.root_material_id
|
root_material_id = extruder_info.root_material_id
|
||||||
root_material_id = self._old_new_materials.get(root_material_id, root_material_id)
|
root_material_id = self._old_new_materials.get(root_material_id, root_material_id)
|
||||||
|
|
||||||
|
build_plate_id = global_stack.variant.getId()
|
||||||
|
|
||||||
# get material diameter of this extruder
|
# get material diameter of this extruder
|
||||||
machine_material_diameter = extruder_stack.materialDiameter
|
machine_material_diameter = extruder_stack.materialDiameter
|
||||||
material_node = material_manager.getMaterialNode(global_stack.definition.getId(),
|
material_node = material_manager.getMaterialNode(global_stack.definition.getId(),
|
||||||
extruder_stack.variant.getName(),
|
extruder_stack.variant.getName(),
|
||||||
|
build_plate_id,
|
||||||
machine_material_diameter,
|
machine_material_diameter,
|
||||||
root_material_id)
|
root_material_id)
|
||||||
|
|
||||||
if material_node is not None and material_node.getContainer() is not None:
|
if material_node is not None and material_node.getContainer() is not None:
|
||||||
extruder_stack.material = material_node.getContainer()
|
extruder_stack.material = material_node.getContainer()
|
||||||
|
|
||||||
|
|
@ -942,7 +971,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
|
||||||
if not extruder_info:
|
if not extruder_info:
|
||||||
continue
|
continue
|
||||||
if "enabled" not in extruder_stack.getMetaData():
|
if "enabled" not in extruder_stack.getMetaData():
|
||||||
extruder_stack.addMetaDataEntry("enabled", "True")
|
extruder_stack.setMetaDataEntry("enabled", "True")
|
||||||
extruder_stack.setMetaDataEntry("enabled", str(extruder_info.enabled))
|
extruder_stack.setMetaDataEntry("enabled", str(extruder_info.enabled))
|
||||||
|
|
||||||
def _updateActiveMachine(self, global_stack):
|
def _updateActiveMachine(self, global_stack):
|
||||||
|
|
|
||||||
|
|
@ -187,7 +187,10 @@ class WorkspaceDialog(QObject):
|
||||||
|
|
||||||
@pyqtProperty(int, constant = True)
|
@pyqtProperty(int, constant = True)
|
||||||
def totalNumberOfSettings(self):
|
def totalNumberOfSettings(self):
|
||||||
return len(ContainerRegistry.getInstance().findDefinitionContainers(id="fdmprinter")[0].getAllKeys())
|
general_definition_containers = ContainerRegistry.getInstance().findDefinitionContainers(id = "fdmprinter")
|
||||||
|
if not general_definition_containers:
|
||||||
|
return 0
|
||||||
|
return len(general_definition_containers[0].getAllKeys())
|
||||||
|
|
||||||
@pyqtProperty(int, notify = numVisibleSettingsChanged)
|
@pyqtProperty(int, notify = numVisibleSettingsChanged)
|
||||||
def numVisibleSettings(self):
|
def numVisibleSettings(self):
|
||||||
|
|
|
||||||
|
|
@ -13,10 +13,12 @@ from . import ThreeMFWorkspaceReader
|
||||||
|
|
||||||
from UM.i18n import i18nCatalog
|
from UM.i18n import i18nCatalog
|
||||||
from UM.Platform import Platform
|
from UM.Platform import Platform
|
||||||
|
|
||||||
catalog = i18nCatalog("cura")
|
catalog = i18nCatalog("cura")
|
||||||
|
|
||||||
|
|
||||||
def getMetaData() -> Dict:
|
def getMetaData() -> Dict:
|
||||||
# Workarround for osx not supporting double file extensions correctly.
|
# Workaround for osx not supporting double file extensions correctly.
|
||||||
if Platform.isOSX():
|
if Platform.isOSX():
|
||||||
workspace_extension = "3mf"
|
workspace_extension = "3mf"
|
||||||
else:
|
else:
|
||||||
|
|
|
||||||
|
|
@ -51,7 +51,7 @@ class ThreeMFWorkspaceWriter(WorkspaceWriter):
|
||||||
self._writeContainerToArchive(container, archive)
|
self._writeContainerToArchive(container, archive)
|
||||||
|
|
||||||
# Write preferences to archive
|
# Write preferences to archive
|
||||||
original_preferences = Preferences.getInstance() #Copy only the preferences that we use to the workspace.
|
original_preferences = Application.getInstance().getPreferences() #Copy only the preferences that we use to the workspace.
|
||||||
temp_preferences = Preferences()
|
temp_preferences = Preferences()
|
||||||
for preference in {"general/visible_settings", "cura/active_mode", "cura/categories_expanded"}:
|
for preference in {"general/visible_settings", "cura/active_mode", "cura/categories_expanded"}:
|
||||||
temp_preferences.addPreference(preference, None)
|
temp_preferences.addPreference(preference, None)
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,9 @@ except ImportError:
|
||||||
import zipfile
|
import zipfile
|
||||||
import UM.Application
|
import UM.Application
|
||||||
|
|
||||||
|
from UM.i18n import i18nCatalog
|
||||||
|
catalog = i18nCatalog("cura")
|
||||||
|
|
||||||
|
|
||||||
class ThreeMFWriter(MeshWriter):
|
class ThreeMFWriter(MeshWriter):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
|
@ -91,7 +94,7 @@ class ThreeMFWriter(MeshWriter):
|
||||||
# Handle per object settings (if any)
|
# Handle per object settings (if any)
|
||||||
stack = um_node.callDecoration("getStack")
|
stack = um_node.callDecoration("getStack")
|
||||||
if stack is not None:
|
if stack is not None:
|
||||||
changed_setting_keys = set(stack.getTop().getAllKeys())
|
changed_setting_keys = stack.getTop().getAllKeys()
|
||||||
|
|
||||||
# Ensure that we save the extruder used for this object in a multi-extrusion setup
|
# Ensure that we save the extruder used for this object in a multi-extrusion setup
|
||||||
if stack.getProperty("machine_extruder_count", "value") > 1:
|
if stack.getProperty("machine_extruder_count", "value") > 1:
|
||||||
|
|
@ -173,6 +176,7 @@ class ThreeMFWriter(MeshWriter):
|
||||||
archive.writestr(relations_file, b'<?xml version="1.0" encoding="UTF-8"?> \n' + ET.tostring(relations_element))
|
archive.writestr(relations_file, b'<?xml version="1.0" encoding="UTF-8"?> \n' + ET.tostring(relations_element))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
Logger.logException("e", "Error writing zip file")
|
Logger.logException("e", "Error writing zip file")
|
||||||
|
self.setInformation(catalog.i18nc("@error:zip", "Error writing 3mf file."))
|
||||||
return False
|
return False
|
||||||
finally:
|
finally:
|
||||||
if not self._store_archive:
|
if not self._store_archive:
|
||||||
|
|
|
||||||
|
|
@ -1,77 +0,0 @@
|
||||||
# Copyright (c) 2016 Ultimaker B.V.
|
|
||||||
# Cura is released under the terms of the LGPLv3 or higher.
|
|
||||||
|
|
||||||
from PyQt5.QtCore import QTimer
|
|
||||||
|
|
||||||
from UM.Extension import Extension
|
|
||||||
from UM.Preferences import Preferences
|
|
||||||
from UM.Application import Application
|
|
||||||
from UM.Resources import Resources
|
|
||||||
from UM.Logger import Logger
|
|
||||||
|
|
||||||
|
|
||||||
class AutoSave(Extension):
|
|
||||||
def __init__(self):
|
|
||||||
super().__init__()
|
|
||||||
|
|
||||||
Preferences.getInstance().preferenceChanged.connect(self._triggerTimer)
|
|
||||||
|
|
||||||
self._global_stack = None
|
|
||||||
|
|
||||||
Preferences.getInstance().addPreference("cura/autosave_delay", 1000 * 10)
|
|
||||||
|
|
||||||
self._change_timer = QTimer()
|
|
||||||
self._change_timer.setInterval(Preferences.getInstance().getValue("cura/autosave_delay"))
|
|
||||||
self._change_timer.setSingleShot(True)
|
|
||||||
|
|
||||||
self._saving = False
|
|
||||||
|
|
||||||
# At this point, the Application instance has not finished its constructor call yet, so directly using something
|
|
||||||
# like Application.getInstance() is not correct. The initialisation now will only gets triggered after the
|
|
||||||
# application finishes its start up successfully.
|
|
||||||
self._init_timer = QTimer()
|
|
||||||
self._init_timer.setInterval(1000)
|
|
||||||
self._init_timer.setSingleShot(True)
|
|
||||||
self._init_timer.timeout.connect(self.initialize)
|
|
||||||
self._init_timer.start()
|
|
||||||
|
|
||||||
def initialize(self):
|
|
||||||
# only initialise if the application is created and has started
|
|
||||||
from cura.CuraApplication import CuraApplication
|
|
||||||
if not CuraApplication.Created:
|
|
||||||
self._init_timer.start()
|
|
||||||
return
|
|
||||||
if not CuraApplication.getInstance().started:
|
|
||||||
self._init_timer.start()
|
|
||||||
return
|
|
||||||
|
|
||||||
self._change_timer.timeout.connect(self._onTimeout)
|
|
||||||
Application.getInstance().globalContainerStackChanged.connect(self._onGlobalStackChanged)
|
|
||||||
self._onGlobalStackChanged()
|
|
||||||
|
|
||||||
self._triggerTimer()
|
|
||||||
|
|
||||||
def _triggerTimer(self, *args):
|
|
||||||
if not self._saving:
|
|
||||||
self._change_timer.start()
|
|
||||||
|
|
||||||
def _onGlobalStackChanged(self):
|
|
||||||
if self._global_stack:
|
|
||||||
self._global_stack.propertyChanged.disconnect(self._triggerTimer)
|
|
||||||
self._global_stack.containersChanged.disconnect(self._triggerTimer)
|
|
||||||
|
|
||||||
self._global_stack = Application.getInstance().getGlobalContainerStack()
|
|
||||||
|
|
||||||
if self._global_stack:
|
|
||||||
self._global_stack.propertyChanged.connect(self._triggerTimer)
|
|
||||||
self._global_stack.containersChanged.connect(self._triggerTimer)
|
|
||||||
|
|
||||||
def _onTimeout(self):
|
|
||||||
self._saving = True # To prevent the save process from triggering another autosave.
|
|
||||||
Logger.log("d", "Autosaving preferences, instances and profiles")
|
|
||||||
|
|
||||||
Application.getInstance().saveSettings()
|
|
||||||
|
|
||||||
Preferences.getInstance().writeToFile(Resources.getStoragePath(Resources.Preferences, Application.getInstance().getApplicationName() + ".cfg"))
|
|
||||||
|
|
||||||
self._saving = False
|
|
||||||
|
|
@ -1,13 +0,0 @@
|
||||||
# Copyright (c) 2016 Ultimaker B.V.
|
|
||||||
# Cura is released under the terms of the LGPLv3 or higher.
|
|
||||||
|
|
||||||
from . import AutoSave
|
|
||||||
|
|
||||||
from UM.i18n import i18nCatalog
|
|
||||||
catalog = i18nCatalog("cura")
|
|
||||||
|
|
||||||
def getMetaData():
|
|
||||||
return {}
|
|
||||||
|
|
||||||
def register(app):
|
|
||||||
return { "extension": AutoSave.AutoSave() }
|
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
{
|
|
||||||
"name": "Auto Save",
|
|
||||||
"author": "Ultimaker B.V.",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"description": "Automatically saves Preferences, Machines and Profiles after changes.",
|
|
||||||
"api": 4,
|
|
||||||
"i18n-catalog": "cura"
|
|
||||||
}
|
|
||||||
|
|
@ -3,7 +3,6 @@
|
||||||
|
|
||||||
from UM.i18n import i18nCatalog
|
from UM.i18n import i18nCatalog
|
||||||
from UM.Extension import Extension
|
from UM.Extension import Extension
|
||||||
from UM.Preferences import Preferences
|
|
||||||
from UM.Application import Application
|
from UM.Application import Application
|
||||||
from UM.PluginRegistry import PluginRegistry
|
from UM.PluginRegistry import PluginRegistry
|
||||||
from UM.Version import Version
|
from UM.Version import Version
|
||||||
|
|
@ -29,7 +28,7 @@ class ChangeLog(Extension, QObject,):
|
||||||
|
|
||||||
self._change_logs = None
|
self._change_logs = None
|
||||||
Application.getInstance().engineCreatedSignal.connect(self._onEngineCreated)
|
Application.getInstance().engineCreatedSignal.connect(self._onEngineCreated)
|
||||||
Preferences.getInstance().addPreference("general/latest_version_changelog_shown", "2.0.0") #First version of CURA with uranium
|
Application.getInstance().getPreferences().addPreference("general/latest_version_changelog_shown", "2.0.0") #First version of CURA with uranium
|
||||||
self.addMenuItem(catalog.i18nc("@item:inmenu", "Show Changelog"), self.showChangelog)
|
self.addMenuItem(catalog.i18nc("@item:inmenu", "Show Changelog"), self.showChangelog)
|
||||||
|
|
||||||
def getChangeLogs(self):
|
def getChangeLogs(self):
|
||||||
|
|
@ -56,7 +55,7 @@ class ChangeLog(Extension, QObject,):
|
||||||
|
|
||||||
def loadChangeLogs(self):
|
def loadChangeLogs(self):
|
||||||
self._change_logs = collections.OrderedDict()
|
self._change_logs = collections.OrderedDict()
|
||||||
with open(os.path.join(PluginRegistry.getInstance().getPluginPath(self.getPluginId()), "ChangeLog.txt"), "r",-1, "utf-8") as f:
|
with open(os.path.join(PluginRegistry.getInstance().getPluginPath(self.getPluginId()), "ChangeLog.txt"), "r", encoding = "utf-8") as f:
|
||||||
open_version = None
|
open_version = None
|
||||||
open_header = "" # Initialise to an empty header in case there is no "*" in the first line of the changelog
|
open_header = "" # Initialise to an empty header in case there is no "*" in the first line of the changelog
|
||||||
for line in f:
|
for line in f:
|
||||||
|
|
@ -79,12 +78,12 @@ class ChangeLog(Extension, QObject,):
|
||||||
if not self._current_app_version:
|
if not self._current_app_version:
|
||||||
return #We're on dev branch.
|
return #We're on dev branch.
|
||||||
|
|
||||||
if Preferences.getInstance().getValue("general/latest_version_changelog_shown") == "master":
|
if Application.getInstance().getPreferences().getValue("general/latest_version_changelog_shown") == "master":
|
||||||
latest_version_shown = Version("0.0.0")
|
latest_version_shown = Version("0.0.0")
|
||||||
else:
|
else:
|
||||||
latest_version_shown = Version(Preferences.getInstance().getValue("general/latest_version_changelog_shown"))
|
latest_version_shown = Version(Application.getInstance().getPreferences().getValue("general/latest_version_changelog_shown"))
|
||||||
|
|
||||||
Preferences.getInstance().setValue("general/latest_version_changelog_shown", Application.getInstance().getVersion())
|
Application.getInstance().getPreferences().setValue("general/latest_version_changelog_shown", Application.getInstance().getVersion())
|
||||||
|
|
||||||
# Do not show the changelog when there is no global container stack
|
# Do not show the changelog when there is no global container stack
|
||||||
# This implies we are running Cura for the first time.
|
# This implies we are running Cura for the first time.
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,132 @@
|
||||||
|
[3.4.1]
|
||||||
|
*Bug fixes
|
||||||
|
- Fixed an issue that would occasionally cause an unnecessary extra skin wall to be printed, which increased print time.
|
||||||
|
- Fixed an issue in which supports were not generated on the initial layer, because the engine expected a brim to be in place.
|
||||||
|
- Conical and tree supports are now limited within the build plate volume.
|
||||||
|
- Fixed various startup crashes, including: copying of the version folder, errors while deleting packages, storing the old files, and losing data on install.
|
||||||
|
|
||||||
|
[3.4.0]
|
||||||
|
|
||||||
|
*Toolbox
|
||||||
|
The plugin browser has been remodeled into the Toolbox. Navigation now involves graphical elements such as tiles, which can be clicked for further details.
|
||||||
|
|
||||||
|
*Upgradable bundled resources
|
||||||
|
It is now possible to have multiple versions of bundled resources installed: the bundled version and the downloaded upgrade. If an upgrade in the form of a package is present, the bundled version will not be loaded. If it's not present, Ultimaker Cura will revert to the bundled version.
|
||||||
|
|
||||||
|
*Package manager recognizes bundled resources
|
||||||
|
Bundled packages are now made visible to the CuraPackageMangager. This means the resources are included by default, as well as the "wrapping" of a package, (e.g. package.json) so that the CuraPackageManger and Toolbox recognize them as being installed.
|
||||||
|
|
||||||
|
*Retraction combing max distance
|
||||||
|
New setting for maximum combing travel distance. Combing travel moves longer than this value will use retraction. Contributed by smartavionics.
|
||||||
|
|
||||||
|
*Infill support
|
||||||
|
When enabled, infill will be generated only where it is needed using a specialized support generation algorithm for the internal support structures of a part. Contributed by BagelOrb.
|
||||||
|
|
||||||
|
*Print outside perimeter before holes
|
||||||
|
This prioritizes outside perimeters before printing holes. By printing holes as late as possible, there is a reduced risk of travel moves dislodging them from the build plate. This setting should only have an effect if printing outer before inner walls. Contributed by smartavionics.
|
||||||
|
|
||||||
|
*Disable omitting retractions in support
|
||||||
|
Previous versions had no option to disable omitting retraction moves when printing supports, which could cause issues with third-party machines or materials. An option has been added to disable this. Contributed by BagelOrb.
|
||||||
|
|
||||||
|
*Support wall line count
|
||||||
|
Added setting to configure how many walls to print around supports. Contributed by BagelOrb.
|
||||||
|
|
||||||
|
*Maximum combing resolution
|
||||||
|
Combing travel moves are kept at least 1.5 mm long to prevent buffer underruns.
|
||||||
|
|
||||||
|
*Avoid supports when traveling
|
||||||
|
Added setting to avoid supports when performing travel moves. This minimizes the risk of the print head hitting support material.
|
||||||
|
|
||||||
|
*Rewrite cross infill
|
||||||
|
Experimental setting that allows you to input a path to an image to manipulate the cross infill density. This will overlay that image on your model. Contributed by BagelOrb.
|
||||||
|
|
||||||
|
*Backup and restore
|
||||||
|
Added functionality to backup and restore settings and profiles to cloud using the Cura Backups plugin.
|
||||||
|
|
||||||
|
*Auto-select model after import
|
||||||
|
User can now set preferences for the behavior of selecting a newly imported model or not.
|
||||||
|
|
||||||
|
*Settings filter timeout
|
||||||
|
The settings filter is triggered on enter or after a 500ms timeout when typing a setting to filter.
|
||||||
|
|
||||||
|
*Event measurements
|
||||||
|
Added time measurement to logs for occurrences, including startup time, file load time, number of items on the build plate when slicing, slicing time, and time and performance when moving items on the build plate, for benchmarking purposes.
|
||||||
|
|
||||||
|
*Send anonymous data
|
||||||
|
Disable button on the ‘Send anonymous data’ popup has changed to a ‘more info’ button, with further options to enable/disable anonymous data messages.
|
||||||
|
|
||||||
|
*Configuration error assistant
|
||||||
|
Detect and show potential configuration file errors to users, e.g. incorrect files and duplicate files in material or quality profiles, there are several places to check. Information is stored and communicated to the user to prevent crashing in future.
|
||||||
|
|
||||||
|
*Disable ensure models are kept apart
|
||||||
|
Disable "Ensure models are kept apart" by default due to to a change in preference files.
|
||||||
|
|
||||||
|
*Prepare and monitor QML files
|
||||||
|
Created two separate QML files for the Prepare and Monitor stages.
|
||||||
|
|
||||||
|
*Hide bed temperature
|
||||||
|
Option to hide bed temperature when no heated bed is present. Contributed by ngraziano.
|
||||||
|
|
||||||
|
*Reprap/Marlin GCODE flavor
|
||||||
|
RepRap firmware now lists values for all extruders in the "Filament used" GCODE comment. Contributed by smartavionics.
|
||||||
|
|
||||||
|
*AutoDesk Inventor integration
|
||||||
|
Open AutoDesk inventor files (parts, assemblies, drawings) directly into Ultimaker Cura. Contributed by thopiekar.
|
||||||
|
|
||||||
|
*Blender integration
|
||||||
|
Open Blender files directly into Ultimaker Cura. Contributed by thopiekar.
|
||||||
|
|
||||||
|
*OpenSCAD integration
|
||||||
|
Open OpenSCAD files directly into Ultimaker Cura. Contributed by thopiekar.
|
||||||
|
|
||||||
|
*FreeCAD integration
|
||||||
|
Open FreeCAD files directly into Ultimaker Cura. Contributed by thopiekar.
|
||||||
|
|
||||||
|
*OctoPrint plugin
|
||||||
|
New version of the OctoPrint plugin for Ultimaker Cura. Contributed by fieldOfView.
|
||||||
|
|
||||||
|
*Cura Backups
|
||||||
|
Backup and restore your configuration, including settings, materials and plugins, for use across different systems.
|
||||||
|
|
||||||
|
*MakePrintable
|
||||||
|
New version of the MakePrintable plugin.
|
||||||
|
|
||||||
|
*Compact Prepare sidebar
|
||||||
|
Plugin that replaces the sidebar with a more compact variation of the original sidebar. Nozzle and material dropdowns are combined into a single line, the “Check compatibility” link is removed, extruder selection buttons are downsized, recommended and custom mode selection buttons are moved to a combobox at the top, and margins are tweaked. Contributed by fieldOfView.
|
||||||
|
|
||||||
|
*PauseAtHeight plugin
|
||||||
|
Bug fixes and improvements for PauseAtHeight plugin. Plugin now accounts for raft layers when choosing “Pause of layer no.” Now positions the nozzle at x and y values of the next layer when resuming. Contributed by JPFrancoia.
|
||||||
|
|
||||||
|
*Bug fixes
|
||||||
|
- Prime tower purge fix. Prime tower purge now starts away from the center, minimizing the chance of overextrusion and nozzle obstructions. Contributed by BagelOrb.
|
||||||
|
- Extruder 2 temp via USB. Fixed a bug where temperatures can’t be read for a second extruder via USB. Contributed by kirilledelman.
|
||||||
|
- Move to next object position before bed heat. Print one at a time mode caused waiting for the bed temperature to reach the first layer temperature while the nozzle was still positioned on the top of the last part. This has been fixed so that the nozzle moves to the location of the next part before waiting for heat up. Contributed by smartavionics.
|
||||||
|
- Non-GCODE USB. Fixed a bug where the USB port doesn’t open if printer doesn't support GCODE. Contributed by ohrn.
|
||||||
|
- Improved wall overlap compensation. Minimizes unexpected behavior on overlap lines, providing smoother results. Contributed by BagelOrb.
|
||||||
|
- Configuration/sync. Fixes minor issues with the configuration/sync menu, such as text rendering on some OSX systems and untranslatable text. Contributed by fieldOfView.
|
||||||
|
- Print job name reslice. Fixed behavior where print job name changes back to origin when reslicing.
|
||||||
|
- Discard/keep. Customized settings don't give an 'discard or keep' dialog when changing material.
|
||||||
|
- Message box styling. Fixed bugs related to message box styling, such as the progress bar overlapping the button in the ‘Sending Data’ popup.
|
||||||
|
- Curaproject naming. Fixed bug related to two "curaprojects" in the file name when saving a project.
|
||||||
|
- No support on first layers. Fixed a bug related to no support generated causing failed prints when model is floating above build plate.
|
||||||
|
- False incompatible configuration. Fixed a bug where PrintCore and materials were flagged even though the configurations are compatible.
|
||||||
|
- Spiralize contour overlaps. Fixed a bug related to spiralize contour overlaps.
|
||||||
|
- Model saved outside build volume. Fixed a bug that would saved a model to file (GCODE) outside the build volume.
|
||||||
|
- Filament diameter line width. Adjust filament diameter to calculate line width in the GCODE parser.
|
||||||
|
- Holes in model surfaces. Fixed a bug where illogical travel moves leave holes in the model surface.
|
||||||
|
- Nozzle legacy file variant. Fixed crashes caused by loading legacy nozzle variant files.
|
||||||
|
- Brim wall order. Fixed a bug related to brim wall order. Contributed by smartavionics.
|
||||||
|
- GCODE reader gaps. Fixed a GCODE reader bug that can create a gap at the start of a spiralized layer.
|
||||||
|
- Korean translation. Fixed some typos in Korean translation.
|
||||||
|
- ARM/Mali systems. Graphics pipeline for ARM/Mali fixed. Contributed by jwalt.
|
||||||
|
- NGC Writer. Fixed missing author for NGC Writer plugin.
|
||||||
|
- Support blocker legacy GPU. Fixes depth picking on older GPUs that do not support the 4.1 shading model which caused the support blocker to put cubes in unexpected locations. Contributed by fieldOfView.
|
||||||
|
|
||||||
|
*Third-party printers
|
||||||
|
- Felix Tec4 printer. Updated definitions for Felix Tec4. Contributed by kerog777.
|
||||||
|
- Deltacomb. Updated definitions for Deltacomb. Contributed by kaleidoscopeit.
|
||||||
|
- Rigid3D Mucit. Added definitions for Rigid3D Mucit. Contributed by Rigid3D.
|
||||||
|
|
||||||
[3.3.0]
|
[3.3.0]
|
||||||
|
|
||||||
*Profile for the Ultimaker S5
|
*Profile for the Ultimaker S5
|
||||||
|
|
@ -24,6 +153,9 @@ Refactored machine manager resulted in less manager classes. Changing settings,
|
||||||
*Multiply models faster
|
*Multiply models faster
|
||||||
Significant speed increase when multiplying models.
|
Significant speed increase when multiplying models.
|
||||||
|
|
||||||
|
*Auto slicing disabled by default
|
||||||
|
The auto slice tool is now disabled by default. Users can still enable the feature in the user preferences dialog.
|
||||||
|
|
||||||
*Updated fonts
|
*Updated fonts
|
||||||
Default font changed to NotoSans to increase readability and consistency with Cura Connect.
|
Default font changed to NotoSans to increase readability and consistency with Cura Connect.
|
||||||
|
|
||||||
|
|
@ -63,8 +195,8 @@ Generate a cube mesh to prevent support material generation in specific areas of
|
||||||
*Real bridging - smartavionics
|
*Real bridging - smartavionics
|
||||||
New experimental feature that detects bridges, adjusting the print speed, slow and fan speed to enhance print quality on bridging parts.
|
New experimental feature that detects bridges, adjusting the print speed, slow and fan speed to enhance print quality on bridging parts.
|
||||||
|
|
||||||
*Updated CuraEngine executable - thopiekar
|
*Updated CuraEngine executable - thopiekar & Ultimaker B.V.
|
||||||
The CuraEngine executable now contains a dedicated icon, author information and a license.
|
The CuraEngine executable contains a dedicated icon, author and license info on Windows now. The icon has been designed by Ultimaker B.V.
|
||||||
|
|
||||||
*Use RapidJSON and ClipperLib from system libraries
|
*Use RapidJSON and ClipperLib from system libraries
|
||||||
Application updated to use verified copies of libraries, reducing maintenance time keeping them up to date (the operating system is now responsible), as well as reducing the amount of code shipped (as necessary code is already on the user’s system).
|
Application updated to use verified copies of libraries, reducing maintenance time keeping them up to date (the operating system is now responsible), as well as reducing the amount of code shipped (as necessary code is already on the user’s system).
|
||||||
|
|
|
||||||
|
|
@ -1,39 +1,47 @@
|
||||||
# Copyright (c) 2017 Ultimaker B.V.
|
# Copyright (c) 2018 Ultimaker B.V.
|
||||||
# Cura is released under the terms of the LGPLv3 or higher.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
|
import argparse #To run the engine in debug mode if the front-end is in debug mode.
|
||||||
|
from collections import defaultdict
|
||||||
|
import os
|
||||||
|
from PyQt5.QtCore import QObject, QTimer, pyqtSlot
|
||||||
|
import sys
|
||||||
|
from time import time
|
||||||
|
from typing import Any, cast, Dict, List, Optional, Set, TYPE_CHECKING
|
||||||
|
|
||||||
from UM.Backend.Backend import Backend, BackendState
|
from UM.Backend.Backend import Backend, BackendState
|
||||||
from UM.Application import Application
|
|
||||||
from UM.Scene.SceneNode import SceneNode
|
from UM.Scene.SceneNode import SceneNode
|
||||||
from UM.Preferences import Preferences
|
|
||||||
from UM.Signal import Signal
|
from UM.Signal import Signal
|
||||||
from UM.Logger import Logger
|
from UM.Logger import Logger
|
||||||
from UM.Message import Message
|
from UM.Message import Message
|
||||||
from UM.PluginRegistry import PluginRegistry
|
from UM.PluginRegistry import PluginRegistry
|
||||||
from UM.Resources import Resources
|
from UM.Resources import Resources
|
||||||
from UM.Platform import Platform
|
from UM.Platform import Platform
|
||||||
from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
|
|
||||||
from UM.Qt.Duration import DurationFormat
|
from UM.Qt.Duration import DurationFormat
|
||||||
from PyQt5.QtCore import QObject, pyqtSlot
|
from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
|
||||||
|
from UM.Settings.Interfaces import DefinitionContainerInterface
|
||||||
|
from UM.Settings.SettingInstance import SettingInstance #For typing.
|
||||||
|
from UM.Tool import Tool #For typing.
|
||||||
|
from UM.Mesh.MeshData import MeshData #For typing.
|
||||||
|
|
||||||
from collections import defaultdict
|
from cura.CuraApplication import CuraApplication
|
||||||
from cura.Settings.ExtruderManager import ExtruderManager
|
from cura.Settings.ExtruderManager import ExtruderManager
|
||||||
from . import ProcessSlicedLayersJob
|
from .ProcessSlicedLayersJob import ProcessSlicedLayersJob
|
||||||
from . import StartSliceJob
|
from .StartSliceJob import StartSliceJob, StartJobResult
|
||||||
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
from time import time
|
|
||||||
|
|
||||||
from PyQt5.QtCore import QTimer
|
|
||||||
|
|
||||||
import Arcus
|
import Arcus
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from cura.Machines.Models.MultiBuildPlateModel import MultiBuildPlateModel
|
||||||
|
from cura.Machines.MachineErrorChecker import MachineErrorChecker
|
||||||
|
from UM.Scene.Scene import Scene
|
||||||
|
from UM.Settings.ContainerStack import ContainerStack
|
||||||
|
|
||||||
from UM.i18n import i18nCatalog
|
from UM.i18n import i18nCatalog
|
||||||
catalog = i18nCatalog("cura")
|
catalog = i18nCatalog("cura")
|
||||||
|
|
||||||
|
|
||||||
class CuraEngineBackend(QObject, Backend):
|
class CuraEngineBackend(QObject, Backend):
|
||||||
|
|
||||||
backendError = Signal()
|
backendError = Signal()
|
||||||
|
|
||||||
## Starts the back-end plug-in.
|
## Starts the back-end plug-in.
|
||||||
|
|
@ -41,30 +49,30 @@ class CuraEngineBackend(QObject, Backend):
|
||||||
# This registers all the signal listeners and prepares for communication
|
# This registers all the signal listeners and prepares for communication
|
||||||
# with the back-end in general.
|
# with the back-end in general.
|
||||||
# CuraEngineBackend is exposed to qml as well.
|
# CuraEngineBackend is exposed to qml as well.
|
||||||
def __init__(self, parent = None):
|
def __init__(self) -> None:
|
||||||
super().__init__(parent = parent)
|
super().__init__()
|
||||||
# Find out where the engine is located, and how it is called.
|
# Find out where the engine is located, and how it is called.
|
||||||
# This depends on how Cura is packaged and which OS we are running on.
|
# This depends on how Cura is packaged and which OS we are running on.
|
||||||
executable_name = "CuraEngine"
|
executable_name = "CuraEngine"
|
||||||
if Platform.isWindows():
|
if Platform.isWindows():
|
||||||
executable_name += ".exe"
|
executable_name += ".exe"
|
||||||
default_engine_location = executable_name
|
default_engine_location = executable_name
|
||||||
if os.path.exists(os.path.join(Application.getInstallPrefix(), "bin", executable_name)):
|
if os.path.exists(os.path.join(CuraApplication.getInstallPrefix(), "bin", executable_name)):
|
||||||
default_engine_location = os.path.join(Application.getInstallPrefix(), "bin", executable_name)
|
default_engine_location = os.path.join(CuraApplication.getInstallPrefix(), "bin", executable_name)
|
||||||
if hasattr(sys, "frozen"):
|
if hasattr(sys, "frozen"):
|
||||||
default_engine_location = os.path.join(os.path.dirname(os.path.abspath(sys.executable)), executable_name)
|
default_engine_location = os.path.join(os.path.dirname(os.path.abspath(sys.executable)), executable_name)
|
||||||
if Platform.isLinux() and not default_engine_location:
|
if Platform.isLinux() and not default_engine_location:
|
||||||
if not os.getenv("PATH"):
|
if not os.getenv("PATH"):
|
||||||
raise OSError("There is something wrong with your Linux installation.")
|
raise OSError("There is something wrong with your Linux installation.")
|
||||||
for pathdir in os.getenv("PATH").split(os.pathsep):
|
for pathdir in cast(str, os.getenv("PATH")).split(os.pathsep):
|
||||||
execpath = os.path.join(pathdir, executable_name)
|
execpath = os.path.join(pathdir, executable_name)
|
||||||
if os.path.exists(execpath):
|
if os.path.exists(execpath):
|
||||||
default_engine_location = execpath
|
default_engine_location = execpath
|
||||||
break
|
break
|
||||||
|
|
||||||
self._application = Application.getInstance()
|
self._application = CuraApplication.getInstance() #type: CuraApplication
|
||||||
self._multi_build_plate_model = None
|
self._multi_build_plate_model = None #type: Optional[MultiBuildPlateModel]
|
||||||
self._machine_error_checker = None
|
self._machine_error_checker = None #type: Optional[MachineErrorChecker]
|
||||||
|
|
||||||
if not default_engine_location:
|
if not default_engine_location:
|
||||||
raise EnvironmentError("Could not find CuraEngine")
|
raise EnvironmentError("Could not find CuraEngine")
|
||||||
|
|
@ -72,16 +80,16 @@ class CuraEngineBackend(QObject, Backend):
|
||||||
Logger.log("i", "Found CuraEngine at: %s", default_engine_location)
|
Logger.log("i", "Found CuraEngine at: %s", default_engine_location)
|
||||||
|
|
||||||
default_engine_location = os.path.abspath(default_engine_location)
|
default_engine_location = os.path.abspath(default_engine_location)
|
||||||
Preferences.getInstance().addPreference("backend/location", default_engine_location)
|
self._application.getPreferences().addPreference("backend/location", default_engine_location)
|
||||||
|
|
||||||
# Workaround to disable layer view processing if layer view is not active.
|
# Workaround to disable layer view processing if layer view is not active.
|
||||||
self._layer_view_active = False
|
self._layer_view_active = False #type: bool
|
||||||
self._onActiveViewChanged()
|
self._onActiveViewChanged()
|
||||||
|
|
||||||
self._stored_layer_data = []
|
self._stored_layer_data = [] #type: List[Arcus.PythonMessage]
|
||||||
self._stored_optimized_layer_data = {} # key is build plate number, then arrays are stored until they go to the ProcessSlicesLayersJob
|
self._stored_optimized_layer_data = {} #type: Dict[int, List[Arcus.PythonMessage]] # key is build plate number, then arrays are stored until they go to the ProcessSlicesLayersJob
|
||||||
|
|
||||||
self._scene = self._application.getController().getScene()
|
self._scene = self._application.getController().getScene() #type: Scene
|
||||||
self._scene.sceneChanged.connect(self._onSceneChanged)
|
self._scene.sceneChanged.connect(self._onSceneChanged)
|
||||||
|
|
||||||
# Triggers for auto-slicing. Auto-slicing is triggered as follows:
|
# Triggers for auto-slicing. Auto-slicing is triggered as follows:
|
||||||
|
|
@ -92,7 +100,7 @@ class CuraEngineBackend(QObject, Backend):
|
||||||
# If there is an error check, stop the auto-slicing timer, and only wait for the error check to be finished
|
# If there is an error check, stop the auto-slicing timer, and only wait for the error check to be finished
|
||||||
# to start the auto-slicing timer again.
|
# to start the auto-slicing timer again.
|
||||||
#
|
#
|
||||||
self._global_container_stack = None
|
self._global_container_stack = None #type: Optional[ContainerStack]
|
||||||
|
|
||||||
# Listeners for receiving messages from the back-end.
|
# Listeners for receiving messages from the back-end.
|
||||||
self._message_handlers["cura.proto.Layer"] = self._onLayerMessage
|
self._message_handlers["cura.proto.Layer"] = self._onLayerMessage
|
||||||
|
|
@ -103,43 +111,45 @@ class CuraEngineBackend(QObject, Backend):
|
||||||
self._message_handlers["cura.proto.PrintTimeMaterialEstimates"] = self._onPrintTimeMaterialEstimates
|
self._message_handlers["cura.proto.PrintTimeMaterialEstimates"] = self._onPrintTimeMaterialEstimates
|
||||||
self._message_handlers["cura.proto.SlicingFinished"] = self._onSlicingFinishedMessage
|
self._message_handlers["cura.proto.SlicingFinished"] = self._onSlicingFinishedMessage
|
||||||
|
|
||||||
self._start_slice_job = None
|
self._start_slice_job = None #type: Optional[StartSliceJob]
|
||||||
self._start_slice_job_build_plate = None
|
self._start_slice_job_build_plate = None #type: Optional[int]
|
||||||
self._slicing = False # Are we currently slicing?
|
self._slicing = False #type: bool # Are we currently slicing?
|
||||||
self._restart = False # Back-end is currently restarting?
|
self._restart = False #type: bool # Back-end is currently restarting?
|
||||||
self._tool_active = False # If a tool is active, some tasks do not have to do anything
|
self._tool_active = False #type: bool # If a tool is active, some tasks do not have to do anything
|
||||||
self._always_restart = True # Always restart the engine when starting a new slice. Don't keep the process running. TODO: Fix engine statelessness.
|
self._always_restart = True #type: bool # Always restart the engine when starting a new slice. Don't keep the process running. TODO: Fix engine statelessness.
|
||||||
self._process_layers_job = None # The currently active job to process layers, or None if it is not processing layers.
|
self._process_layers_job = None #type: Optional[ProcessSlicedLayersJob] # The currently active job to process layers, or None if it is not processing layers.
|
||||||
self._build_plates_to_be_sliced = [] # what needs slicing?
|
self._build_plates_to_be_sliced = [] #type: List[int] # what needs slicing?
|
||||||
self._engine_is_fresh = True # Is the newly started engine used before or not?
|
self._engine_is_fresh = True #type: bool # Is the newly started engine used before or not?
|
||||||
|
|
||||||
self._backend_log_max_lines = 20000 # Maximum number of lines to buffer
|
self._backend_log_max_lines = 20000 #type: int # Maximum number of lines to buffer
|
||||||
self._error_message = None # Pop-up message that shows errors.
|
self._error_message = None #type: Optional[Message] # Pop-up message that shows errors.
|
||||||
self._last_num_objects = defaultdict(int) # Count number of objects to see if there is something changed
|
self._last_num_objects = defaultdict(int) #type: Dict[int, int] # Count number of objects to see if there is something changed
|
||||||
self._postponed_scene_change_sources = [] # scene change is postponed (by a tool)
|
self._postponed_scene_change_sources = [] #type: List[SceneNode] # scene change is postponed (by a tool)
|
||||||
|
|
||||||
self._slice_start_time = None
|
self._slice_start_time = None #type: Optional[float]
|
||||||
self._is_disabled = False
|
self._is_disabled = False #type: bool
|
||||||
|
|
||||||
Preferences.getInstance().addPreference("general/auto_slice", False)
|
self._application.getPreferences().addPreference("general/auto_slice", False)
|
||||||
|
|
||||||
self._use_timer = False
|
self._use_timer = False #type: bool
|
||||||
# When you update a setting and other settings get changed through inheritance, many propertyChanged signals are fired.
|
# When you update a setting and other settings get changed through inheritance, many propertyChanged signals are fired.
|
||||||
# This timer will group them up, and only slice for the last setting changed signal.
|
# This timer will group them up, and only slice for the last setting changed signal.
|
||||||
# TODO: Properly group propertyChanged signals by whether they are triggered by the same user interaction.
|
# TODO: Properly group propertyChanged signals by whether they are triggered by the same user interaction.
|
||||||
self._change_timer = QTimer()
|
self._change_timer = QTimer() #type: QTimer
|
||||||
self._change_timer.setSingleShot(True)
|
self._change_timer.setSingleShot(True)
|
||||||
self._change_timer.setInterval(500)
|
self._change_timer.setInterval(500)
|
||||||
self.determineAutoSlicing()
|
self.determineAutoSlicing()
|
||||||
Preferences.getInstance().preferenceChanged.connect(self._onPreferencesChanged)
|
self._application.getPreferences().preferenceChanged.connect(self._onPreferencesChanged)
|
||||||
|
|
||||||
self._application.initializationFinished.connect(self.initialize)
|
self._application.initializationFinished.connect(self.initialize)
|
||||||
|
|
||||||
def initialize(self):
|
def initialize(self) -> None:
|
||||||
self._multi_build_plate_model = self._application.getMultiBuildPlateModel()
|
self._multi_build_plate_model = self._application.getMultiBuildPlateModel()
|
||||||
|
|
||||||
self._application.getController().activeViewChanged.connect(self._onActiveViewChanged)
|
self._application.getController().activeViewChanged.connect(self._onActiveViewChanged)
|
||||||
self._multi_build_plate_model.activeBuildPlateChanged.connect(self._onActiveViewChanged)
|
|
||||||
|
if self._multi_build_plate_model:
|
||||||
|
self._multi_build_plate_model.activeBuildPlateChanged.connect(self._onActiveViewChanged)
|
||||||
|
|
||||||
self._application.globalContainerStackChanged.connect(self._onGlobalStackChanged)
|
self._application.globalContainerStackChanged.connect(self._onGlobalStackChanged)
|
||||||
self._onGlobalStackChanged()
|
self._onGlobalStackChanged()
|
||||||
|
|
@ -161,16 +171,24 @@ class CuraEngineBackend(QObject, Backend):
|
||||||
#
|
#
|
||||||
# This function should terminate the engine process.
|
# This function should terminate the engine process.
|
||||||
# Called when closing the application.
|
# Called when closing the application.
|
||||||
def close(self):
|
def close(self) -> None:
|
||||||
# Terminate CuraEngine if it is still running at this point
|
# Terminate CuraEngine if it is still running at this point
|
||||||
self._terminate()
|
self._terminate()
|
||||||
|
|
||||||
## Get the command that is used to call the engine.
|
## Get the command that is used to call the engine.
|
||||||
# This is useful for debugging and used to actually start the engine.
|
# This is useful for debugging and used to actually start the engine.
|
||||||
# \return list of commands and args / parameters.
|
# \return list of commands and args / parameters.
|
||||||
def getEngineCommand(self):
|
def getEngineCommand(self) -> List[str]:
|
||||||
json_path = Resources.getPath(Resources.DefinitionContainers, "fdmprinter.def.json")
|
json_path = Resources.getPath(Resources.DefinitionContainers, "fdmprinter.def.json")
|
||||||
return [Preferences.getInstance().getValue("backend/location"), "connect", "127.0.0.1:{0}".format(self._port), "-j", json_path, ""]
|
command = [self._application.getPreferences().getValue("backend/location"), "connect", "127.0.0.1:{0}".format(self._port), "-j", json_path, ""]
|
||||||
|
|
||||||
|
parser = argparse.ArgumentParser(prog = "cura", add_help = False)
|
||||||
|
parser.add_argument("--debug", action = "store_true", default = False, help = "Turn on the debug mode by setting this option.")
|
||||||
|
known_args = vars(parser.parse_known_args()[0])
|
||||||
|
if known_args["debug"]:
|
||||||
|
command.append("-vvv")
|
||||||
|
|
||||||
|
return command
|
||||||
|
|
||||||
## Emitted when we get a message containing print duration and material amount.
|
## Emitted when we get a message containing print duration and material amount.
|
||||||
# This also implies the slicing has finished.
|
# This also implies the slicing has finished.
|
||||||
|
|
@ -185,13 +203,13 @@ class CuraEngineBackend(QObject, Backend):
|
||||||
slicingCancelled = Signal()
|
slicingCancelled = Signal()
|
||||||
|
|
||||||
@pyqtSlot()
|
@pyqtSlot()
|
||||||
def stopSlicing(self):
|
def stopSlicing(self) -> None:
|
||||||
self.backendStateChange.emit(BackendState.NotStarted)
|
self.backendStateChange.emit(BackendState.NotStarted)
|
||||||
if self._slicing: # We were already slicing. Stop the old job.
|
if self._slicing: # We were already slicing. Stop the old job.
|
||||||
self._terminate()
|
self._terminate()
|
||||||
self._createSocket()
|
self._createSocket()
|
||||||
|
|
||||||
if self._process_layers_job: # We were processing layers. Stop that, the layers are going to change soon.
|
if self._process_layers_job is not None: # We were processing layers. Stop that, the layers are going to change soon.
|
||||||
Logger.log("d", "Aborting process layers job...")
|
Logger.log("d", "Aborting process layers job...")
|
||||||
self._process_layers_job.abort()
|
self._process_layers_job.abort()
|
||||||
self._process_layers_job = None
|
self._process_layers_job = None
|
||||||
|
|
@ -201,12 +219,12 @@ class CuraEngineBackend(QObject, Backend):
|
||||||
|
|
||||||
## Manually triggers a reslice
|
## Manually triggers a reslice
|
||||||
@pyqtSlot()
|
@pyqtSlot()
|
||||||
def forceSlice(self):
|
def forceSlice(self) -> None:
|
||||||
self.markSliceAll()
|
self.markSliceAll()
|
||||||
self.slice()
|
self.slice()
|
||||||
|
|
||||||
## Perform a slice of the scene.
|
## Perform a slice of the scene.
|
||||||
def slice(self):
|
def slice(self) -> None:
|
||||||
Logger.log("d", "Starting to slice...")
|
Logger.log("d", "Starting to slice...")
|
||||||
self._slice_start_time = time()
|
self._slice_start_time = time()
|
||||||
if not self._build_plates_to_be_sliced:
|
if not self._build_plates_to_be_sliced:
|
||||||
|
|
@ -219,10 +237,10 @@ class CuraEngineBackend(QObject, Backend):
|
||||||
return
|
return
|
||||||
|
|
||||||
if not hasattr(self._scene, "gcode_dict"):
|
if not hasattr(self._scene, "gcode_dict"):
|
||||||
self._scene.gcode_dict = {}
|
self._scene.gcode_dict = {} #type: ignore #Because we are creating the missing attribute here.
|
||||||
|
|
||||||
# see if we really have to slice
|
# see if we really have to slice
|
||||||
active_build_plate = Application.getInstance().getMultiBuildPlateModel().activeBuildPlate
|
active_build_plate = self._application.getMultiBuildPlateModel().activeBuildPlate
|
||||||
build_plate_to_be_sliced = self._build_plates_to_be_sliced.pop(0)
|
build_plate_to_be_sliced = self._build_plates_to_be_sliced.pop(0)
|
||||||
Logger.log("d", "Going to slice build plate [%s]!" % build_plate_to_be_sliced)
|
Logger.log("d", "Going to slice build plate [%s]!" % build_plate_to_be_sliced)
|
||||||
num_objects = self._numObjectsPerBuildPlate()
|
num_objects = self._numObjectsPerBuildPlate()
|
||||||
|
|
@ -231,16 +249,16 @@ class CuraEngineBackend(QObject, Backend):
|
||||||
self._stored_optimized_layer_data[build_plate_to_be_sliced] = []
|
self._stored_optimized_layer_data[build_plate_to_be_sliced] = []
|
||||||
|
|
||||||
if build_plate_to_be_sliced not in num_objects or num_objects[build_plate_to_be_sliced] == 0:
|
if build_plate_to_be_sliced not in num_objects or num_objects[build_plate_to_be_sliced] == 0:
|
||||||
self._scene.gcode_dict[build_plate_to_be_sliced] = []
|
self._scene.gcode_dict[build_plate_to_be_sliced] = [] #type: ignore #Because we created this attribute above.
|
||||||
Logger.log("d", "Build plate %s has no objects to be sliced, skipping", build_plate_to_be_sliced)
|
Logger.log("d", "Build plate %s has no objects to be sliced, skipping", build_plate_to_be_sliced)
|
||||||
if self._build_plates_to_be_sliced:
|
if self._build_plates_to_be_sliced:
|
||||||
self.slice()
|
self.slice()
|
||||||
return
|
return
|
||||||
|
|
||||||
if Application.getInstance().getPrintInformation() and build_plate_to_be_sliced == active_build_plate:
|
if self._application.getPrintInformation() and build_plate_to_be_sliced == active_build_plate:
|
||||||
Application.getInstance().getPrintInformation().setToZeroPrintInformation(build_plate_to_be_sliced)
|
self._application.getPrintInformation().setToZeroPrintInformation(build_plate_to_be_sliced)
|
||||||
|
|
||||||
if self._process is None:
|
if self._process is None: # type: ignore
|
||||||
self._createSocket()
|
self._createSocket()
|
||||||
self.stopSlicing()
|
self.stopSlicing()
|
||||||
self._engine_is_fresh = False # Yes we're going to use the engine
|
self._engine_is_fresh = False # Yes we're going to use the engine
|
||||||
|
|
@ -248,14 +266,14 @@ class CuraEngineBackend(QObject, Backend):
|
||||||
self.processingProgress.emit(0.0)
|
self.processingProgress.emit(0.0)
|
||||||
self.backendStateChange.emit(BackendState.NotStarted)
|
self.backendStateChange.emit(BackendState.NotStarted)
|
||||||
|
|
||||||
self._scene.gcode_dict[build_plate_to_be_sliced] = [] #[] indexed by build plate number
|
self._scene.gcode_dict[build_plate_to_be_sliced] = [] #type: ignore #[] indexed by build plate number
|
||||||
self._slicing = True
|
self._slicing = True
|
||||||
self.slicingStarted.emit()
|
self.slicingStarted.emit()
|
||||||
|
|
||||||
self.determineAutoSlicing() # Switch timer on or off if appropriate
|
self.determineAutoSlicing() # Switch timer on or off if appropriate
|
||||||
|
|
||||||
slice_message = self._socket.createMessage("cura.proto.Slice")
|
slice_message = self._socket.createMessage("cura.proto.Slice")
|
||||||
self._start_slice_job = StartSliceJob.StartSliceJob(slice_message)
|
self._start_slice_job = StartSliceJob(slice_message)
|
||||||
self._start_slice_job_build_plate = build_plate_to_be_sliced
|
self._start_slice_job_build_plate = build_plate_to_be_sliced
|
||||||
self._start_slice_job.setBuildPlate(self._start_slice_job_build_plate)
|
self._start_slice_job.setBuildPlate(self._start_slice_job_build_plate)
|
||||||
self._start_slice_job.start()
|
self._start_slice_job.start()
|
||||||
|
|
@ -263,7 +281,7 @@ class CuraEngineBackend(QObject, Backend):
|
||||||
|
|
||||||
## Terminate the engine process.
|
## Terminate the engine process.
|
||||||
# Start the engine process by calling _createSocket()
|
# Start the engine process by calling _createSocket()
|
||||||
def _terminate(self):
|
def _terminate(self) -> None:
|
||||||
self._slicing = False
|
self._slicing = False
|
||||||
self._stored_layer_data = []
|
self._stored_layer_data = []
|
||||||
if self._start_slice_job_build_plate in self._stored_optimized_layer_data:
|
if self._start_slice_job_build_plate in self._stored_optimized_layer_data:
|
||||||
|
|
@ -275,15 +293,15 @@ class CuraEngineBackend(QObject, Backend):
|
||||||
self.processingProgress.emit(0)
|
self.processingProgress.emit(0)
|
||||||
Logger.log("d", "Attempting to kill the engine process")
|
Logger.log("d", "Attempting to kill the engine process")
|
||||||
|
|
||||||
if Application.getInstance().getCommandLineOption("external-backend", False):
|
if self._application.getUseExternalBackend():
|
||||||
return
|
return
|
||||||
|
|
||||||
if self._process is not None:
|
if self._process is not None: # type: ignore
|
||||||
Logger.log("d", "Killing engine process")
|
Logger.log("d", "Killing engine process")
|
||||||
try:
|
try:
|
||||||
self._process.terminate()
|
self._process.terminate() # type: ignore
|
||||||
Logger.log("d", "Engine process is killed. Received return code %s", self._process.wait())
|
Logger.log("d", "Engine process is killed. Received return code %s", self._process.wait()) # type: ignore
|
||||||
self._process = None
|
self._process = None # type: ignore
|
||||||
|
|
||||||
except Exception as e: # terminating a process that is already terminating causes an exception, silently ignore this.
|
except Exception as e: # terminating a process that is already terminating causes an exception, silently ignore this.
|
||||||
Logger.log("d", "Exception occurred while trying to kill the engine %s", str(e))
|
Logger.log("d", "Exception occurred while trying to kill the engine %s", str(e))
|
||||||
|
|
@ -296,7 +314,7 @@ class CuraEngineBackend(QObject, Backend):
|
||||||
# bootstrapping of a slice job.
|
# bootstrapping of a slice job.
|
||||||
#
|
#
|
||||||
# \param job The start slice job that was just finished.
|
# \param job The start slice job that was just finished.
|
||||||
def _onStartSliceCompleted(self, job):
|
def _onStartSliceCompleted(self, job: StartSliceJob) -> None:
|
||||||
if self._error_message:
|
if self._error_message:
|
||||||
self._error_message.hide()
|
self._error_message.hide()
|
||||||
|
|
||||||
|
|
@ -304,13 +322,13 @@ class CuraEngineBackend(QObject, Backend):
|
||||||
if self._start_slice_job is job:
|
if self._start_slice_job is job:
|
||||||
self._start_slice_job = None
|
self._start_slice_job = None
|
||||||
|
|
||||||
if job.isCancelled() or job.getError() or job.getResult() == StartSliceJob.StartJobResult.Error:
|
if job.isCancelled() or job.getError() or job.getResult() == StartJobResult.Error:
|
||||||
self.backendStateChange.emit(BackendState.Error)
|
self.backendStateChange.emit(BackendState.Error)
|
||||||
self.backendError.emit(job)
|
self.backendError.emit(job)
|
||||||
return
|
return
|
||||||
|
|
||||||
if job.getResult() == StartSliceJob.StartJobResult.MaterialIncompatible:
|
if job.getResult() == StartJobResult.MaterialIncompatible:
|
||||||
if Application.getInstance().platformActivity:
|
if self._application.platformActivity:
|
||||||
self._error_message = Message(catalog.i18nc("@info:status",
|
self._error_message = Message(catalog.i18nc("@info:status",
|
||||||
"Unable to slice with the current material as it is incompatible with the selected machine or configuration."), title = catalog.i18nc("@info:title", "Unable to slice"))
|
"Unable to slice with the current material as it is incompatible with the selected machine or configuration."), title = catalog.i18nc("@info:title", "Unable to slice"))
|
||||||
self._error_message.show()
|
self._error_message.show()
|
||||||
|
|
@ -320,10 +338,13 @@ class CuraEngineBackend(QObject, Backend):
|
||||||
self.backendStateChange.emit(BackendState.NotStarted)
|
self.backendStateChange.emit(BackendState.NotStarted)
|
||||||
return
|
return
|
||||||
|
|
||||||
if job.getResult() == StartSliceJob.StartJobResult.SettingError:
|
if job.getResult() == StartJobResult.SettingError:
|
||||||
if Application.getInstance().platformActivity:
|
if self._application.platformActivity:
|
||||||
|
if not self._global_container_stack:
|
||||||
|
Logger.log("w", "Global container stack not assigned to CuraEngineBackend!")
|
||||||
|
return
|
||||||
extruders = list(ExtruderManager.getInstance().getMachineExtruders(self._global_container_stack.getId()))
|
extruders = list(ExtruderManager.getInstance().getMachineExtruders(self._global_container_stack.getId()))
|
||||||
error_keys = []
|
error_keys = [] #type: List[str]
|
||||||
for extruder in extruders:
|
for extruder in extruders:
|
||||||
error_keys.extend(extruder.getErrorKeys())
|
error_keys.extend(extruder.getErrorKeys())
|
||||||
if not extruders:
|
if not extruders:
|
||||||
|
|
@ -331,7 +352,7 @@ class CuraEngineBackend(QObject, Backend):
|
||||||
error_labels = set()
|
error_labels = set()
|
||||||
for key in error_keys:
|
for key in error_keys:
|
||||||
for stack in [self._global_container_stack] + extruders: #Search all container stacks for the definition of this setting. Some are only in an extruder stack.
|
for stack in [self._global_container_stack] + extruders: #Search all container stacks for the definition of this setting. Some are only in an extruder stack.
|
||||||
definitions = stack.getBottom().findDefinitions(key = key)
|
definitions = cast(DefinitionContainerInterface, stack.getBottom()).findDefinitions(key = key)
|
||||||
if definitions:
|
if definitions:
|
||||||
break #Found it! No need to continue search.
|
break #Found it! No need to continue search.
|
||||||
else: #No stack has a definition for this setting.
|
else: #No stack has a definition for this setting.
|
||||||
|
|
@ -339,8 +360,7 @@ class CuraEngineBackend(QObject, Backend):
|
||||||
continue
|
continue
|
||||||
error_labels.add(definitions[0].label)
|
error_labels.add(definitions[0].label)
|
||||||
|
|
||||||
error_labels = ", ".join(error_labels)
|
self._error_message = Message(catalog.i18nc("@info:status", "Unable to slice with the current settings. The following settings have errors: {0}").format(", ".join(error_labels)),
|
||||||
self._error_message = Message(catalog.i18nc("@info:status", "Unable to slice with the current settings. The following settings have errors: {0}").format(error_labels),
|
|
||||||
title = catalog.i18nc("@info:title", "Unable to slice"))
|
title = catalog.i18nc("@info:title", "Unable to slice"))
|
||||||
self._error_message.show()
|
self._error_message.show()
|
||||||
self.backendStateChange.emit(BackendState.Error)
|
self.backendStateChange.emit(BackendState.Error)
|
||||||
|
|
@ -349,29 +369,30 @@ class CuraEngineBackend(QObject, Backend):
|
||||||
self.backendStateChange.emit(BackendState.NotStarted)
|
self.backendStateChange.emit(BackendState.NotStarted)
|
||||||
return
|
return
|
||||||
|
|
||||||
elif job.getResult() == StartSliceJob.StartJobResult.ObjectSettingError:
|
elif job.getResult() == StartJobResult.ObjectSettingError:
|
||||||
errors = {}
|
errors = {}
|
||||||
for node in DepthFirstIterator(Application.getInstance().getController().getScene().getRoot()):
|
for node in DepthFirstIterator(self._application.getController().getScene().getRoot()): #type: ignore #Ignore type error because iter() should get called automatically by Python syntax.
|
||||||
stack = node.callDecoration("getStack")
|
stack = node.callDecoration("getStack")
|
||||||
if not stack:
|
if not stack:
|
||||||
continue
|
continue
|
||||||
for key in stack.getErrorKeys():
|
for key in stack.getErrorKeys():
|
||||||
definition = self._global_container_stack.getBottom().findDefinitions(key = key)
|
if not self._global_container_stack:
|
||||||
|
Logger.log("e", "CuraEngineBackend does not have global_container_stack assigned.")
|
||||||
|
continue
|
||||||
|
definition = cast(DefinitionContainerInterface, self._global_container_stack.getBottom()).findDefinitions(key = key)
|
||||||
if not definition:
|
if not definition:
|
||||||
Logger.log("e", "When checking settings for errors, unable to find definition for key {key} in per-object stack.".format(key = key))
|
Logger.log("e", "When checking settings for errors, unable to find definition for key {key} in per-object stack.".format(key = key))
|
||||||
continue
|
continue
|
||||||
definition = definition[0]
|
errors[key] = definition[0].label
|
||||||
errors[key] = definition.label
|
self._error_message = Message(catalog.i18nc("@info:status", "Unable to slice due to some per-model settings. The following settings have errors on one or more models: {error_labels}").format(error_labels = ", ".join(errors.values())),
|
||||||
error_labels = ", ".join(errors.values())
|
|
||||||
self._error_message = Message(catalog.i18nc("@info:status", "Unable to slice due to some per-model settings. The following settings have errors on one or more models: {error_labels}").format(error_labels = error_labels),
|
|
||||||
title = catalog.i18nc("@info:title", "Unable to slice"))
|
title = catalog.i18nc("@info:title", "Unable to slice"))
|
||||||
self._error_message.show()
|
self._error_message.show()
|
||||||
self.backendStateChange.emit(BackendState.Error)
|
self.backendStateChange.emit(BackendState.Error)
|
||||||
self.backendError.emit(job)
|
self.backendError.emit(job)
|
||||||
return
|
return
|
||||||
|
|
||||||
if job.getResult() == StartSliceJob.StartJobResult.BuildPlateError:
|
if job.getResult() == StartJobResult.BuildPlateError:
|
||||||
if Application.getInstance().platformActivity:
|
if self._application.platformActivity:
|
||||||
self._error_message = Message(catalog.i18nc("@info:status", "Unable to slice because the prime tower or prime position(s) are invalid."),
|
self._error_message = Message(catalog.i18nc("@info:status", "Unable to slice because the prime tower or prime position(s) are invalid."),
|
||||||
title = catalog.i18nc("@info:title", "Unable to slice"))
|
title = catalog.i18nc("@info:title", "Unable to slice"))
|
||||||
self._error_message.show()
|
self._error_message.show()
|
||||||
|
|
@ -380,8 +401,16 @@ class CuraEngineBackend(QObject, Backend):
|
||||||
else:
|
else:
|
||||||
self.backendStateChange.emit(BackendState.NotStarted)
|
self.backendStateChange.emit(BackendState.NotStarted)
|
||||||
|
|
||||||
if job.getResult() == StartSliceJob.StartJobResult.NothingToSlice:
|
if job.getResult() == StartJobResult.ObjectsWithDisabledExtruder:
|
||||||
if Application.getInstance().platformActivity:
|
self._error_message = Message(catalog.i18nc("@info:status", "Unable to slice because there are objects associated with disabled Extruder %s." % job.getMessage()),
|
||||||
|
title = catalog.i18nc("@info:title", "Unable to slice"))
|
||||||
|
self._error_message.show()
|
||||||
|
self.backendStateChange.emit(BackendState.Error)
|
||||||
|
self.backendError.emit(job)
|
||||||
|
return
|
||||||
|
|
||||||
|
if job.getResult() == StartJobResult.NothingToSlice:
|
||||||
|
if self._application.platformActivity:
|
||||||
self._error_message = Message(catalog.i18nc("@info:status", "Nothing to slice because none of the models fit the build volume. Please scale or rotate models to fit."),
|
self._error_message = Message(catalog.i18nc("@info:status", "Nothing to slice because none of the models fit the build volume. Please scale or rotate models to fit."),
|
||||||
title = catalog.i18nc("@info:title", "Unable to slice"))
|
title = catalog.i18nc("@info:title", "Unable to slice"))
|
||||||
self._error_message.show()
|
self._error_message.show()
|
||||||
|
|
@ -398,26 +427,27 @@ class CuraEngineBackend(QObject, Backend):
|
||||||
# Notify the user that it's now up to the backend to do it's job
|
# Notify the user that it's now up to the backend to do it's job
|
||||||
self.backendStateChange.emit(BackendState.Processing)
|
self.backendStateChange.emit(BackendState.Processing)
|
||||||
|
|
||||||
Logger.log("d", "Sending slice message took %s seconds", time() - self._slice_start_time )
|
if self._slice_start_time:
|
||||||
|
Logger.log("d", "Sending slice message took %s seconds", time() - self._slice_start_time )
|
||||||
|
|
||||||
## Determine enable or disable auto slicing. Return True for enable timer and False otherwise.
|
## Determine enable or disable auto slicing. Return True for enable timer and False otherwise.
|
||||||
# It disables when
|
# It disables when
|
||||||
# - preference auto slice is off
|
# - preference auto slice is off
|
||||||
# - decorator isBlockSlicing is found (used in g-code reader)
|
# - decorator isBlockSlicing is found (used in g-code reader)
|
||||||
def determineAutoSlicing(self):
|
def determineAutoSlicing(self) -> bool:
|
||||||
enable_timer = True
|
enable_timer = True
|
||||||
self._is_disabled = False
|
self._is_disabled = False
|
||||||
|
|
||||||
if not Preferences.getInstance().getValue("general/auto_slice"):
|
if not self._application.getPreferences().getValue("general/auto_slice"):
|
||||||
enable_timer = False
|
enable_timer = False
|
||||||
for node in DepthFirstIterator(self._scene.getRoot()):
|
for node in DepthFirstIterator(self._scene.getRoot()): #type: ignore #Ignore type error because iter() should get called automatically by Python syntax.
|
||||||
if node.callDecoration("isBlockSlicing"):
|
if node.callDecoration("isBlockSlicing"):
|
||||||
enable_timer = False
|
enable_timer = False
|
||||||
self.backendStateChange.emit(BackendState.Disabled)
|
self.backendStateChange.emit(BackendState.Disabled)
|
||||||
self._is_disabled = True
|
self._is_disabled = True
|
||||||
gcode_list = node.callDecoration("getGCodeList")
|
gcode_list = node.callDecoration("getGCodeList")
|
||||||
if gcode_list is not None:
|
if gcode_list is not None:
|
||||||
self._scene.gcode_dict[node.callDecoration("getBuildPlateNumber")] = gcode_list
|
self._scene.gcode_dict[node.callDecoration("getBuildPlateNumber")] = gcode_list #type: ignore #Because we generate this attribute dynamically.
|
||||||
|
|
||||||
if self._use_timer == enable_timer:
|
if self._use_timer == enable_timer:
|
||||||
return self._use_timer
|
return self._use_timer
|
||||||
|
|
@ -430,13 +460,14 @@ class CuraEngineBackend(QObject, Backend):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
## Return a dict with number of objects per build plate
|
## Return a dict with number of objects per build plate
|
||||||
def _numObjectsPerBuildPlate(self):
|
def _numObjectsPerBuildPlate(self) -> Dict[int, int]:
|
||||||
num_objects = defaultdict(int)
|
num_objects = defaultdict(int) #type: Dict[int, int]
|
||||||
for node in DepthFirstIterator(self._scene.getRoot()):
|
for node in DepthFirstIterator(self._scene.getRoot()): #type: ignore #Ignore type error because iter() should get called automatically by Python syntax.
|
||||||
# Only count sliceable objects
|
# Only count sliceable objects
|
||||||
if node.callDecoration("isSliceable"):
|
if node.callDecoration("isSliceable"):
|
||||||
build_plate_number = node.callDecoration("getBuildPlateNumber")
|
build_plate_number = node.callDecoration("getBuildPlateNumber")
|
||||||
num_objects[build_plate_number] += 1
|
if build_plate_number is not None:
|
||||||
|
num_objects[build_plate_number] += 1
|
||||||
return num_objects
|
return num_objects
|
||||||
|
|
||||||
## Listener for when the scene has changed.
|
## Listener for when the scene has changed.
|
||||||
|
|
@ -444,7 +475,7 @@ class CuraEngineBackend(QObject, Backend):
|
||||||
# This should start a slice if the scene is now ready to slice.
|
# This should start a slice if the scene is now ready to slice.
|
||||||
#
|
#
|
||||||
# \param source The scene node that was changed.
|
# \param source The scene node that was changed.
|
||||||
def _onSceneChanged(self, source):
|
def _onSceneChanged(self, source: SceneNode) -> None:
|
||||||
if not isinstance(source, SceneNode):
|
if not isinstance(source, SceneNode):
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
@ -465,15 +496,14 @@ class CuraEngineBackend(QObject, Backend):
|
||||||
else:
|
else:
|
||||||
# we got a single scenenode
|
# we got a single scenenode
|
||||||
if not source.callDecoration("isGroup"):
|
if not source.callDecoration("isGroup"):
|
||||||
if source.getMeshData() is None:
|
mesh_data = source.getMeshData()
|
||||||
return
|
if mesh_data is None or mesh_data.getVertices() is None:
|
||||||
if source.getMeshData().getVertices() is None:
|
|
||||||
return
|
return
|
||||||
|
|
||||||
build_plate_changed.add(source_build_plate_number)
|
# There are some SceneNodes that do not have any build plate associated, then do not add to the list.
|
||||||
|
if source_build_plate_number is not None:
|
||||||
|
build_plate_changed.add(source_build_plate_number)
|
||||||
|
|
||||||
build_plate_changed.discard(None)
|
|
||||||
build_plate_changed.discard(-1) # object not on build plate
|
|
||||||
if not build_plate_changed:
|
if not build_plate_changed:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
@ -499,8 +529,8 @@ class CuraEngineBackend(QObject, Backend):
|
||||||
## Called when an error occurs in the socket connection towards the engine.
|
## Called when an error occurs in the socket connection towards the engine.
|
||||||
#
|
#
|
||||||
# \param error The exception that occurred.
|
# \param error The exception that occurred.
|
||||||
def _onSocketError(self, error):
|
def _onSocketError(self, error: Arcus.Error) -> None:
|
||||||
if Application.getInstance().isShuttingDown():
|
if self._application.isShuttingDown():
|
||||||
return
|
return
|
||||||
|
|
||||||
super()._onSocketError(error)
|
super()._onSocketError(error)
|
||||||
|
|
@ -513,20 +543,28 @@ class CuraEngineBackend(QObject, Backend):
|
||||||
if error.getErrorCode() not in [Arcus.ErrorCode.BindFailedError, Arcus.ErrorCode.ConnectionResetError, Arcus.ErrorCode.Debug]:
|
if error.getErrorCode() not in [Arcus.ErrorCode.BindFailedError, Arcus.ErrorCode.ConnectionResetError, Arcus.ErrorCode.Debug]:
|
||||||
Logger.log("w", "A socket error caused the connection to be reset")
|
Logger.log("w", "A socket error caused the connection to be reset")
|
||||||
|
|
||||||
|
# _terminate()' function sets the job status to 'cancel', after reconnecting to another Port the job status
|
||||||
|
# needs to be updated. Otherwise backendState is "Unable To Slice"
|
||||||
|
if error.getErrorCode() == Arcus.ErrorCode.BindFailedError and self._start_slice_job is not None:
|
||||||
|
self._start_slice_job.setIsCancelled(False)
|
||||||
|
|
||||||
## Remove old layer data (if any)
|
## Remove old layer data (if any)
|
||||||
def _clearLayerData(self, build_plate_numbers = set()):
|
def _clearLayerData(self, build_plate_numbers: Set = None) -> None:
|
||||||
for node in DepthFirstIterator(self._scene.getRoot()):
|
# Clear out any old gcode
|
||||||
|
self._scene.gcode_dict = {} # type: ignore
|
||||||
|
|
||||||
|
for node in DepthFirstIterator(self._scene.getRoot()): #type: ignore #Ignore type error because iter() should get called automatically by Python syntax.
|
||||||
if node.callDecoration("getLayerData"):
|
if node.callDecoration("getLayerData"):
|
||||||
if not build_plate_numbers or node.callDecoration("getBuildPlateNumber") in build_plate_numbers:
|
if not build_plate_numbers or node.callDecoration("getBuildPlateNumber") in build_plate_numbers:
|
||||||
node.getParent().removeChild(node)
|
node.getParent().removeChild(node)
|
||||||
|
|
||||||
def markSliceAll(self):
|
def markSliceAll(self) -> None:
|
||||||
for build_plate_number in range(Application.getInstance().getMultiBuildPlateModel().maxBuildPlate + 1):
|
for build_plate_number in range(self._application.getMultiBuildPlateModel().maxBuildPlate + 1):
|
||||||
if build_plate_number not in self._build_plates_to_be_sliced:
|
if build_plate_number not in self._build_plates_to_be_sliced:
|
||||||
self._build_plates_to_be_sliced.append(build_plate_number)
|
self._build_plates_to_be_sliced.append(build_plate_number)
|
||||||
|
|
||||||
## Convenient function: mark everything to slice, emit state and clear layer data
|
## Convenient function: mark everything to slice, emit state and clear layer data
|
||||||
def needsSlicing(self):
|
def needsSlicing(self) -> None:
|
||||||
self.stopSlicing()
|
self.stopSlicing()
|
||||||
self.markSliceAll()
|
self.markSliceAll()
|
||||||
self.processingProgress.emit(0.0)
|
self.processingProgress.emit(0.0)
|
||||||
|
|
@ -538,7 +576,7 @@ class CuraEngineBackend(QObject, Backend):
|
||||||
## A setting has changed, so check if we must reslice.
|
## A setting has changed, so check if we must reslice.
|
||||||
# \param instance The setting instance that has changed.
|
# \param instance The setting instance that has changed.
|
||||||
# \param property The property of the setting instance that has changed.
|
# \param property The property of the setting instance that has changed.
|
||||||
def _onSettingChanged(self, instance, property):
|
def _onSettingChanged(self, instance: SettingInstance, property: str) -> None:
|
||||||
if property == "value": # Only reslice if the value has changed.
|
if property == "value": # Only reslice if the value has changed.
|
||||||
self.needsSlicing()
|
self.needsSlicing()
|
||||||
self._onChanged()
|
self._onChanged()
|
||||||
|
|
@ -547,7 +585,7 @@ class CuraEngineBackend(QObject, Backend):
|
||||||
if self._use_timer:
|
if self._use_timer:
|
||||||
self._change_timer.stop()
|
self._change_timer.stop()
|
||||||
|
|
||||||
def _onStackErrorCheckFinished(self):
|
def _onStackErrorCheckFinished(self) -> None:
|
||||||
self.determineAutoSlicing()
|
self.determineAutoSlicing()
|
||||||
if self._is_disabled:
|
if self._is_disabled:
|
||||||
return
|
return
|
||||||
|
|
@ -559,25 +597,26 @@ class CuraEngineBackend(QObject, Backend):
|
||||||
## Called when a sliced layer data message is received from the engine.
|
## Called when a sliced layer data message is received from the engine.
|
||||||
#
|
#
|
||||||
# \param message The protobuf message containing sliced layer data.
|
# \param message The protobuf message containing sliced layer data.
|
||||||
def _onLayerMessage(self, message):
|
def _onLayerMessage(self, message: Arcus.PythonMessage) -> None:
|
||||||
self._stored_layer_data.append(message)
|
self._stored_layer_data.append(message)
|
||||||
|
|
||||||
## Called when an optimized sliced layer data message is received from the engine.
|
## Called when an optimized sliced layer data message is received from the engine.
|
||||||
#
|
#
|
||||||
# \param message The protobuf message containing sliced layer data.
|
# \param message The protobuf message containing sliced layer data.
|
||||||
def _onOptimizedLayerMessage(self, message):
|
def _onOptimizedLayerMessage(self, message: Arcus.PythonMessage) -> None:
|
||||||
if self._start_slice_job_build_plate not in self._stored_optimized_layer_data:
|
if self._start_slice_job_build_plate is not None:
|
||||||
self._stored_optimized_layer_data[self._start_slice_job_build_plate] = []
|
if self._start_slice_job_build_plate not in self._stored_optimized_layer_data:
|
||||||
self._stored_optimized_layer_data[self._start_slice_job_build_plate].append(message)
|
self._stored_optimized_layer_data[self._start_slice_job_build_plate] = []
|
||||||
|
self._stored_optimized_layer_data[self._start_slice_job_build_plate].append(message)
|
||||||
|
|
||||||
## Called when a progress message is received from the engine.
|
## Called when a progress message is received from the engine.
|
||||||
#
|
#
|
||||||
# \param message The protobuf message containing the slicing progress.
|
# \param message The protobuf message containing the slicing progress.
|
||||||
def _onProgressMessage(self, message):
|
def _onProgressMessage(self, message: Arcus.PythonMessage) -> None:
|
||||||
self.processingProgress.emit(message.amount)
|
self.processingProgress.emit(message.amount)
|
||||||
self.backendStateChange.emit(BackendState.Processing)
|
self.backendStateChange.emit(BackendState.Processing)
|
||||||
|
|
||||||
def _invokeSlice(self):
|
def _invokeSlice(self) -> None:
|
||||||
if self._use_timer:
|
if self._use_timer:
|
||||||
# if the error check is scheduled, wait for the error check finish signal to trigger auto-slice,
|
# if the error check is scheduled, wait for the error check finish signal to trigger auto-slice,
|
||||||
# otherwise business as usual
|
# otherwise business as usual
|
||||||
|
|
@ -593,26 +632,27 @@ class CuraEngineBackend(QObject, Backend):
|
||||||
## Called when the engine sends a message that slicing is finished.
|
## Called when the engine sends a message that slicing is finished.
|
||||||
#
|
#
|
||||||
# \param message The protobuf message signalling that slicing is finished.
|
# \param message The protobuf message signalling that slicing is finished.
|
||||||
def _onSlicingFinishedMessage(self, message):
|
def _onSlicingFinishedMessage(self, message: Arcus.PythonMessage) -> None:
|
||||||
self.backendStateChange.emit(BackendState.Done)
|
self.backendStateChange.emit(BackendState.Done)
|
||||||
self.processingProgress.emit(1.0)
|
self.processingProgress.emit(1.0)
|
||||||
|
|
||||||
gcode_list = self._scene.gcode_dict[self._start_slice_job_build_plate]
|
gcode_list = self._scene.gcode_dict[self._start_slice_job_build_plate] #type: ignore #Because we generate this attribute dynamically.
|
||||||
for index, line in enumerate(gcode_list):
|
for index, line in enumerate(gcode_list):
|
||||||
replaced = line.replace("{print_time}", str(Application.getInstance().getPrintInformation().currentPrintTime.getDisplayString(DurationFormat.Format.ISO8601)))
|
replaced = line.replace("{print_time}", str(self._application.getPrintInformation().currentPrintTime.getDisplayString(DurationFormat.Format.ISO8601)))
|
||||||
replaced = replaced.replace("{filament_amount}", str(Application.getInstance().getPrintInformation().materialLengths))
|
replaced = replaced.replace("{filament_amount}", str(self._application.getPrintInformation().materialLengths))
|
||||||
replaced = replaced.replace("{filament_weight}", str(Application.getInstance().getPrintInformation().materialWeights))
|
replaced = replaced.replace("{filament_weight}", str(self._application.getPrintInformation().materialWeights))
|
||||||
replaced = replaced.replace("{filament_cost}", str(Application.getInstance().getPrintInformation().materialCosts))
|
replaced = replaced.replace("{filament_cost}", str(self._application.getPrintInformation().materialCosts))
|
||||||
replaced = replaced.replace("{jobname}", str(Application.getInstance().getPrintInformation().jobName))
|
replaced = replaced.replace("{jobname}", str(self._application.getPrintInformation().jobName))
|
||||||
|
|
||||||
gcode_list[index] = replaced
|
gcode_list[index] = replaced
|
||||||
|
|
||||||
self._slicing = False
|
self._slicing = False
|
||||||
Logger.log("d", "Slicing took %s seconds", time() - self._slice_start_time )
|
if self._slice_start_time:
|
||||||
|
Logger.log("d", "Slicing took %s seconds", time() - self._slice_start_time )
|
||||||
Logger.log("d", "Number of models per buildplate: %s", dict(self._numObjectsPerBuildPlate()))
|
Logger.log("d", "Number of models per buildplate: %s", dict(self._numObjectsPerBuildPlate()))
|
||||||
|
|
||||||
# See if we need to process the sliced layers job.
|
# See if we need to process the sliced layers job.
|
||||||
active_build_plate = Application.getInstance().getMultiBuildPlateModel().activeBuildPlate
|
active_build_plate = self._application.getMultiBuildPlateModel().activeBuildPlate
|
||||||
if (
|
if (
|
||||||
self._layer_view_active and
|
self._layer_view_active and
|
||||||
(self._process_layers_job is None or not self._process_layers_job.isRunning()) and
|
(self._process_layers_job is None or not self._process_layers_job.isRunning()) and
|
||||||
|
|
@ -634,25 +674,31 @@ class CuraEngineBackend(QObject, Backend):
|
||||||
## Called when a g-code message is received from the engine.
|
## Called when a g-code message is received from the engine.
|
||||||
#
|
#
|
||||||
# \param message The protobuf message containing g-code, encoded as UTF-8.
|
# \param message The protobuf message containing g-code, encoded as UTF-8.
|
||||||
def _onGCodeLayerMessage(self, message):
|
def _onGCodeLayerMessage(self, message: Arcus.PythonMessage) -> None:
|
||||||
self._scene.gcode_dict[self._start_slice_job_build_plate].append(message.data.decode("utf-8", "replace"))
|
self._scene.gcode_dict[self._start_slice_job_build_plate].append(message.data.decode("utf-8", "replace")) #type: ignore #Because we generate this attribute dynamically.
|
||||||
|
|
||||||
## Called when a g-code prefix message is received from the engine.
|
## Called when a g-code prefix message is received from the engine.
|
||||||
#
|
#
|
||||||
# \param message The protobuf message containing the g-code prefix,
|
# \param message The protobuf message containing the g-code prefix,
|
||||||
# encoded as UTF-8.
|
# encoded as UTF-8.
|
||||||
def _onGCodePrefixMessage(self, message):
|
def _onGCodePrefixMessage(self, message: Arcus.PythonMessage) -> None:
|
||||||
self._scene.gcode_dict[self._start_slice_job_build_plate].insert(0, message.data.decode("utf-8", "replace"))
|
self._scene.gcode_dict[self._start_slice_job_build_plate].insert(0, message.data.decode("utf-8", "replace")) #type: ignore #Because we generate this attribute dynamically.
|
||||||
|
|
||||||
## Creates a new socket connection.
|
## Creates a new socket connection.
|
||||||
def _createSocket(self):
|
def _createSocket(self, protocol_file: str = None) -> None:
|
||||||
super()._createSocket(os.path.abspath(os.path.join(PluginRegistry.getInstance().getPluginPath(self.getPluginId()), "Cura.proto")))
|
if not protocol_file:
|
||||||
|
plugin_path = PluginRegistry.getInstance().getPluginPath(self.getPluginId())
|
||||||
|
if not plugin_path:
|
||||||
|
Logger.log("e", "Could not get plugin path!", self.getPluginId())
|
||||||
|
return
|
||||||
|
protocol_file = os.path.abspath(os.path.join(plugin_path, "Cura.proto"))
|
||||||
|
super()._createSocket(protocol_file)
|
||||||
self._engine_is_fresh = True
|
self._engine_is_fresh = True
|
||||||
|
|
||||||
## Called when anything has changed to the stuff that needs to be sliced.
|
## Called when anything has changed to the stuff that needs to be sliced.
|
||||||
#
|
#
|
||||||
# This indicates that we should probably re-slice soon.
|
# This indicates that we should probably re-slice soon.
|
||||||
def _onChanged(self, *args, **kwargs):
|
def _onChanged(self, *args: Any, **kwargs: Any) -> None:
|
||||||
self.needsSlicing()
|
self.needsSlicing()
|
||||||
if self._use_timer:
|
if self._use_timer:
|
||||||
# if the error check is scheduled, wait for the error check finish signal to trigger auto-slice,
|
# if the error check is scheduled, wait for the error check finish signal to trigger auto-slice,
|
||||||
|
|
@ -670,7 +716,7 @@ class CuraEngineBackend(QObject, Backend):
|
||||||
#
|
#
|
||||||
# \param message The protobuf message containing the print time per feature and
|
# \param message The protobuf message containing the print time per feature and
|
||||||
# material amount per extruder
|
# material amount per extruder
|
||||||
def _onPrintTimeMaterialEstimates(self, message):
|
def _onPrintTimeMaterialEstimates(self, message: Arcus.PythonMessage) -> None:
|
||||||
material_amounts = []
|
material_amounts = []
|
||||||
for index in range(message.repeatedMessageCount("materialEstimates")):
|
for index in range(message.repeatedMessageCount("materialEstimates")):
|
||||||
material_amounts.append(message.getRepeatedMessage("materialEstimates", index).material_amount)
|
material_amounts.append(message.getRepeatedMessage("materialEstimates", index).material_amount)
|
||||||
|
|
@ -681,7 +727,7 @@ class CuraEngineBackend(QObject, Backend):
|
||||||
## Called for parsing message to retrieve estimated time per feature
|
## Called for parsing message to retrieve estimated time per feature
|
||||||
#
|
#
|
||||||
# \param message The protobuf message containing the print time per feature
|
# \param message The protobuf message containing the print time per feature
|
||||||
def _parseMessagePrintTimes(self, message):
|
def _parseMessagePrintTimes(self, message: Arcus.PythonMessage) -> Dict[str, float]:
|
||||||
result = {
|
result = {
|
||||||
"inset_0": message.time_inset_0,
|
"inset_0": message.time_inset_0,
|
||||||
"inset_x": message.time_inset_x,
|
"inset_x": message.time_inset_x,
|
||||||
|
|
@ -698,7 +744,7 @@ class CuraEngineBackend(QObject, Backend):
|
||||||
return result
|
return result
|
||||||
|
|
||||||
## Called when the back-end connects to the front-end.
|
## Called when the back-end connects to the front-end.
|
||||||
def _onBackendConnected(self):
|
def _onBackendConnected(self) -> None:
|
||||||
if self._restart:
|
if self._restart:
|
||||||
self._restart = False
|
self._restart = False
|
||||||
self._onChanged()
|
self._onChanged()
|
||||||
|
|
@ -709,7 +755,7 @@ class CuraEngineBackend(QObject, Backend):
|
||||||
# continuously slicing while the user is dragging some tool handle.
|
# continuously slicing while the user is dragging some tool handle.
|
||||||
#
|
#
|
||||||
# \param tool The tool that the user is using.
|
# \param tool The tool that the user is using.
|
||||||
def _onToolOperationStarted(self, tool):
|
def _onToolOperationStarted(self, tool: Tool) -> None:
|
||||||
self._tool_active = True # Do not react on scene change
|
self._tool_active = True # Do not react on scene change
|
||||||
self.disableTimer()
|
self.disableTimer()
|
||||||
# Restart engine as soon as possible, we know we want to slice afterwards
|
# Restart engine as soon as possible, we know we want to slice afterwards
|
||||||
|
|
@ -722,7 +768,7 @@ class CuraEngineBackend(QObject, Backend):
|
||||||
# This indicates that we can safely start slicing again.
|
# This indicates that we can safely start slicing again.
|
||||||
#
|
#
|
||||||
# \param tool The tool that the user was using.
|
# \param tool The tool that the user was using.
|
||||||
def _onToolOperationStopped(self, tool):
|
def _onToolOperationStopped(self, tool: Tool) -> None:
|
||||||
self._tool_active = False # React on scene change again
|
self._tool_active = False # React on scene change again
|
||||||
self.determineAutoSlicing() # Switch timer on if appropriate
|
self.determineAutoSlicing() # Switch timer on if appropriate
|
||||||
# Process all the postponed scene changes
|
# Process all the postponed scene changes
|
||||||
|
|
@ -730,18 +776,17 @@ class CuraEngineBackend(QObject, Backend):
|
||||||
source = self._postponed_scene_change_sources.pop(0)
|
source = self._postponed_scene_change_sources.pop(0)
|
||||||
self._onSceneChanged(source)
|
self._onSceneChanged(source)
|
||||||
|
|
||||||
def _startProcessSlicedLayersJob(self, build_plate_number):
|
def _startProcessSlicedLayersJob(self, build_plate_number: int) -> None:
|
||||||
self._process_layers_job = ProcessSlicedLayersJob.ProcessSlicedLayersJob(self._stored_optimized_layer_data[build_plate_number])
|
self._process_layers_job = ProcessSlicedLayersJob(self._stored_optimized_layer_data[build_plate_number])
|
||||||
self._process_layers_job.setBuildPlate(build_plate_number)
|
self._process_layers_job.setBuildPlate(build_plate_number)
|
||||||
self._process_layers_job.finished.connect(self._onProcessLayersFinished)
|
self._process_layers_job.finished.connect(self._onProcessLayersFinished)
|
||||||
self._process_layers_job.start()
|
self._process_layers_job.start()
|
||||||
|
|
||||||
## Called when the user changes the active view mode.
|
## Called when the user changes the active view mode.
|
||||||
def _onActiveViewChanged(self):
|
def _onActiveViewChanged(self) -> None:
|
||||||
application = Application.getInstance()
|
view = self._application.getController().getActiveView()
|
||||||
view = application.getController().getActiveView()
|
|
||||||
if view:
|
if view:
|
||||||
active_build_plate = application.getMultiBuildPlateModel().activeBuildPlate
|
active_build_plate = self._application.getMultiBuildPlateModel().activeBuildPlate
|
||||||
if view.getPluginId() == "SimulationView": # If switching to layer view, we should process the layers if that hasn't been done yet.
|
if view.getPluginId() == "SimulationView": # If switching to layer view, we should process the layers if that hasn't been done yet.
|
||||||
self._layer_view_active = True
|
self._layer_view_active = True
|
||||||
# There is data and we're not slicing at the moment
|
# There is data and we're not slicing at the moment
|
||||||
|
|
@ -759,14 +804,14 @@ class CuraEngineBackend(QObject, Backend):
|
||||||
## Called when the back-end self-terminates.
|
## Called when the back-end self-terminates.
|
||||||
#
|
#
|
||||||
# We should reset our state and start listening for new connections.
|
# We should reset our state and start listening for new connections.
|
||||||
def _onBackendQuit(self):
|
def _onBackendQuit(self) -> None:
|
||||||
if not self._restart:
|
if not self._restart:
|
||||||
if self._process:
|
if self._process: # type: ignore
|
||||||
Logger.log("d", "Backend quit with return code %s. Resetting process and socket.", self._process.wait())
|
Logger.log("d", "Backend quit with return code %s. Resetting process and socket.", self._process.wait()) # type: ignore
|
||||||
self._process = None
|
self._process = None # type: ignore
|
||||||
|
|
||||||
## Called when the global container stack changes
|
## Called when the global container stack changes
|
||||||
def _onGlobalStackChanged(self):
|
def _onGlobalStackChanged(self) -> None:
|
||||||
if self._global_container_stack:
|
if self._global_container_stack:
|
||||||
self._global_container_stack.propertyChanged.disconnect(self._onSettingChanged)
|
self._global_container_stack.propertyChanged.disconnect(self._onSettingChanged)
|
||||||
self._global_container_stack.containersChanged.disconnect(self._onChanged)
|
self._global_container_stack.containersChanged.disconnect(self._onChanged)
|
||||||
|
|
@ -776,7 +821,7 @@ class CuraEngineBackend(QObject, Backend):
|
||||||
extruder.propertyChanged.disconnect(self._onSettingChanged)
|
extruder.propertyChanged.disconnect(self._onSettingChanged)
|
||||||
extruder.containersChanged.disconnect(self._onChanged)
|
extruder.containersChanged.disconnect(self._onChanged)
|
||||||
|
|
||||||
self._global_container_stack = Application.getInstance().getGlobalContainerStack()
|
self._global_container_stack = self._application.getGlobalContainerStack()
|
||||||
|
|
||||||
if self._global_container_stack:
|
if self._global_container_stack:
|
||||||
self._global_container_stack.propertyChanged.connect(self._onSettingChanged) # Note: Only starts slicing when the value changed.
|
self._global_container_stack.propertyChanged.connect(self._onSettingChanged) # Note: Only starts slicing when the value changed.
|
||||||
|
|
@ -787,26 +832,26 @@ class CuraEngineBackend(QObject, Backend):
|
||||||
extruder.containersChanged.connect(self._onChanged)
|
extruder.containersChanged.connect(self._onChanged)
|
||||||
self._onChanged()
|
self._onChanged()
|
||||||
|
|
||||||
def _onProcessLayersFinished(self, job):
|
def _onProcessLayersFinished(self, job: ProcessSlicedLayersJob) -> None:
|
||||||
del self._stored_optimized_layer_data[job.getBuildPlate()]
|
del self._stored_optimized_layer_data[job.getBuildPlate()]
|
||||||
self._process_layers_job = None
|
self._process_layers_job = None
|
||||||
Logger.log("d", "See if there is more to slice(2)...")
|
Logger.log("d", "See if there is more to slice(2)...")
|
||||||
self._invokeSlice()
|
self._invokeSlice()
|
||||||
|
|
||||||
## Connect slice function to timer.
|
## Connect slice function to timer.
|
||||||
def enableTimer(self):
|
def enableTimer(self) -> None:
|
||||||
if not self._use_timer:
|
if not self._use_timer:
|
||||||
self._change_timer.timeout.connect(self.slice)
|
self._change_timer.timeout.connect(self.slice)
|
||||||
self._use_timer = True
|
self._use_timer = True
|
||||||
|
|
||||||
## Disconnect slice function from timer.
|
## Disconnect slice function from timer.
|
||||||
# This means that slicing will not be triggered automatically
|
# This means that slicing will not be triggered automatically
|
||||||
def disableTimer(self):
|
def disableTimer(self) -> None:
|
||||||
if self._use_timer:
|
if self._use_timer:
|
||||||
self._use_timer = False
|
self._use_timer = False
|
||||||
self._change_timer.timeout.disconnect(self.slice)
|
self._change_timer.timeout.disconnect(self.slice)
|
||||||
|
|
||||||
def _onPreferencesChanged(self, preference):
|
def _onPreferencesChanged(self, preference: str) -> None:
|
||||||
if preference != "general/auto_slice":
|
if preference != "general/auto_slice":
|
||||||
return
|
return
|
||||||
auto_slice = self.determineAutoSlicing()
|
auto_slice = self.determineAutoSlicing()
|
||||||
|
|
@ -814,11 +859,14 @@ class CuraEngineBackend(QObject, Backend):
|
||||||
self._change_timer.start()
|
self._change_timer.start()
|
||||||
|
|
||||||
## Tickle the backend so in case of auto slicing, it starts the timer.
|
## Tickle the backend so in case of auto slicing, it starts the timer.
|
||||||
def tickle(self):
|
def tickle(self) -> None:
|
||||||
if self._use_timer:
|
if self._use_timer:
|
||||||
self._change_timer.start()
|
self._change_timer.start()
|
||||||
|
|
||||||
def _extruderChanged(self):
|
def _extruderChanged(self) -> None:
|
||||||
|
if not self._multi_build_plate_model:
|
||||||
|
Logger.log("w", "CuraEngineBackend does not have multi_build_plate_model assigned!")
|
||||||
|
return
|
||||||
for build_plate_number in range(self._multi_build_plate_model.maxBuildPlate + 1):
|
for build_plate_number in range(self._multi_build_plate_model.maxBuildPlate + 1):
|
||||||
if build_plate_number not in self._build_plates_to_be_sliced:
|
if build_plate_number not in self._build_plates_to_be_sliced:
|
||||||
self._build_plates_to_be_sliced.append(build_plate_number)
|
self._build_plates_to_be_sliced.append(build_plate_number)
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,6 @@ import gc
|
||||||
from UM.Job import Job
|
from UM.Job import Job
|
||||||
from UM.Application import Application
|
from UM.Application import Application
|
||||||
from UM.Mesh.MeshData import MeshData
|
from UM.Mesh.MeshData import MeshData
|
||||||
from UM.Preferences import Preferences
|
|
||||||
from UM.View.GL.OpenGLContext import OpenGLContext
|
from UM.View.GL.OpenGLContext import OpenGLContext
|
||||||
|
|
||||||
from UM.Message import Message
|
from UM.Message import Message
|
||||||
|
|
@ -199,7 +198,7 @@ class ProcessSlicedLayersJob(Job):
|
||||||
material_color_map[0, :] = color
|
material_color_map[0, :] = color
|
||||||
|
|
||||||
# We have to scale the colors for compatibility mode
|
# We have to scale the colors for compatibility mode
|
||||||
if OpenGLContext.isLegacyOpenGL() or bool(Preferences.getInstance().getValue("view/force_layer_view_compatibility_mode")):
|
if OpenGLContext.isLegacyOpenGL() or bool(Application.getInstance().getPreferences().getValue("view/force_layer_view_compatibility_mode")):
|
||||||
line_type_brightness = 0.5 # for compatibility mode
|
line_type_brightness = 0.5 # for compatibility mode
|
||||||
else:
|
else:
|
||||||
line_type_brightness = 1.0
|
line_type_brightness = 1.0
|
||||||
|
|
|
||||||
|
|
@ -1,21 +1,25 @@
|
||||||
# Copyright (c) 2017 Ultimaker B.V.
|
# Copyright (c) 2018 Ultimaker B.V.
|
||||||
# Cura is released under the terms of the LGPLv3 or higher.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
import numpy
|
import numpy
|
||||||
from string import Formatter
|
from string import Formatter
|
||||||
from enum import IntEnum
|
from enum import IntEnum
|
||||||
import time
|
import time
|
||||||
|
from typing import Any, cast, Dict, List, Optional, Set
|
||||||
import re
|
import re
|
||||||
|
import Arcus #For typing.
|
||||||
|
|
||||||
from UM.Job import Job
|
from UM.Job import Job
|
||||||
from UM.Application import Application
|
|
||||||
from UM.Logger import Logger
|
from UM.Logger import Logger
|
||||||
|
from UM.Settings.ContainerStack import ContainerStack #For typing.
|
||||||
|
from UM.Settings.SettingRelation import SettingRelation #For typing.
|
||||||
|
|
||||||
from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
|
from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
|
||||||
|
from UM.Scene.Scene import Scene #For typing.
|
||||||
from UM.Settings.Validator import ValidatorState
|
from UM.Settings.Validator import ValidatorState
|
||||||
from UM.Settings.SettingRelation import RelationType
|
from UM.Settings.SettingRelation import RelationType
|
||||||
|
|
||||||
|
from cura.CuraApplication import CuraApplication
|
||||||
from cura.Scene.CuraSceneNode import CuraSceneNode
|
from cura.Scene.CuraSceneNode import CuraSceneNode
|
||||||
from cura.OneAtATimeIterator import OneAtATimeIterator
|
from cura.OneAtATimeIterator import OneAtATimeIterator
|
||||||
from cura.Settings.ExtruderManager import ExtruderManager
|
from cura.Settings.ExtruderManager import ExtruderManager
|
||||||
|
|
@ -32,66 +36,60 @@ class StartJobResult(IntEnum):
|
||||||
MaterialIncompatible = 5
|
MaterialIncompatible = 5
|
||||||
BuildPlateError = 6
|
BuildPlateError = 6
|
||||||
ObjectSettingError = 7 #When an error occurs in per-object settings.
|
ObjectSettingError = 7 #When an error occurs in per-object settings.
|
||||||
|
ObjectsWithDisabledExtruder = 8
|
||||||
|
|
||||||
|
|
||||||
## Formatter class that handles token expansion in start/end gcod
|
## Formatter class that handles token expansion in start/end gcode
|
||||||
class GcodeStartEndFormatter(Formatter):
|
class GcodeStartEndFormatter(Formatter):
|
||||||
def get_value(self, key, args, kwargs): # [CodeStyle: get_value is an overridden function from the Formatter class]
|
def get_value(self, key: str, args: str, kwargs: dict, default_extruder_nr: str = "-1") -> str: #type: ignore # [CodeStyle: get_value is an overridden function from the Formatter class]
|
||||||
# The kwargs dictionary contains a dictionary for each stack (with a string of the extruder_nr as their key),
|
# The kwargs dictionary contains a dictionary for each stack (with a string of the extruder_nr as their key),
|
||||||
# and a default_extruder_nr to use when no extruder_nr is specified
|
# and a default_extruder_nr to use when no extruder_nr is specified
|
||||||
|
|
||||||
if isinstance(key, str):
|
extruder_nr = int(default_extruder_nr)
|
||||||
|
|
||||||
|
key_fragments = [fragment.strip() for fragment in key.split(",")]
|
||||||
|
if len(key_fragments) == 2:
|
||||||
try:
|
try:
|
||||||
extruder_nr = kwargs["default_extruder_nr"]
|
extruder_nr = int(key_fragments[1])
|
||||||
except ValueError:
|
except ValueError:
|
||||||
extruder_nr = -1
|
|
||||||
|
|
||||||
key_fragments = [fragment.strip() for fragment in key.split(',')]
|
|
||||||
if len(key_fragments) == 2:
|
|
||||||
try:
|
try:
|
||||||
extruder_nr = int(key_fragments[1])
|
extruder_nr = int(kwargs["-1"][key_fragments[1]]) # get extruder_nr values from the global stack #TODO: How can you ever provide the '-1' kwarg?
|
||||||
except ValueError:
|
except (KeyError, ValueError):
|
||||||
try:
|
# either the key does not exist, or the value is not an int
|
||||||
extruder_nr = int(kwargs["-1"][key_fragments[1]]) # get extruder_nr values from the global stack
|
Logger.log("w", "Unable to determine stack nr '%s' for key '%s' in start/end g-code, using global stack", key_fragments[1], key_fragments[0])
|
||||||
except (KeyError, ValueError):
|
elif len(key_fragments) != 1:
|
||||||
# either the key does not exist, or the value is not an int
|
|
||||||
Logger.log("w", "Unable to determine stack nr '%s' for key '%s' in start/end g-code, using global stack", key_fragments[1], key_fragments[0])
|
|
||||||
elif len(key_fragments) != 1:
|
|
||||||
Logger.log("w", "Incorrectly formatted placeholder '%s' in start/end g-code", key)
|
|
||||||
return "{" + str(key) + "}"
|
|
||||||
|
|
||||||
key = key_fragments[0]
|
|
||||||
try:
|
|
||||||
return kwargs[str(extruder_nr)][key]
|
|
||||||
except KeyError:
|
|
||||||
Logger.log("w", "Unable to replace '%s' placeholder in start/end g-code", key)
|
|
||||||
return "{" + key + "}"
|
|
||||||
else:
|
|
||||||
Logger.log("w", "Incorrectly formatted placeholder '%s' in start/end g-code", key)
|
Logger.log("w", "Incorrectly formatted placeholder '%s' in start/end g-code", key)
|
||||||
return "{" + str(key) + "}"
|
return "{" + key + "}"
|
||||||
|
|
||||||
|
key = key_fragments[0]
|
||||||
|
try:
|
||||||
|
return kwargs[str(extruder_nr)][key]
|
||||||
|
except KeyError:
|
||||||
|
Logger.log("w", "Unable to replace '%s' placeholder in start/end g-code", key)
|
||||||
|
return "{" + key + "}"
|
||||||
|
|
||||||
|
|
||||||
## Job class that builds up the message of scene data to send to CuraEngine.
|
## Job class that builds up the message of scene data to send to CuraEngine.
|
||||||
class StartSliceJob(Job):
|
class StartSliceJob(Job):
|
||||||
def __init__(self, slice_message):
|
def __init__(self, slice_message: Arcus.PythonMessage) -> None:
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
|
||||||
self._scene = Application.getInstance().getController().getScene()
|
self._scene = CuraApplication.getInstance().getController().getScene() #type: Scene
|
||||||
self._slice_message = slice_message
|
self._slice_message = slice_message #type: Arcus.PythonMessage
|
||||||
self._is_cancelled = False
|
self._is_cancelled = False #type: bool
|
||||||
self._build_plate_number = None
|
self._build_plate_number = None #type: Optional[int]
|
||||||
|
|
||||||
self._all_extruders_settings = None # cache for all setting values from all stacks (global & extruder) for the current machine
|
self._all_extruders_settings = None #type: Optional[Dict[str, Any]] # cache for all setting values from all stacks (global & extruder) for the current machine
|
||||||
|
|
||||||
def getSliceMessage(self):
|
def getSliceMessage(self) -> Arcus.PythonMessage:
|
||||||
return self._slice_message
|
return self._slice_message
|
||||||
|
|
||||||
def setBuildPlate(self, build_plate_number):
|
def setBuildPlate(self, build_plate_number: int) -> None:
|
||||||
self._build_plate_number = build_plate_number
|
self._build_plate_number = build_plate_number
|
||||||
|
|
||||||
## Check if a stack has any errors.
|
## Check if a stack has any errors.
|
||||||
## returns true if it has errors, false otherwise.
|
## returns true if it has errors, false otherwise.
|
||||||
def _checkStackForErrors(self, stack):
|
def _checkStackForErrors(self, stack: ContainerStack) -> bool:
|
||||||
if stack is None:
|
if stack is None:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
@ -104,28 +102,28 @@ class StartSliceJob(Job):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
## Runs the job that initiates the slicing.
|
## Runs the job that initiates the slicing.
|
||||||
def run(self):
|
def run(self) -> None:
|
||||||
if self._build_plate_number is None:
|
if self._build_plate_number is None:
|
||||||
self.setResult(StartJobResult.Error)
|
self.setResult(StartJobResult.Error)
|
||||||
return
|
return
|
||||||
|
|
||||||
stack = Application.getInstance().getGlobalContainerStack()
|
stack = CuraApplication.getInstance().getGlobalContainerStack()
|
||||||
if not stack:
|
if not stack:
|
||||||
self.setResult(StartJobResult.Error)
|
self.setResult(StartJobResult.Error)
|
||||||
return
|
return
|
||||||
|
|
||||||
# Don't slice if there is a setting with an error value.
|
# Don't slice if there is a setting with an error value.
|
||||||
if Application.getInstance().getMachineManager().stacksHaveErrors:
|
if CuraApplication.getInstance().getMachineManager().stacksHaveErrors:
|
||||||
self.setResult(StartJobResult.SettingError)
|
self.setResult(StartJobResult.SettingError)
|
||||||
return
|
return
|
||||||
|
|
||||||
if Application.getInstance().getBuildVolume().hasErrors():
|
if CuraApplication.getInstance().getBuildVolume().hasErrors():
|
||||||
self.setResult(StartJobResult.BuildPlateError)
|
self.setResult(StartJobResult.BuildPlateError)
|
||||||
return
|
return
|
||||||
|
|
||||||
# Don't slice if the buildplate or the nozzle type is incompatible with the materials
|
# Don't slice if the buildplate or the nozzle type is incompatible with the materials
|
||||||
if not Application.getInstance().getMachineManager().variantBuildplateCompatible and \
|
if not CuraApplication.getInstance().getMachineManager().variantBuildplateCompatible and \
|
||||||
not Application.getInstance().getMachineManager().variantBuildplateUsable:
|
not CuraApplication.getInstance().getMachineManager().variantBuildplateUsable:
|
||||||
self.setResult(StartJobResult.MaterialIncompatible)
|
self.setResult(StartJobResult.MaterialIncompatible)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
@ -140,7 +138,7 @@ class StartSliceJob(Job):
|
||||||
|
|
||||||
|
|
||||||
# Don't slice if there is a per object setting with an error value.
|
# Don't slice if there is a per object setting with an error value.
|
||||||
for node in DepthFirstIterator(self._scene.getRoot()):
|
for node in DepthFirstIterator(self._scene.getRoot()): #type: ignore #Ignore type error because iter() should get called automatically by Python syntax.
|
||||||
if not isinstance(node, CuraSceneNode) or not node.isSelectable():
|
if not isinstance(node, CuraSceneNode) or not node.isSelectable():
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
|
@ -150,7 +148,7 @@ class StartSliceJob(Job):
|
||||||
|
|
||||||
with self._scene.getSceneLock():
|
with self._scene.getSceneLock():
|
||||||
# Remove old layer data.
|
# Remove old layer data.
|
||||||
for node in DepthFirstIterator(self._scene.getRoot()):
|
for node in DepthFirstIterator(self._scene.getRoot()): #type: ignore #Ignore type error because iter() should get called automatically by Python syntax.
|
||||||
if node.callDecoration("getLayerData") and node.callDecoration("getBuildPlateNumber") == self._build_plate_number:
|
if node.callDecoration("getLayerData") and node.callDecoration("getBuildPlateNumber") == self._build_plate_number:
|
||||||
node.getParent().removeChild(node)
|
node.getParent().removeChild(node)
|
||||||
break
|
break
|
||||||
|
|
@ -158,7 +156,7 @@ class StartSliceJob(Job):
|
||||||
# Get the objects in their groups to print.
|
# Get the objects in their groups to print.
|
||||||
object_groups = []
|
object_groups = []
|
||||||
if stack.getProperty("print_sequence", "value") == "one_at_a_time":
|
if stack.getProperty("print_sequence", "value") == "one_at_a_time":
|
||||||
for node in OneAtATimeIterator(self._scene.getRoot()):
|
for node in OneAtATimeIterator(self._scene.getRoot()): #type: ignore #Ignore type error because iter() should get called automatically by Python syntax.
|
||||||
temp_list = []
|
temp_list = []
|
||||||
|
|
||||||
# Node can't be printed, so don't bother sending it.
|
# Node can't be printed, so don't bother sending it.
|
||||||
|
|
@ -184,7 +182,7 @@ class StartSliceJob(Job):
|
||||||
else:
|
else:
|
||||||
temp_list = []
|
temp_list = []
|
||||||
has_printing_mesh = False
|
has_printing_mesh = False
|
||||||
for node in DepthFirstIterator(self._scene.getRoot()):
|
for node in DepthFirstIterator(self._scene.getRoot()): #type: ignore #Ignore type error because iter() should get called automatically by Python syntax.
|
||||||
if node.callDecoration("isSliceable") and node.getMeshData() and node.getMeshData().getVertices() is not None:
|
if node.callDecoration("isSliceable") and node.getMeshData() and node.getMeshData().getVertices() is not None:
|
||||||
per_object_stack = node.callDecoration("getStack")
|
per_object_stack = node.callDecoration("getStack")
|
||||||
is_non_printing_mesh = False
|
is_non_printing_mesh = False
|
||||||
|
|
@ -211,18 +209,31 @@ class StartSliceJob(Job):
|
||||||
if temp_list:
|
if temp_list:
|
||||||
object_groups.append(temp_list)
|
object_groups.append(temp_list)
|
||||||
|
|
||||||
extruders_enabled = {position: stack.isEnabled for position, stack in Application.getInstance().getGlobalContainerStack().extruders.items()}
|
global_stack = CuraApplication.getInstance().getGlobalContainerStack()
|
||||||
|
if not global_stack:
|
||||||
|
return
|
||||||
|
extruders_enabled = {position: stack.isEnabled for position, stack in global_stack.extruders.items()}
|
||||||
filtered_object_groups = []
|
filtered_object_groups = []
|
||||||
|
has_model_with_disabled_extruders = False
|
||||||
|
associated_disabled_extruders = set()
|
||||||
for group in object_groups:
|
for group in object_groups:
|
||||||
stack = Application.getInstance().getGlobalContainerStack()
|
stack = global_stack
|
||||||
skip_group = False
|
skip_group = False
|
||||||
for node in group:
|
for node in group:
|
||||||
if not extruders_enabled[node.callDecoration("getActiveExtruderPosition")]:
|
extruder_position = node.callDecoration("getActiveExtruderPosition")
|
||||||
|
if not extruders_enabled[extruder_position]:
|
||||||
skip_group = True
|
skip_group = True
|
||||||
break
|
has_model_with_disabled_extruders = True
|
||||||
|
associated_disabled_extruders.add(extruder_position)
|
||||||
if not skip_group:
|
if not skip_group:
|
||||||
filtered_object_groups.append(group)
|
filtered_object_groups.append(group)
|
||||||
|
|
||||||
|
if has_model_with_disabled_extruders:
|
||||||
|
self.setResult(StartJobResult.ObjectsWithDisabledExtruder)
|
||||||
|
associated_disabled_extruders = {str(c) for c in sorted([int(p) + 1 for p in associated_disabled_extruders])}
|
||||||
|
self.setMessage(", ".join(associated_disabled_extruders))
|
||||||
|
return
|
||||||
|
|
||||||
# There are cases when there is nothing to slice. This can happen due to one at a time slicing not being
|
# There are cases when there is nothing to slice. This can happen due to one at a time slicing not being
|
||||||
# able to find a possible sequence or because there are no objects on the build plate (or they are outside
|
# able to find a possible sequence or because there are no objects on the build plate (or they are outside
|
||||||
# the build volume)
|
# the build volume)
|
||||||
|
|
@ -272,13 +283,16 @@ class StartSliceJob(Job):
|
||||||
|
|
||||||
self.setResult(StartJobResult.Finished)
|
self.setResult(StartJobResult.Finished)
|
||||||
|
|
||||||
def cancel(self):
|
def cancel(self) -> None:
|
||||||
super().cancel()
|
super().cancel()
|
||||||
self._is_cancelled = True
|
self._is_cancelled = True
|
||||||
|
|
||||||
def isCancelled(self):
|
def isCancelled(self) -> bool:
|
||||||
return self._is_cancelled
|
return self._is_cancelled
|
||||||
|
|
||||||
|
def setIsCancelled(self, value: bool):
|
||||||
|
self._is_cancelled = value
|
||||||
|
|
||||||
## Creates a dictionary of tokens to replace in g-code pieces.
|
## Creates a dictionary of tokens to replace in g-code pieces.
|
||||||
#
|
#
|
||||||
# This indicates what should be replaced in the start and end g-codes.
|
# This indicates what should be replaced in the start and end g-codes.
|
||||||
|
|
@ -286,15 +300,10 @@ class StartSliceJob(Job):
|
||||||
# with.
|
# with.
|
||||||
# \return A dictionary of replacement tokens to the values they should be
|
# \return A dictionary of replacement tokens to the values they should be
|
||||||
# replaced with.
|
# replaced with.
|
||||||
def _buildReplacementTokens(self, stack) -> dict:
|
def _buildReplacementTokens(self, stack: ContainerStack) -> Dict[str, Any]:
|
||||||
default_extruder_position = int(Application.getInstance().getMachineManager().defaultExtruderPosition)
|
|
||||||
result = {}
|
result = {}
|
||||||
for key in stack.getAllKeys():
|
for key in stack.getAllKeys():
|
||||||
setting_type = stack.definition.getProperty(key, "type")
|
|
||||||
value = stack.getProperty(key, "value")
|
value = stack.getProperty(key, "value")
|
||||||
if setting_type == "extruder" and value == -1:
|
|
||||||
# replace with the default value
|
|
||||||
value = default_extruder_position
|
|
||||||
result[key] = value
|
result[key] = value
|
||||||
Job.yieldThread()
|
Job.yieldThread()
|
||||||
|
|
||||||
|
|
@ -304,7 +313,7 @@ class StartSliceJob(Job):
|
||||||
result["date"] = time.strftime("%d-%m-%Y")
|
result["date"] = time.strftime("%d-%m-%Y")
|
||||||
result["day"] = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"][int(time.strftime("%w"))]
|
result["day"] = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"][int(time.strftime("%w"))]
|
||||||
|
|
||||||
initial_extruder_stack = Application.getInstance().getExtruderManager().getUsedExtruderStacks()[0]
|
initial_extruder_stack = CuraApplication.getInstance().getExtruderManager().getUsedExtruderStacks()[0]
|
||||||
initial_extruder_nr = initial_extruder_stack.getProperty("extruder_nr", "value")
|
initial_extruder_nr = initial_extruder_stack.getProperty("extruder_nr", "value")
|
||||||
result["initial_extruder_nr"] = initial_extruder_nr
|
result["initial_extruder_nr"] = initial_extruder_nr
|
||||||
|
|
||||||
|
|
@ -313,9 +322,9 @@ class StartSliceJob(Job):
|
||||||
## Replace setting tokens in a piece of g-code.
|
## Replace setting tokens in a piece of g-code.
|
||||||
# \param value A piece of g-code to replace tokens in.
|
# \param value A piece of g-code to replace tokens in.
|
||||||
# \param default_extruder_nr Stack nr to use when no stack nr is specified, defaults to the global stack
|
# \param default_extruder_nr Stack nr to use when no stack nr is specified, defaults to the global stack
|
||||||
def _expandGcodeTokens(self, value: str, default_extruder_nr: int = -1):
|
def _expandGcodeTokens(self, value: str, default_extruder_nr: int = -1) -> str:
|
||||||
if not self._all_extruders_settings:
|
if not self._all_extruders_settings:
|
||||||
global_stack = Application.getInstance().getGlobalContainerStack()
|
global_stack = cast(ContainerStack, CuraApplication.getInstance().getGlobalContainerStack())
|
||||||
|
|
||||||
# NB: keys must be strings for the string formatter
|
# NB: keys must be strings for the string formatter
|
||||||
self._all_extruders_settings = {
|
self._all_extruders_settings = {
|
||||||
|
|
@ -337,7 +346,7 @@ class StartSliceJob(Job):
|
||||||
return str(value)
|
return str(value)
|
||||||
|
|
||||||
## Create extruder message from stack
|
## Create extruder message from stack
|
||||||
def _buildExtruderMessage(self, stack):
|
def _buildExtruderMessage(self, stack: ContainerStack) -> None:
|
||||||
message = self._slice_message.addRepeatedMessage("extruders")
|
message = self._slice_message.addRepeatedMessage("extruders")
|
||||||
message.id = int(stack.getMetaDataEntry("position"))
|
message.id = int(stack.getMetaDataEntry("position"))
|
||||||
|
|
||||||
|
|
@ -364,7 +373,7 @@ class StartSliceJob(Job):
|
||||||
#
|
#
|
||||||
# The settings are taken from the global stack. This does not include any
|
# The settings are taken from the global stack. This does not include any
|
||||||
# per-extruder settings or per-object settings.
|
# per-extruder settings or per-object settings.
|
||||||
def _buildGlobalSettingsMessage(self, stack):
|
def _buildGlobalSettingsMessage(self, stack: ContainerStack) -> None:
|
||||||
settings = self._buildReplacementTokens(stack)
|
settings = self._buildReplacementTokens(stack)
|
||||||
|
|
||||||
# Pre-compute material material_bed_temp_prepend and material_print_temp_prepend
|
# Pre-compute material material_bed_temp_prepend and material_print_temp_prepend
|
||||||
|
|
@ -378,7 +387,7 @@ class StartSliceJob(Job):
|
||||||
|
|
||||||
# Replace the setting tokens in start and end g-code.
|
# Replace the setting tokens in start and end g-code.
|
||||||
# Use values from the first used extruder by default so we get the expected temperatures
|
# Use values from the first used extruder by default so we get the expected temperatures
|
||||||
initial_extruder_stack = Application.getInstance().getExtruderManager().getUsedExtruderStacks()[0]
|
initial_extruder_stack = CuraApplication.getInstance().getExtruderManager().getUsedExtruderStacks()[0]
|
||||||
initial_extruder_nr = initial_extruder_stack.getProperty("extruder_nr", "value")
|
initial_extruder_nr = initial_extruder_stack.getProperty("extruder_nr", "value")
|
||||||
|
|
||||||
settings["machine_start_gcode"] = self._expandGcodeTokens(settings["machine_start_gcode"], initial_extruder_nr)
|
settings["machine_start_gcode"] = self._expandGcodeTokens(settings["machine_start_gcode"], initial_extruder_nr)
|
||||||
|
|
@ -399,7 +408,7 @@ class StartSliceJob(Job):
|
||||||
#
|
#
|
||||||
# \param stack The global stack with all settings, from which to read the
|
# \param stack The global stack with all settings, from which to read the
|
||||||
# limit_to_extruder property.
|
# limit_to_extruder property.
|
||||||
def _buildGlobalInheritsStackMessage(self, stack):
|
def _buildGlobalInheritsStackMessage(self, stack: ContainerStack) -> None:
|
||||||
for key in stack.getAllKeys():
|
for key in stack.getAllKeys():
|
||||||
extruder_position = int(round(float(stack.getProperty(key, "limit_to_extruder"))))
|
extruder_position = int(round(float(stack.getProperty(key, "limit_to_extruder"))))
|
||||||
if extruder_position >= 0: # Set to a specific extruder.
|
if extruder_position >= 0: # Set to a specific extruder.
|
||||||
|
|
@ -409,9 +418,9 @@ class StartSliceJob(Job):
|
||||||
Job.yieldThread()
|
Job.yieldThread()
|
||||||
|
|
||||||
## Check if a node has per object settings and ensure that they are set correctly in the message
|
## Check if a node has per object settings and ensure that they are set correctly in the message
|
||||||
# \param node \type{SceneNode} Node to check.
|
# \param node Node to check.
|
||||||
# \param message object_lists message to put the per object settings in
|
# \param message object_lists message to put the per object settings in
|
||||||
def _handlePerObjectSettings(self, node, message):
|
def _handlePerObjectSettings(self, node: CuraSceneNode, message: Arcus.PythonMessage):
|
||||||
stack = node.callDecoration("getStack")
|
stack = node.callDecoration("getStack")
|
||||||
|
|
||||||
# Check if the node has a stack attached to it and the stack has any settings in the top container.
|
# Check if the node has a stack attached to it and the stack has any settings in the top container.
|
||||||
|
|
@ -420,7 +429,7 @@ class StartSliceJob(Job):
|
||||||
|
|
||||||
# Check all settings for relations, so we can also calculate the correct values for dependent settings.
|
# Check all settings for relations, so we can also calculate the correct values for dependent settings.
|
||||||
top_of_stack = stack.getTop() # Cache for efficiency.
|
top_of_stack = stack.getTop() # Cache for efficiency.
|
||||||
changed_setting_keys = set(top_of_stack.getAllKeys())
|
changed_setting_keys = top_of_stack.getAllKeys()
|
||||||
|
|
||||||
# Add all relations to changed settings as well.
|
# Add all relations to changed settings as well.
|
||||||
for key in top_of_stack.getAllKeys():
|
for key in top_of_stack.getAllKeys():
|
||||||
|
|
@ -449,9 +458,9 @@ class StartSliceJob(Job):
|
||||||
Job.yieldThread()
|
Job.yieldThread()
|
||||||
|
|
||||||
## Recursive function to put all settings that require each other for value changes in a list
|
## Recursive function to put all settings that require each other for value changes in a list
|
||||||
# \param relations_set \type{set} Set of keys (strings) of settings that are influenced
|
# \param relations_set Set of keys of settings that are influenced
|
||||||
# \param relations list of relation objects that need to be checked.
|
# \param relations list of relation objects that need to be checked.
|
||||||
def _addRelations(self, relations_set, relations):
|
def _addRelations(self, relations_set: Set[str], relations: List[SettingRelation]):
|
||||||
for relation in filter(lambda r: r.role == "value" or r.role == "limit_to_extruder", relations):
|
for relation in filter(lambda r: r.role == "value" or r.role == "limit_to_extruder", relations):
|
||||||
if relation.type == RelationType.RequiresTarget:
|
if relation.type == RelationType.RequiresTarget:
|
||||||
continue
|
continue
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ from UM.PluginRegistry import PluginRegistry
|
||||||
from UM.Logger import Logger
|
from UM.Logger import Logger
|
||||||
from UM.Settings.ContainerFormatError import ContainerFormatError
|
from UM.Settings.ContainerFormatError import ContainerFormatError
|
||||||
from UM.Settings.InstanceContainer import InstanceContainer # The new profile to make.
|
from UM.Settings.InstanceContainer import InstanceContainer # The new profile to make.
|
||||||
from cura.ProfileReader import ProfileReader
|
from cura.ReaderWriters.ProfileReader import ProfileReader
|
||||||
|
|
||||||
import zipfile
|
import zipfile
|
||||||
|
|
||||||
|
|
@ -75,7 +75,7 @@ class CuraProfileReader(ProfileReader):
|
||||||
def _loadProfile(self, serialized, profile_id):
|
def _loadProfile(self, serialized, profile_id):
|
||||||
# Create an empty profile.
|
# Create an empty profile.
|
||||||
profile = InstanceContainer(profile_id)
|
profile = InstanceContainer(profile_id)
|
||||||
profile.addMetaDataEntry("type", "quality_changes")
|
profile.setMetaDataEntry("type", "quality_changes")
|
||||||
try:
|
try:
|
||||||
profile.deserialize(serialized)
|
profile.deserialize(serialized)
|
||||||
except ContainerFormatError as e:
|
except ContainerFormatError as e:
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,7 @@
|
||||||
# Uranium is released under the terms of the LGPLv3 or higher.
|
# Uranium is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
from UM.Logger import Logger
|
from UM.Logger import Logger
|
||||||
from UM.SaveFile import SaveFile
|
from cura.ReaderWriters.ProfileWriter import ProfileWriter
|
||||||
from cura.ProfileWriter import ProfileWriter
|
|
||||||
import zipfile
|
import zipfile
|
||||||
|
|
||||||
## Writes profiles to Cura's own profile format with config files.
|
## Writes profiles to Cura's own profile format with config files.
|
||||||
|
|
|
||||||
|
|
@ -5,13 +5,14 @@ from PyQt5.QtCore import QUrl
|
||||||
from PyQt5.QtGui import QDesktopServices
|
from PyQt5.QtGui import QDesktopServices
|
||||||
|
|
||||||
from UM.Extension import Extension
|
from UM.Extension import Extension
|
||||||
from UM.Preferences import Preferences
|
from UM.Application import Application
|
||||||
from UM.Logger import Logger
|
from UM.Logger import Logger
|
||||||
from UM.i18n import i18nCatalog
|
from UM.i18n import i18nCatalog
|
||||||
|
from UM.Settings.ContainerRegistry import ContainerRegistry
|
||||||
|
|
||||||
from cura.Settings.GlobalStack import GlobalStack
|
from cura.Settings.GlobalStack import GlobalStack
|
||||||
|
|
||||||
from .FirmwareUpdateCheckerJob import FirmwareUpdateCheckerJob
|
from .FirmwareUpdateCheckerJob import FirmwareUpdateCheckerJob
|
||||||
from UM.Settings.ContainerRegistry import ContainerRegistry
|
|
||||||
|
|
||||||
i18n_catalog = i18nCatalog("cura")
|
i18n_catalog = i18nCatalog("cura")
|
||||||
|
|
||||||
|
|
@ -27,15 +28,16 @@ class FirmwareUpdateChecker(Extension):
|
||||||
|
|
||||||
# Initialize the Preference called `latest_checked_firmware` that stores the last version
|
# Initialize the Preference called `latest_checked_firmware` that stores the last version
|
||||||
# checked for the UM3. In the future if we need to check other printers' firmware
|
# checked for the UM3. In the future if we need to check other printers' firmware
|
||||||
Preferences.getInstance().addPreference("info/latest_checked_firmware", "")
|
Application.getInstance().getPreferences().addPreference("info/latest_checked_firmware", "")
|
||||||
|
|
||||||
# Listen to a Signal that indicates a change in the list of printers, just if the user has enabled the
|
# Listen to a Signal that indicates a change in the list of printers, just if the user has enabled the
|
||||||
# 'check for updates' option
|
# 'check for updates' option
|
||||||
Preferences.getInstance().addPreference("info/automatic_update_check", True)
|
Application.getInstance().getPreferences().addPreference("info/automatic_update_check", True)
|
||||||
if Preferences.getInstance().getValue("info/automatic_update_check"):
|
if Application.getInstance().getPreferences().getValue("info/automatic_update_check"):
|
||||||
ContainerRegistry.getInstance().containerAdded.connect(self._onContainerAdded)
|
ContainerRegistry.getInstance().containerAdded.connect(self._onContainerAdded)
|
||||||
|
|
||||||
self._download_url = None
|
self._download_url = None
|
||||||
|
self._check_job = None
|
||||||
|
|
||||||
## Callback for the message that is spawned when there is a new version.
|
## Callback for the message that is spawned when there is a new version.
|
||||||
def _onActionTriggered(self, message, action):
|
def _onActionTriggered(self, message, action):
|
||||||
|
|
@ -51,6 +53,9 @@ class FirmwareUpdateChecker(Extension):
|
||||||
if isinstance(container, GlobalStack):
|
if isinstance(container, GlobalStack):
|
||||||
self.checkFirmwareVersion(container, True)
|
self.checkFirmwareVersion(container, True)
|
||||||
|
|
||||||
|
def _onJobFinished(self, *args, **kwargs):
|
||||||
|
self._check_job = None
|
||||||
|
|
||||||
## Connect with software.ultimaker.com, load latest.version and check version info.
|
## Connect with software.ultimaker.com, load latest.version and check version info.
|
||||||
# If the version info is different from the current version, spawn a message to
|
# If the version info is different from the current version, spawn a message to
|
||||||
# allow the user to download it.
|
# allow the user to download it.
|
||||||
|
|
@ -58,7 +63,13 @@ class FirmwareUpdateChecker(Extension):
|
||||||
# \param silent type(boolean) Suppresses messages other than "new version found" messages.
|
# \param silent type(boolean) Suppresses messages other than "new version found" messages.
|
||||||
# This is used when checking for a new firmware version at startup.
|
# This is used when checking for a new firmware version at startup.
|
||||||
def checkFirmwareVersion(self, container = None, silent = False):
|
def checkFirmwareVersion(self, container = None, silent = False):
|
||||||
job = FirmwareUpdateCheckerJob(container = container, silent = silent, url = self.JEDI_VERSION_URL,
|
# Do not run multiple check jobs in parallel
|
||||||
callback = self._onActionTriggered,
|
if self._check_job is not None:
|
||||||
set_download_url_callback = self._onSetDownloadUrl)
|
Logger.log("i", "A firmware update check is already running, do nothing.")
|
||||||
job.start()
|
return
|
||||||
|
|
||||||
|
self._check_job = FirmwareUpdateCheckerJob(container = container, silent = silent, url = self.JEDI_VERSION_URL,
|
||||||
|
callback = self._onActionTriggered,
|
||||||
|
set_download_url_callback = self._onSetDownloadUrl)
|
||||||
|
self._check_job.start()
|
||||||
|
self._check_job.finished.connect(self._onJobFinished)
|
||||||
|
|
|
||||||
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