Merge remote-tracking branch 'Ultimaker/master' into Felix_Profile

This commit is contained in:
kerog777 2018-08-21 22:18:03 -07:00
commit 17c08eacf4
1203 changed files with 305148 additions and 137799 deletions

View file

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

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

View file

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

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

View file

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

View file

@ -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&amp;utm_medium=software&amp;utm_campaign=resources</url>
<translation type="gettext">Cura</translation> <translation type="gettext">Cura</translation>
</component> </component>

View file

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

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

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

View file

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

View file

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

View file

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

View file

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

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

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

View file

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

View file

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

View 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

View file

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

View file

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

View file

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

View file

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

View file

@ -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"),

View file

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

View file

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

View file

@ -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] = {}

View file

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

View file

@ -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"],

View file

@ -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([])

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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

View file

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

View file

@ -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()):

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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"]):

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View 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

View file

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

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

View file

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

View file

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

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

View file

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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() }

View file

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

View file

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

View file

@ -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 cant 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 doesnt 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 users 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 users system).

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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