diff --git a/cura/ApplicationMetadata.py b/cura/ApplicationMetadata.py index 6e0aa5f04f..2e15d60a93 100644 --- a/cura/ApplicationMetadata.py +++ b/cura/ApplicationMetadata.py @@ -13,7 +13,7 @@ DEFAULT_CURA_DEBUG_MODE = False # Each release has a fixed SDK version coupled with it. It doesn't make sense to make it configurable because, for # example Cura 3.2 with SDK version 6.1 will not work. So the SDK version is hard-coded here and left out of the # CuraVersion.py.in template. -CuraSDKVersion = "7.3.0" +CuraSDKVersion = "7.4.0" try: from cura.CuraVersion import CuraAppName # type: ignore diff --git a/cura/Arranging/Arrange.py b/cura/Arranging/Arrange.py index c9d3498c7b..e4a64afd3f 100644 --- a/cura/Arranging/Arrange.py +++ b/cura/Arranging/Arrange.py @@ -2,6 +2,7 @@ # Cura is released under the terms of the LGPLv3 or higher. from typing import Optional +from UM.Decorators import deprecated from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator from UM.Logger import Logger from UM.Math.Polygon import Polygon @@ -32,6 +33,7 @@ class Arrange: build_volume = None # type: Optional[BuildVolume] + @deprecated("Use the functions in Nest2dArrange instead", "4.8") def __init__(self, x, y, offset_x, offset_y, scale = 0.5): self._scale = scale # convert input coordinates to arrange coordinates world_x, world_y = int(x * self._scale), int(y * self._scale) @@ -45,6 +47,7 @@ class Arrange: self._is_empty = True @classmethod + @deprecated("Use the functions in Nest2dArrange instead", "4.8") def create(cls, scene_root = None, fixed_nodes = None, scale = 0.5, x = 350, y = 250, min_offset = 8) -> "Arrange": """Helper to create an :py:class:`cura.Arranging.Arrange.Arrange` instance @@ -101,6 +104,7 @@ class Arrange: self._last_priority = 0 + @deprecated("Use the functions in Nest2dArrange instead", "4.8") def findNodePlacement(self, node: SceneNode, offset_shape_arr: ShapeArray, hull_shape_arr: ShapeArray, step = 1) -> bool: """Find placement for a node (using offset shape) and place it (using hull shape) diff --git a/cura/Arranging/ArrangeObjectsJob.py b/cura/Arranging/ArrangeObjectsJob.py index 387bf92688..46b1aa2d71 100644 --- a/cura/Arranging/ArrangeObjectsJob.py +++ b/cura/Arranging/ArrangeObjectsJob.py @@ -1,24 +1,17 @@ # Copyright (c) 2019 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. -from PyQt5.QtCore import QCoreApplication +from typing import List from UM.Application import Application from UM.Job import Job -from UM.Scene.SceneNode import SceneNode -from UM.Math.Vector import Vector -from UM.Operations.TranslateOperation import TranslateOperation -from UM.Operations.GroupedOperation import GroupedOperation from UM.Logger import Logger from UM.Message import Message +from UM.Scene.SceneNode import SceneNode from UM.i18n import i18nCatalog +from cura.Arranging.Nest2DArrange import arrange + i18n_catalog = i18nCatalog("cura") -from cura.Scene.ZOffsetDecorator import ZOffsetDecorator -from cura.Arranging.Arrange import Arrange -from cura.Arranging.ShapeArray import ShapeArray - -from typing import List - class ArrangeObjectsJob(Job): def __init__(self, nodes: List[SceneNode], fixed_nodes: List[SceneNode], min_offset = 8) -> None: @@ -30,80 +23,22 @@ class ArrangeObjectsJob(Job): def run(self): status_message = Message(i18n_catalog.i18nc("@info:status", "Finding new location for objects"), lifetime = 0, - dismissable=False, + dismissable = False, progress = 0, title = i18n_catalog.i18nc("@info:title", "Finding Location")) status_message.show() - 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) - - # Build set to exclude children (those get arranged together with the parents). - included_as_child = set() - for node in self._nodes: - included_as_child.update(node.getAllChildren()) - - # Collect nodes to be placed - nodes_arr = [] # fill with (size, node, offset_shape_arr, hull_shape_arr) - for node in self._nodes: - if node in included_as_child: - continue - offset_shape_arr, hull_shape_arr = ShapeArray.fromNode(node, min_offset = self._min_offset, include_children = True) - 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)) - - # Sort the nodes with the biggest area first. - nodes_arr.sort(key=lambda item: item[0]) - nodes_arr.reverse() - - # Place nodes one at a time - start_priority = 0 - last_priority = start_priority - last_size = None - grouped_operation = GroupedOperation() - found_solution_for_all = True - not_fit_count = 0 - 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, - # it will also not fit for the next object (while what can be untrue). - if last_size == size: # This optimization works if many of the objects have the same size - start_priority = last_priority - else: - start_priority = 0 - best_spot = arranger.bestSpot(hull_shape_arr, start_prio = start_priority) - x, y = best_spot.x, best_spot.y - node.removeDecorator(ZOffsetDecorator) - if node.getBoundingBox(): - center_y = node.getWorldPosition().y - node.getBoundingBox().bottom - else: - center_y = 0 - if x is not None: # We could find a place - last_size = size - last_priority = best_spot.priority - - 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)) - else: - Logger.log("d", "Arrange all: could not find spot!") - found_solution_for_all = False - 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) - Job.yieldThread() - QCoreApplication.processEvents() - - grouped_operation.push() + found_solution_for_all = None + try: + found_solution_for_all = arrange(self._nodes, Application.getInstance().getBuildVolume(), self._fixed_nodes) + except: # If the thread crashes, the message should still close + Logger.logException("e", "Unable to arrange the objects on the buildplate. The arrange algorithm has crashed.") status_message.hide() - - if not found_solution_for_all: - no_full_solution_message = Message(i18n_catalog.i18nc("@info:status", "Unable to find a location within the build volume for all objects"), - title = i18n_catalog.i18nc("@info:title", "Can't Find Location")) + if found_solution_for_all is not None and not found_solution_for_all: + no_full_solution_message = Message( + i18n_catalog.i18nc("@info:status", + "Unable to find a location within the build volume for all objects"), + title = i18n_catalog.i18nc("@info:title", "Can't Find Location")) no_full_solution_message.show() - self.finished.emit(self) diff --git a/cura/Arranging/Nest2DArrange.py b/cura/Arranging/Nest2DArrange.py new file mode 100644 index 0000000000..d4da9266c7 --- /dev/null +++ b/cura/Arranging/Nest2DArrange.py @@ -0,0 +1,144 @@ +import numpy +from pynest2d import Point, Box, Item, NfpConfig, nest +from typing import List, TYPE_CHECKING, Optional, Tuple + +from UM.Application import Application +from UM.Logger import Logger +from UM.Math.Matrix import Matrix +from UM.Math.Polygon import Polygon +from UM.Math.Quaternion import Quaternion +from UM.Math.Vector import Vector +from UM.Operations.AddSceneNodeOperation import AddSceneNodeOperation +from UM.Operations.GroupedOperation import GroupedOperation +from UM.Operations.RotateOperation import RotateOperation +from UM.Operations.TranslateOperation import TranslateOperation + + +if TYPE_CHECKING: + from UM.Scene.SceneNode import SceneNode + from cura.BuildVolume import BuildVolume + + +def findNodePlacement(nodes_to_arrange: List["SceneNode"], build_volume: "BuildVolume", fixed_nodes: Optional[List["SceneNode"]] = None, factor = 10000) -> Tuple[bool, List[Item]]: + """ + Find placement for a set of scene nodes, but don't actually move them just yet. + :param nodes_to_arrange: The list of nodes that need to be moved. + :param build_volume: The build volume that we want to place the nodes in. It gets size & disallowed areas from this. + :param fixed_nodes: List of nods that should not be moved, but should be used when deciding where the others nodes + are placed. + :param factor: The library that we use is int based. This factor defines how accurate we want it to be. + + :return: tuple (found_solution_for_all, node_items) + WHERE + found_solution_for_all: Whether the algorithm found a place on the buildplate for all the objects + node_items: A list of the nodes return by libnest2d, which contain the new positions on the buildplate + """ + + machine_width = build_volume.getWidth() + machine_depth = build_volume.getDepth() + build_plate_bounding_box = Box(machine_width * factor, machine_depth * factor) + + if fixed_nodes is None: + fixed_nodes = [] + + # Add all the items we want to arrange + node_items = [] + for node in nodes_to_arrange: + hull_polygon = node.callDecoration("getConvexHull") + if not hull_polygon or hull_polygon.getPoints is None: + Logger.log("w", "Object {} cannot be arranged because it has no convex hull.".format(node.getName())) + continue + converted_points = [] + for point in hull_polygon.getPoints(): + converted_points.append(Point(point[0] * factor, point[1] * factor)) + item = Item(converted_points) + node_items.append(item) + + # Use a tiny margin for the build_plate_polygon (the nesting doesn't like overlapping disallowed areas) + half_machine_width = 0.5 * machine_width - 1 + half_machine_depth = 0.5 * machine_depth - 1 + build_plate_polygon = Polygon(numpy.array([ + [half_machine_width, -half_machine_depth], + [-half_machine_width, -half_machine_depth], + [-half_machine_width, half_machine_depth], + [half_machine_width, half_machine_depth] + ], numpy.float32)) + + disallowed_areas = build_volume.getDisallowedAreas() + num_disallowed_areas_added = 0 + for area in disallowed_areas: + converted_points = [] + + # Clip the disallowed areas so that they don't overlap the bounding box (The arranger chokes otherwise) + clipped_area = area.intersectionConvexHulls(build_plate_polygon) + + if clipped_area.getPoints() is not None: # numpy array has to be explicitly checked against None + for point in clipped_area.getPoints(): + converted_points.append(Point(point[0] * factor, point[1] * factor)) + + disallowed_area = Item(converted_points) + disallowed_area.markAsDisallowedAreaInBin(0) + node_items.append(disallowed_area) + num_disallowed_areas_added += 1 + + for node in fixed_nodes: + converted_points = [] + hull_polygon = node.callDecoration("getConvexHull") + + if hull_polygon is not None and hull_polygon.getPoints() is not None: # numpy array has to be explicitly checked against None + for point in hull_polygon.getPoints(): + converted_points.append(Point(point[0] * factor, point[1] * factor)) + item = Item(converted_points) + item.markAsFixedInBin(0) + node_items.append(item) + num_disallowed_areas_added += 1 + + config = NfpConfig() + config.accuracy = 1.0 + + num_bins = nest(node_items, build_plate_bounding_box, 10000, config) + + # Strip the fixed items (previously placed) and the disallowed areas from the results again. + node_items = list(filter(lambda item: not item.isFixed(), node_items)) + + found_solution_for_all = num_bins == 1 + + return found_solution_for_all, node_items + + +def arrange(nodes_to_arrange: List["SceneNode"], build_volume: "BuildVolume", fixed_nodes: Optional[List["SceneNode"]] = None, factor = 10000, add_new_nodes_in_scene: bool = False) -> bool: + """ + Find placement for a set of scene nodes, and move them by using a single grouped operation. + :param nodes_to_arrange: The list of nodes that need to be moved. + :param build_volume: The build volume that we want to place the nodes in. It gets size & disallowed areas from this. + :param fixed_nodes: List of nods that should not be moved, but should be used when deciding where the others nodes + are placed. + :param factor: The library that we use is int based. This factor defines how accuracte we want it to be. + :param add_new_nodes_in_scene: Whether to create new scene nodes before applying the transformations and rotations + + :return: found_solution_for_all: Whether the algorithm found a place on the buildplate for all the objects + """ + scene_root = Application.getInstance().getController().getScene().getRoot() + found_solution_for_all, node_items = findNodePlacement(nodes_to_arrange, build_volume, fixed_nodes, factor) + + not_fit_count = 0 + grouped_operation = GroupedOperation() + for node, node_item in zip(nodes_to_arrange, node_items): + if add_new_nodes_in_scene: + grouped_operation.addOperation(AddSceneNodeOperation(node, scene_root)) + + if node_item.binId() == 0: + # We found a spot for it + rotation_matrix = Matrix() + rotation_matrix.setByRotationAxis(node_item.rotation(), Vector(0, -1, 0)) + grouped_operation.addOperation(RotateOperation(node, Quaternion.fromMatrix(rotation_matrix))) + grouped_operation.addOperation(TranslateOperation(node, Vector(node_item.translation().x() / factor, 0, + node_item.translation().y() / factor))) + else: + # We didn't find a spot + grouped_operation.addOperation( + TranslateOperation(node, Vector(200, node.getWorldPosition().y, -not_fit_count * 20), set_position = True)) + not_fit_count += 1 + grouped_operation.push() + + return found_solution_for_all diff --git a/cura/BuildVolume.py b/cura/BuildVolume.py index 373f708389..7c843fcdea 100755 --- a/cura/BuildVolume.py +++ b/cura/BuildVolume.py @@ -180,12 +180,21 @@ class BuildVolume(SceneNode): def setWidth(self, width: float) -> None: self._width = width + def getWidth(self) -> float: + return self._width + def setHeight(self, height: float) -> None: self._height = height + def getHeight(self) -> float: + return self._height + def setDepth(self, depth: float) -> None: self._depth = depth + def getDepth(self) -> float: + return self._depth + def setShape(self, shape: str) -> None: if shape: self._shape = shape @@ -782,7 +791,9 @@ class BuildVolume(SceneNode): break if self._global_container_stack.getProperty("prime_tower_brim_enable", "value") and self._global_container_stack.getProperty("adhesion_type", "value") != "raft": brim_size = self._calculateBedAdhesionSize(used_extruders, "brim") - prime_tower_areas[extruder_id][area_index] = prime_tower_area.getMinkowskiHull(Polygon.approximatedCircle(brim_size)) + # Use 2x the brim size, since we need 1x brim size distance due to the object brim and another + # times the brim due to the brim of the prime tower + prime_tower_areas[extruder_id][area_index] = prime_tower_area.getMinkowskiHull(Polygon.approximatedCircle(2 * brim_size, num_segments = 24)) if not prime_tower_collision: result_areas[extruder_id].extend(prime_tower_areas[extruder_id]) result_areas_no_brim[extruder_id].extend(prime_tower_areas[extruder_id]) @@ -834,7 +845,7 @@ class BuildVolume(SceneNode): prime_tower_y += brim_size radius = prime_tower_size / 2 - prime_tower_area = Polygon.approximatedCircle(radius) + prime_tower_area = Polygon.approximatedCircle(radius, num_segments = 24) prime_tower_area = prime_tower_area.translate(prime_tower_x - radius, prime_tower_y - radius) prime_tower_area = prime_tower_area.getMinkowskiHull(Polygon.approximatedCircle(0)) diff --git a/cura/CuraApplication.py b/cura/CuraApplication.py index 57f238a9d7..08cc644025 100755 --- a/cura/CuraApplication.py +++ b/cura/CuraApplication.py @@ -36,6 +36,7 @@ from UM.Scene.Camera import Camera from UM.Scene.GroupDecorator import GroupDecorator from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator from UM.Scene.SceneNode import SceneNode +from UM.Scene.SceneNodeSettings import SceneNodeSettings from UM.Scene.Selection import Selection from UM.Scene.ToolHandle import ToolHandle from UM.Settings.ContainerRegistry import ContainerRegistry @@ -52,7 +53,7 @@ from cura.API.Account import Account from cura.Arranging.Arrange import Arrange from cura.Arranging.ArrangeObjectsAllBuildPlatesJob import ArrangeObjectsAllBuildPlatesJob from cura.Arranging.ArrangeObjectsJob import ArrangeObjectsJob -from cura.Arranging.ShapeArray import ShapeArray +from cura.Arranging.Nest2DArrange import arrange from cura.Machines.MachineErrorChecker import MachineErrorChecker from cura.Machines.Models.BuildPlateModel import BuildPlateModel from cura.Machines.Models.CustomQualityProfilesDropDownMenuModel import CustomQualityProfilesDropDownMenuModel @@ -801,6 +802,8 @@ class CuraApplication(QtApplication): self._setLoadingHint(self._i18n_catalog.i18nc("@info:progress", "Initializing build volume...")) root = self.getController().getScene().getRoot() self._volume = BuildVolume.BuildVolume(self, root) + + # Ensure that the old style arranger still works. Arrange.build_volume = self._volume # initialize info objects @@ -1379,6 +1382,7 @@ class CuraApplication(QtApplication): def arrangeAll(self) -> None: nodes_to_arrange = [] active_build_plate = self.getMultiBuildPlateModel().activeBuildPlate + locked_nodes = [] for node in DepthFirstIterator(self.getController().getScene().getRoot()): if not isinstance(node, SceneNode): continue @@ -1400,8 +1404,12 @@ class CuraApplication(QtApplication): # Skip nodes that are too big bounding_box = node.getBoundingBox() if bounding_box is None or bounding_box.width < self._volume.getBoundingBox().width or bounding_box.depth < self._volume.getBoundingBox().depth: - nodes_to_arrange.append(node) - self.arrange(nodes_to_arrange, fixed_nodes = []) + # Arrange only the unlocked nodes and keep the locked ones in place + if UM.Util.parseBool(node.getSetting(SceneNodeSettings.LockPosition)): + locked_nodes.append(node) + else: + nodes_to_arrange.append(node) + self.arrange(nodes_to_arrange, locked_nodes) def arrange(self, nodes: List[SceneNode], fixed_nodes: List[SceneNode]) -> None: """Arrange a set of nodes given a set of fixed nodes @@ -1815,17 +1823,21 @@ class CuraApplication(QtApplication): for node_ in DepthFirstIterator(root): if node_.callDecoration("isSliceable") and node_.callDecoration("getBuildPlateNumber") == target_build_plate: fixed_nodes.append(node_) - 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 = fixed_nodes) - min_offset = 8 + default_extruder_position = self.getMachineManager().defaultExtruderPosition default_extruder_id = self._global_container_stack.extruderList[int(default_extruder_position)].getId() select_models_on_load = self.getPreferences().getValue("cura/select_models_on_load") - for original_node in nodes: + nodes_to_arrange = [] # type: List[CuraSceneNode] + + fixed_nodes = [] + for node_ in DepthFirstIterator(self.getController().getScene().getRoot()): + # Only count sliceable objects + if node_.callDecoration("isSliceable"): + fixed_nodes.append(node_) + for original_node in nodes: # Create a CuraSceneNode just if the original node is not that type if isinstance(original_node, CuraSceneNode): node = original_node @@ -1833,8 +1845,8 @@ class CuraApplication(QtApplication): node = CuraSceneNode() node.setMeshData(original_node.getMeshData()) - #Setting meshdata does not apply scaling. - if(original_node.getScale() != Vector(1.0, 1.0, 1.0)): + # Setting meshdata does not apply scaling. + if original_node.getScale() != Vector(1.0, 1.0, 1.0): node.scale(original_node.getScale()) node.setSelectable(True) @@ -1865,19 +1877,15 @@ class CuraApplication(QtApplication): if file_extension != "3mf": if node.callDecoration("isSliceable"): - # Only check position if it's not already blatantly obvious that it won't fit. - if node.getBoundingBox() is None or self._volume.getBoundingBox() is None or node.getBoundingBox().width < self._volume.getBoundingBox().width or node.getBoundingBox().depth < self._volume.getBoundingBox().depth: - # Find node location - offset_shape_arr, hull_shape_arr = ShapeArray.fromNode(node, min_offset = min_offset) + # Ensure that the bottom of the bounding box is on the build plate + if node.getBoundingBox(): + center_y = node.getWorldPosition().y - node.getBoundingBox().bottom + else: + center_y = 0 - # If a model is to small then it will not contain any points - if offset_shape_arr is None and hull_shape_arr is None: - Message(self._i18n_catalog.i18nc("@info:status", "The selected model was too small to load."), - title = self._i18n_catalog.i18nc("@info:title", "Warning")).show() - return + node.translate(Vector(0, center_y, 0)) - # Step is for skipping tests to make it a lot faster. it also makes the outcome somewhat rougher - arranger.findNodePlacement(node, offset_shape_arr, hull_shape_arr, step = 10) + nodes_to_arrange.append(node) # This node is deep copied from some other node which already has a BuildPlateDecorator, but the deepcopy # of BuildPlateDecorator produces one that's associated with build plate -1. So, here we need to check if @@ -1897,6 +1905,8 @@ class CuraApplication(QtApplication): if select_models_on_load: Selection.add(node) + arrange(nodes_to_arrange, self.getBuildVolume(), fixed_nodes) + self.fileCompleted.emit(file_name) def addNonSliceableExtension(self, extension): diff --git a/cura/MultiplyObjectsJob.py b/cura/MultiplyObjectsJob.py index 7507f2520e..1ba78edacf 100644 --- a/cura/MultiplyObjectsJob.py +++ b/cura/MultiplyObjectsJob.py @@ -4,21 +4,16 @@ import copy from typing import List -from PyQt5.QtCore import QCoreApplication - +from UM.Application import Application from UM.Job import Job -from UM.Operations.GroupedOperation import GroupedOperation from UM.Message import Message +from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator from UM.Scene.SceneNode import SceneNode from UM.i18n import i18nCatalog +from cura.Arranging.Nest2DArrange import arrange + i18n_catalog = i18nCatalog("cura") -from cura.Arranging.Arrange import Arrange -from cura.Arranging.ShapeArray import ShapeArray - -from UM.Application import Application -from UM.Operations.AddSceneNodeOperation import AddSceneNodeOperation - class MultiplyObjectsJob(Job): def __init__(self, objects, count, min_offset = 8): @@ -28,28 +23,27 @@ class MultiplyObjectsJob(Job): self._min_offset = min_offset def run(self) -> None: - status_message = Message(i18n_catalog.i18nc("@info:status", "Multiplying and placing objects"), lifetime=0, - dismissable=False, progress=0, title = i18n_catalog.i18nc("@info:title", "Placing Objects")) + status_message = Message(i18n_catalog.i18nc("@info:status", "Multiplying and placing objects"), lifetime = 0, + dismissable = False, progress = 0, + title = i18n_catalog.i18nc("@info:title", "Placing Objects")) status_message.show() scene = Application.getInstance().getController().getScene() - total_progress = len(self._objects) * self._count - current_progress = 0 - global_container_stack = Application.getInstance().getGlobalContainerStack() if global_container_stack is None: return # We can't do anything in this case. - machine_width = global_container_stack.getProperty("machine_width", "value") - machine_depth = global_container_stack.getProperty("machine_depth", "value") root = scene.getRoot() - scale = 0.5 - arranger = Arrange.create(x = machine_width, y = machine_depth, scene_root = root, scale = scale, min_offset = self._min_offset) + processed_nodes = [] # type: List[SceneNode] nodes = [] - not_fit_count = 0 - found_solution_for_all = False + fixed_nodes = [] + for node_ in DepthFirstIterator(root): + # Only count sliceable objects + if node_.callDecoration("isSliceable"): + fixed_nodes.append(node_) + for node in self._objects: # If object is part of a group, multiply group current_node = node @@ -60,31 +54,8 @@ class MultiplyObjectsJob(Job): continue processed_nodes.append(current_node) - node_too_big = False - 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, scale = scale) - else: - node_too_big = True - - found_solution_for_all = True - arranger.resetLastPriority() for _ in range(self._count): - # 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 offset_shape_arr is not None and hull_shape_arr is not None: - solution_found = arranger.findNodePlacement(new_node, offset_shape_arr, hull_shape_arr) - else: - # The node has no shape, so no need to arrange it. The solution is simple: Do nothing. - solution_found = True - - if node_too_big or not solution_found: - found_solution_for_all = False - new_location = new_node.getPosition() - new_location = new_location.set(z = - not_fit_count * 20) - new_node.setPosition(new_location) - not_fit_count += 1 # Same build plate build_plate_number = current_node.callDecoration("getBuildPlateNumber") @@ -93,20 +64,15 @@ class MultiplyObjectsJob(Job): child.callDecoration("setBuildPlateNumber", build_plate_number) nodes.append(new_node) - current_progress += 1 - status_message.setProgress((current_progress / total_progress) * 100) - QCoreApplication.processEvents() - Job.yieldThread() - QCoreApplication.processEvents() - Job.yieldThread() + found_solution_for_all = True if nodes: - operation = GroupedOperation() - for new_node in nodes: - operation.addOperation(AddSceneNodeOperation(new_node, current_node.getParent())) - operation.push() + found_solution_for_all = arrange(nodes, Application.getInstance().getBuildVolume(), fixed_nodes, + factor = 10000, add_new_nodes_in_scene = True) status_message.hide() if not found_solution_for_all: - no_full_solution_message = Message(i18n_catalog.i18nc("@info:status", "Unable to find a location within the build volume for all objects"), title = i18n_catalog.i18nc("@info:title", "Placing Object")) + no_full_solution_message = Message( + i18n_catalog.i18nc("@info:status", "Unable to find a location within the build volume for all objects"), + title = i18n_catalog.i18nc("@info:title", "Placing Object")) no_full_solution_message.show() diff --git a/cura/OAuth2/AuthorizationHelpers.py b/cura/OAuth2/AuthorizationHelpers.py index e825afd2a9..b4ea2d8382 100644 --- a/cura/OAuth2/AuthorizationHelpers.py +++ b/cura/OAuth2/AuthorizationHelpers.py @@ -1,11 +1,11 @@ -# Copyright (c) 2019 Ultimaker B.V. +# Copyright (c) 2020 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. from datetime import datetime import json import random from hashlib import sha512 from base64 import b64encode -from typing import Optional +from typing import Optional, Any, Dict, Tuple import requests @@ -16,6 +16,7 @@ from cura.OAuth2.Models import AuthenticationResponse, UserProfile, OAuth2Settin catalog = i18nCatalog("cura") TOKEN_TIMESTAMP_FORMAT = "%Y-%m-%d %H:%M:%S" + class AuthorizationHelpers: """Class containing several helpers to deal with the authorization flow.""" @@ -121,10 +122,13 @@ class AuthorizationHelpers: if not user_data or not isinstance(user_data, dict): Logger.log("w", "Could not parse user data from token: %s", user_data) return None + return UserProfile( user_id = user_data["user_id"], username = user_data["username"], - profile_image_url = user_data.get("profile_image_url", "") + profile_image_url = user_data.get("profile_image_url", ""), + organization_id = user_data.get("organization", {}).get("organization_id", ""), + subscriptions = user_data.get("subscriptions", []) ) @staticmethod diff --git a/cura/OAuth2/Models.py b/cura/OAuth2/Models.py index 93b44e8057..f49fdc1421 100644 --- a/cura/OAuth2/Models.py +++ b/cura/OAuth2/Models.py @@ -1,6 +1,6 @@ -# Copyright (c) 2019 Ultimaker B.V. +# Copyright (c) 2020 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. -from typing import Optional, Dict, Any +from typing import Optional, Dict, Any, List class BaseModel: @@ -27,6 +27,8 @@ class UserProfile(BaseModel): user_id = None # type: Optional[str] username = None # type: Optional[str] profile_image_url = None # type: Optional[str] + organization_id = None # type: Optional[str] + subscriptions = None # type: Optional[List[Dict[str, Any]]] class AuthenticationResponse(BaseModel): diff --git a/cura/OneAtATimeIterator.py b/cura/OneAtATimeIterator.py index def0dac4fe..8bdddba554 100644 --- a/cura/OneAtATimeIterator.py +++ b/cura/OneAtATimeIterator.py @@ -27,10 +27,13 @@ class OneAtATimeIterator(Iterator.Iterator): if not issubclass(type(node), SceneNode): continue + # Node can't be printed, so don't bother sending it. + if getattr(node, "_outside_buildarea", False): + continue + if node.callDecoration("getConvexHull"): node_list.append(node) - if len(node_list) < 2: self._node_stack = node_list[:] return @@ -38,8 +41,8 @@ class OneAtATimeIterator(Iterator.Iterator): # Copy the list 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] + # Initialise the hit map (pre-compute all hits between all objects) + self._hit_map = [[self._checkHit(i, j) for i in node_list] for j in node_list] # Check if we have to files that block each other. If this is the case, there is no solution! for a in range(0, len(node_list)): diff --git a/cura/Scene/ConvexHullDecorator.py b/cura/Scene/ConvexHullDecorator.py index a4228ce422..36697b7c57 100644 --- a/cura/Scene/ConvexHullDecorator.py +++ b/cura/Scene/ConvexHullDecorator.py @@ -269,7 +269,7 @@ class ConvexHullDecorator(SceneNodeDecorator): if mesh is None: return Polygon([]) # Node has no mesh data, so just return an empty Polygon. - world_transform = self._node.getWorldTransformation(copy = False) + world_transform = self._node.getWorldTransformation(copy = True) # Check the cache if mesh is self._2d_convex_hull_mesh and world_transform == self._2d_convex_hull_mesh_world_transform: diff --git a/cura/Settings/CuraContainerRegistry.py b/cura/Settings/CuraContainerRegistry.py index c5d46f9a79..2527d761bd 100644 --- a/cura/Settings/CuraContainerRegistry.py +++ b/cura/Settings/CuraContainerRegistry.py @@ -5,7 +5,7 @@ import os import re import configparser -from typing import Any, cast, Dict, Optional, List, Union +from typing import Any, cast, Dict, Optional, List, Union, Tuple from PyQt5.QtWidgets import QMessageBox from UM.Decorators import override @@ -179,7 +179,7 @@ class CuraContainerRegistry(ContainerRegistry): """Imports a profile from a file :param file_name: The full path and filename of the profile to import. - :return: Dict with a 'status' key containing the string 'ok' or 'error', + :return: Dict with a 'status' key containing the string 'ok', 'warning' or 'error', and a 'message' key containing a message for the user. """ @@ -305,6 +305,7 @@ class CuraContainerRegistry(ContainerRegistry): # Import all profiles profile_ids_added = [] # type: List[str] + additional_message = None for profile_index, profile in enumerate(profile_or_list): if profile_index == 0: # This is assumed to be the global profile @@ -323,18 +324,26 @@ class CuraContainerRegistry(ContainerRegistry): else: # More extruders in the imported file than in the machine. continue # Delete the additional profiles. - result = self._configureProfile(profile, profile_id, new_name, expected_machine_definition) - if result is not None: - # Remove any profiles that did got added. - for profile_id in profile_ids_added: + configuration_successful, message = self._configureProfile(profile, profile_id, new_name, expected_machine_definition) + if configuration_successful: + additional_message = message + else: + # Remove any profiles that were added. + for profile_id in profile_ids_added + [profile.getId()]: self.removeContainer(profile_id) - + if not message: + message = "" return {"status": "error", "message": catalog.i18nc( - "@info:status Don't translate the XML tag !", - "Failed to import profile from {0}:", - file_name) + " " + result} + "@info:status Don't translate the XML tag !", + "Failed to import profile from {0}:", + file_name) + " " + message} profile_ids_added.append(profile.getId()) - return {"status": "ok", "message": catalog.i18nc("@info:status", "Successfully imported profile {0}", profile_or_list[0].getName())} + result_status = "ok" + success_message = catalog.i18nc("@info:status", "Successfully imported profile {0}.", profile_or_list[0].getName()) + if additional_message: + result_status = "warning" + success_message += additional_message + return {"status": result_status, "message": success_message} # This message is throw when the profile reader doesn't find any profile in the file return {"status": "error", "message": catalog.i18nc("@info:status", "File {0} does not contain any valid profile.", file_name)} @@ -395,14 +404,18 @@ class CuraContainerRegistry(ContainerRegistry): return False return True - def _configureProfile(self, profile: InstanceContainer, id_seed: str, new_name: str, machine_definition_id: str) -> Optional[str]: + def _configureProfile(self, profile: InstanceContainer, id_seed: str, new_name: str, machine_definition_id: str) -> Tuple[bool, Optional[str]]: """Update an imported profile to match the current machine configuration. :param profile: The profile to configure. :param id_seed: The base ID for the profile. May be changed so it does not conflict with existing containers. :param new_name: The new name for the profile. - :return: None if configuring was successful or an error message if an error occurred. + :returns: tuple (configuration_successful, message) + WHERE + bool configuration_successful: Whether the process of configuring the profile was successful + optional str message: A message indicating the outcome of configuring the profile. If the configuration + is successful, this message can be None or contain a warning """ profile.setDirty(True) # Ensure the profiles are correctly saved @@ -423,26 +436,39 @@ class CuraContainerRegistry(ContainerRegistry): quality_type = profile.getMetaDataEntry("quality_type") if not quality_type: - return catalog.i18nc("@info:status", "Profile is missing a quality type.") + return False, catalog.i18nc("@info:status", "Profile is missing a quality type.") global_stack = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack() - if global_stack is None: - return None + if not global_stack: + return False, catalog.i18nc("@info:status", "Global stack is missing.") + definition_id = ContainerTree.getInstance().machines[global_stack.definition.getId()].quality_definition profile.setDefinition(definition_id) + if not self.addContainer(profile): + return False, catalog.i18nc("@info:status", "Unable to add the profile.") + + # "not_supported" profiles can be imported. + if quality_type == empty_quality_container.getMetaDataEntry("quality_type"): + return True, None + # Check to make sure the imported profile actually makes sense in context of the current configuration. # This prevents issues where importing a "draft" profile for a machine without "draft" qualities would report as # successfully imported but then fail to show up. - quality_group_dict = ContainerTree.getInstance().getCurrentQualityGroups() - # "not_supported" profiles can be imported. - if quality_type != empty_quality_container.getMetaDataEntry("quality_type") and quality_type not in quality_group_dict: - return catalog.i18nc("@info:status", "Could not find a quality type {0} for the current configuration.", quality_type) + available_quality_groups_dict = {name: quality_group for name, quality_group in ContainerTree.getInstance().getCurrentQualityGroups().items() if quality_group.is_available} + all_quality_groups_dict = ContainerTree.getInstance().getCurrentQualityGroups() - if not self.addContainer(profile): - return catalog.i18nc("@info:status", "Unable to add the profile.") + # If the quality type doesn't exist at all in the quality_groups of this machine, reject the profile + if quality_type not in all_quality_groups_dict: + return False, catalog.i18nc("@info:status", "Quality type '{0}' is not compatible with the current active machine definition '{1}'.", quality_type, definition_id) - return None + # If the quality_type exists in the quality_groups of this printer but it is not available with the current + # machine configuration (e.g. not available for the selected nozzles), accept it with a warning + if quality_type not in available_quality_groups_dict: + return True, "\n\n" + catalog.i18nc("@info:status", "Warning: The profile is not visible because its quality type '{0}' is not available for the current configuration. " + "Switch to a material/nozzle combination that can use this quality type.", quality_type) + + return True, None @override(ContainerRegistry) def saveDirtyContainers(self) -> None: diff --git a/cura/Settings/MachineManager.py b/cura/Settings/MachineManager.py index 4b0308a861..58fd8171b5 100755 --- a/cura/Settings/MachineManager.py +++ b/cura/Settings/MachineManager.py @@ -369,7 +369,8 @@ class MachineManager(QObject): material_node = variant_node.materials.get(extruder.material.getMetaDataEntry("base_file")) if material_node is None: Logger.log("w", "An extruder has an unknown material, switching it to the preferred material") - self.setMaterialById(extruder.getMetaDataEntry("position"), machine_node.preferred_material) + if not self.setMaterialById(extruder.getMetaDataEntry("position"), machine_node.preferred_material): + Logger.log("w", "Failed to switch to %s keeping old material instead", machine_node.preferred_material) @staticmethod @@ -997,6 +998,11 @@ class MachineManager(QObject): self.activeMaterialChanged.emit() self.activeIntentChanged.emit() + # Force an update of resolve values + property_names = ["resolve", "validationState"] + for setting_key in self._global_container_stack.getAllKeys(): + self._global_container_stack.propertiesChanged.emit(setting_key, property_names) + def _onMaterialNameChanged(self) -> None: self.activeMaterialChanged.emit() @@ -1152,6 +1158,7 @@ class MachineManager(QObject): extruder.qualityChanges = quality_changes_container self.setIntentByCategory(quality_changes_group.intent_category) + self._reCalculateNumUserSettings() self.activeQualityGroupChanged.emit() self.activeQualityChangesGroupChanged.emit() @@ -1452,17 +1459,21 @@ class MachineManager(QObject): self.updateMaterialWithVariant(None) # Update all materials self._updateQualityWithMaterial() - @pyqtSlot(str, str) - def setMaterialById(self, position: str, root_material_id: str) -> None: + @pyqtSlot(str, str, result = bool) + def setMaterialById(self, position: str, root_material_id: str) -> bool: if self._global_container_stack is None: - return + return False machine_definition_id = self._global_container_stack.definition.id position = str(position) extruder_stack = self._global_container_stack.extruderList[int(position)] nozzle_name = extruder_stack.variant.getName() - material_node = ContainerTree.getInstance().machines[machine_definition_id].variants[nozzle_name].materials[root_material_id] - self.setMaterial(position, material_node) + + materials = ContainerTree.getInstance().machines[machine_definition_id].variants[nozzle_name].materials + if root_material_id in materials: + self.setMaterial(position, materials[root_material_id]) + return True + return False @pyqtSlot(str, "QVariant") def setMaterial(self, position: str, container_node, global_stack: Optional["GlobalStack"] = None) -> None: diff --git a/cura/UltimakerCloud/UltimakerCloudScope.py b/cura/UltimakerCloud/UltimakerCloudScope.py index 0e9adaf2e7..5477423099 100644 --- a/cura/UltimakerCloud/UltimakerCloudScope.py +++ b/cura/UltimakerCloud/UltimakerCloudScope.py @@ -7,10 +7,9 @@ from cura.CuraApplication import CuraApplication class UltimakerCloudScope(DefaultUserAgentScope): - """Add an Authorization header to the request for Ultimaker Cloud Api requests. - - When the user is not logged in or a token is not available, a warning will be logged - Also add the user agent headers (see DefaultUserAgentScope) + """ + Add an Authorization header to the request for Ultimaker Cloud Api requests, if available. + Also add the user agent headers (see DefaultUserAgentScope). """ def __init__(self, application: CuraApplication): @@ -22,7 +21,7 @@ class UltimakerCloudScope(DefaultUserAgentScope): super().requestHook(request) token = self._account.accessToken if not self._account.isLoggedIn or token is None: - Logger.warning("Cannot add authorization to Cloud Api request") + Logger.debug("User is not logged in for Cloud API request to {url}".format(url = request.url().toDisplayString())) return header_dict = { diff --git a/cura_app.py b/cura_app.py index 61fd544f8f..cc8a1d575c 100755 --- a/cura_app.py +++ b/cura_app.py @@ -22,6 +22,7 @@ import os # tries to create PyQt objects on a non-main thread. import Arcus # @UnusedImport import Savitar # @UnusedImport +import pynest2d # @UnusedImport from PyQt5.QtNetwork import QSslConfiguration, QSslSocket diff --git a/plugins/3MFReader/ThreeMFReader.py b/plugins/3MFReader/ThreeMFReader.py index 2c29728d66..2e3f5630c1 100755 --- a/plugins/3MFReader/ThreeMFReader.py +++ b/plugins/3MFReader/ThreeMFReader.py @@ -19,6 +19,7 @@ from UM.Scene.SceneNode import SceneNode # For typing. from cura.CuraApplication import CuraApplication from cura.Machines.ContainerTree import ContainerTree from cura.Scene.BuildPlateDecorator import BuildPlateDecorator +from cura.Scene.ConvexHullDecorator import ConvexHullDecorator from cura.Scene.CuraSceneNode import CuraSceneNode from cura.Scene.SliceableObjectDecorator import SliceableObjectDecorator from cura.Scene.ZOffsetDecorator import ZOffsetDecorator @@ -108,6 +109,10 @@ class ThreeMFReader(MeshReader): um_node = CuraSceneNode() # This adds a SettingOverrideDecorator um_node.addDecorator(BuildPlateDecorator(active_build_plate)) + try: + um_node.addDecorator(ConvexHullDecorator()) + except: + pass um_node.setName(node_name) um_node.setId(node_id) transformation = self._createMatrixFromTransformationString(savitar_node.getTransformation()) diff --git a/plugins/3MFReader/plugin.json b/plugins/3MFReader/plugin.json index ec0b7bf079..b80d83ae01 100644 --- a/plugins/3MFReader/plugin.json +++ b/plugins/3MFReader/plugin.json @@ -3,6 +3,6 @@ "author": "Ultimaker B.V.", "version": "1.0.1", "description": "Provides support for reading 3MF files.", - "api": "7.3.0", + "api": "7.4.0", "i18n-catalog": "cura" } diff --git a/plugins/3MFWriter/plugin.json b/plugins/3MFWriter/plugin.json index ce3ccadc9b..18611f84f0 100644 --- a/plugins/3MFWriter/plugin.json +++ b/plugins/3MFWriter/plugin.json @@ -3,6 +3,6 @@ "author": "Ultimaker B.V.", "version": "1.0.1", "description": "Provides support for writing 3MF files.", - "api": "7.3.0", + "api": "7.4.0", "i18n-catalog": "cura" } diff --git a/plugins/AMFReader/plugin.json b/plugins/AMFReader/plugin.json index 80f9a10940..632a2dcd7e 100644 --- a/plugins/AMFReader/plugin.json +++ b/plugins/AMFReader/plugin.json @@ -3,5 +3,5 @@ "author": "fieldOfView", "version": "1.0.0", "description": "Provides support for reading AMF files.", - "api": "7.3.0" + "api": "7.4.0" } diff --git a/plugins/CuraDrive/plugin.json b/plugins/CuraDrive/plugin.json index 9160127fac..14c3b45b6d 100644 --- a/plugins/CuraDrive/plugin.json +++ b/plugins/CuraDrive/plugin.json @@ -3,6 +3,6 @@ "author": "Ultimaker B.V.", "description": "Backup and restore your configuration.", "version": "1.2.0", - "api": "7.3.0", + "api": "7.4.0", "i18n-catalog": "cura" } diff --git a/plugins/CuraEngineBackend/CuraEngineBackend.py b/plugins/CuraEngineBackend/CuraEngineBackend.py index 519d302618..4b196f7b5d 100755 --- a/plugins/CuraEngineBackend/CuraEngineBackend.py +++ b/plugins/CuraEngineBackend/CuraEngineBackend.py @@ -82,7 +82,7 @@ class CuraEngineBackend(QObject, Backend): default_engine_location = execpath break - self._application = CuraApplication.getInstance() #type: CuraApplication + application = CuraApplication.getInstance() #type: CuraApplication self._multi_build_plate_model = None #type: Optional[MultiBuildPlateModel] self._machine_error_checker = None #type: Optional[MachineErrorChecker] @@ -92,7 +92,7 @@ class CuraEngineBackend(QObject, Backend): Logger.log("i", "Found CuraEngine at: %s", default_engine_location) default_engine_location = os.path.abspath(default_engine_location) - self._application.getPreferences().addPreference("backend/location", default_engine_location) + application.getPreferences().addPreference("backend/location", default_engine_location) # Workaround to disable layer view processing if layer view is not active. self._layer_view_active = False #type: bool @@ -101,7 +101,7 @@ class CuraEngineBackend(QObject, Backend): self._stored_layer_data = [] # type: List[Arcus.PythonMessage] 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() #type: Scene + self._scene = application.getController().getScene() #type: Scene self._scene.sceneChanged.connect(self._onSceneChanged) # Triggers for auto-slicing. Auto-slicing is triggered as follows: @@ -141,7 +141,7 @@ class CuraEngineBackend(QObject, Backend): self._slice_start_time = None #type: Optional[float] self._is_disabled = False #type: bool - self._application.getPreferences().addPreference("general/auto_slice", False) + application.getPreferences().addPreference("general/auto_slice", False) self._use_timer = False #type: bool # When you update a setting and other settings get changed through inheritance, many propertyChanged signals are fired. @@ -151,19 +151,20 @@ class CuraEngineBackend(QObject, Backend): self._change_timer.setSingleShot(True) self._change_timer.setInterval(500) self.determineAutoSlicing() - self._application.getPreferences().preferenceChanged.connect(self._onPreferencesChanged) + application.getPreferences().preferenceChanged.connect(self._onPreferencesChanged) - self._application.initializationFinished.connect(self.initialize) + application.initializationFinished.connect(self.initialize) def initialize(self) -> None: - self._multi_build_plate_model = self._application.getMultiBuildPlateModel() + application = CuraApplication.getInstance() + self._multi_build_plate_model = application.getMultiBuildPlateModel() - self._application.getController().activeViewChanged.connect(self._onActiveViewChanged) + application.getController().activeViewChanged.connect(self._onActiveViewChanged) if self._multi_build_plate_model: self._multi_build_plate_model.activeBuildPlateChanged.connect(self._onActiveViewChanged) - self._application.getMachineManager().globalContainerChanged.connect(self._onGlobalStackChanged) + application.getMachineManager().globalContainerChanged.connect(self._onGlobalStackChanged) self._onGlobalStackChanged() # extruder enable / disable. Actually wanted to use machine manager here, but the initialization order causes it to crash @@ -173,10 +174,10 @@ class CuraEngineBackend(QObject, Backend): self.backendConnected.connect(self._onBackendConnected) # When a tool operation is in progress, don't slice. So we need to listen for tool operations. - self._application.getController().toolOperationStarted.connect(self._onToolOperationStarted) - self._application.getController().toolOperationStopped.connect(self._onToolOperationStopped) + application.getController().toolOperationStarted.connect(self._onToolOperationStarted) + application.getController().toolOperationStopped.connect(self._onToolOperationStopped) - self._machine_error_checker = self._application.getMachineErrorChecker() + self._machine_error_checker = application.getMachineErrorChecker() self._machine_error_checker.errorCheckFinished.connect(self._onStackErrorCheckFinished) def close(self) -> None: @@ -195,7 +196,7 @@ class CuraEngineBackend(QObject, Backend): This is useful for debugging and used to actually start the engine. :return: list of commands and args / parameters. """ - command = [self._application.getPreferences().getValue("backend/location"), "connect", "127.0.0.1:{0}".format(self._port), ""] + command = [CuraApplication.getInstance().getPreferences().getValue("backend/location"), "connect", "127.0.0.1:{0}".format(self._port), ""] 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.") @@ -259,7 +260,8 @@ class CuraEngineBackend(QObject, Backend): self._scene.gcode_dict = {} #type: ignore #Because we are creating the missing attribute here. # see if we really have to slice - active_build_plate = self._application.getMultiBuildPlateModel().activeBuildPlate + application = CuraApplication.getInstance() + active_build_plate = application.getMultiBuildPlateModel().activeBuildPlate 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) num_objects = self._numObjectsPerBuildPlate() @@ -274,8 +276,8 @@ class CuraEngineBackend(QObject, Backend): self.slice() return self._stored_optimized_layer_data[build_plate_to_be_sliced] = [] - if self._application.getPrintInformation() and build_plate_to_be_sliced == active_build_plate: - self._application.getPrintInformation().setToZeroPrintInformation(build_plate_to_be_sliced) + if application.getPrintInformation() and build_plate_to_be_sliced == active_build_plate: + application.getPrintInformation().setToZeroPrintInformation(build_plate_to_be_sliced) if self._process is None: # type: ignore self._createSocket() @@ -314,7 +316,7 @@ class CuraEngineBackend(QObject, Backend): self.processingProgress.emit(0) Logger.log("d", "Attempting to kill the engine process") - if self._application.getUseExternalBackend(): + if CuraApplication.getInstance().getUseExternalBackend(): return if self._process is not None: # type: ignore @@ -350,8 +352,9 @@ class CuraEngineBackend(QObject, Backend): self.backendError.emit(job) return + application = CuraApplication.getInstance() if job.getResult() == StartJobResult.MaterialIncompatible: - if self._application.platformActivity: + if application.platformActivity: 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")) self._error_message.show() @@ -362,7 +365,7 @@ class CuraEngineBackend(QObject, Backend): return if job.getResult() == StartJobResult.SettingError: - if self._application.platformActivity: + if application.platformActivity: if not self._global_container_stack: Logger.log("w", "Global container stack not assigned to CuraEngineBackend!") return @@ -394,7 +397,7 @@ class CuraEngineBackend(QObject, Backend): elif job.getResult() == StartJobResult.ObjectSettingError: errors = {} - for node in DepthFirstIterator(self._application.getController().getScene().getRoot()): + for node in DepthFirstIterator(application.getController().getScene().getRoot()): stack = node.callDecoration("getStack") if not stack: continue @@ -415,7 +418,7 @@ class CuraEngineBackend(QObject, Backend): return if job.getResult() == StartJobResult.BuildPlateError: - if self._application.platformActivity: + if application.platformActivity: 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")) self._error_message.show() @@ -433,7 +436,7 @@ class CuraEngineBackend(QObject, Backend): return if job.getResult() == StartJobResult.NothingToSlice: - if self._application.platformActivity: + if application.platformActivity: self._error_message = Message(catalog.i18nc("@info:status", "Please review settings and check if your models:" "\n- Fit within the build volume" "\n- Are assigned to an enabled extruder" @@ -466,7 +469,7 @@ class CuraEngineBackend(QObject, Backend): enable_timer = True self._is_disabled = False - if not self._application.getPreferences().getValue("general/auto_slice"): + if not CuraApplication.getInstance().getPreferences().getValue("general/auto_slice"): enable_timer = False for node in DepthFirstIterator(self._scene.getRoot()): if node.callDecoration("isBlockSlicing"): @@ -560,7 +563,7 @@ class CuraEngineBackend(QObject, Backend): :param error: The exception that occurred. """ - if self._application.isShuttingDown(): + if CuraApplication.getInstance().isShuttingDown(): return super()._onSocketError(error) @@ -600,7 +603,7 @@ class CuraEngineBackend(QObject, Backend): cast(SceneNode, node.getParent()).removeChild(node) def markSliceAll(self) -> None: - for build_plate_number in range(self._application.getMultiBuildPlateModel().maxBuildPlate + 1): + for build_plate_number in range(CuraApplication.getInstance().getMultiBuildPlateModel().maxBuildPlate + 1): if build_plate_number not in self._build_plates_to_be_sliced: self._build_plates_to_be_sliced.append(build_plate_number) @@ -696,12 +699,13 @@ class CuraEngineBackend(QObject, Backend): gcode_list = self._scene.gcode_dict[self._start_slice_job_build_plate] #type: ignore #Because we generate this attribute dynamically. except KeyError: # Can occur if the g-code has been cleared while a slice message is still arriving from the other end. gcode_list = [] + application = CuraApplication.getInstance() for index, line in enumerate(gcode_list): - replaced = line.replace("{print_time}", str(self._application.getPrintInformation().currentPrintTime.getDisplayString(DurationFormat.Format.ISO8601))) - replaced = replaced.replace("{filament_amount}", str(self._application.getPrintInformation().materialLengths)) - replaced = replaced.replace("{filament_weight}", str(self._application.getPrintInformation().materialWeights)) - replaced = replaced.replace("{filament_cost}", str(self._application.getPrintInformation().materialCosts)) - replaced = replaced.replace("{jobname}", str(self._application.getPrintInformation().jobName)) + replaced = line.replace("{print_time}", str(application.getPrintInformation().currentPrintTime.getDisplayString(DurationFormat.Format.ISO8601))) + replaced = replaced.replace("{filament_amount}", str(application.getPrintInformation().materialLengths)) + replaced = replaced.replace("{filament_weight}", str(application.getPrintInformation().materialWeights)) + replaced = replaced.replace("{filament_cost}", str(application.getPrintInformation().materialCosts)) + replaced = replaced.replace("{jobname}", str(application.getPrintInformation().jobName)) gcode_list[index] = replaced @@ -711,7 +715,7 @@ class CuraEngineBackend(QObject, Backend): Logger.log("d", "Number of models per buildplate: %s", dict(self._numObjectsPerBuildPlate())) # See if we need to process the sliced layers job. - active_build_plate = self._application.getMultiBuildPlateModel().activeBuildPlate + active_build_plate = application.getMultiBuildPlateModel().activeBuildPlate if ( self._layer_view_active and (self._process_layers_job is None or not self._process_layers_job.isRunning()) and @@ -870,9 +874,9 @@ class CuraEngineBackend(QObject, Backend): def _onActiveViewChanged(self) -> None: """Called when the user changes the active view mode.""" - view = self._application.getController().getActiveView() + view = CuraApplication.getInstance().getController().getActiveView() if view: - active_build_plate = self._application.getMultiBuildPlateModel().activeBuildPlate + active_build_plate = CuraApplication.getInstance().getMultiBuildPlateModel().activeBuildPlate 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 # There is data and we're not slicing at the moment @@ -909,7 +913,7 @@ class CuraEngineBackend(QObject, Backend): extruder.propertyChanged.disconnect(self._onSettingChanged) extruder.containersChanged.disconnect(self._onChanged) - self._global_container_stack = self._application.getMachineManager().activeMachine + self._global_container_stack = CuraApplication.getInstance().getMachineManager().activeMachine if self._global_container_stack: self._global_container_stack.propertyChanged.connect(self._onSettingChanged) # Note: Only starts slicing when the value changed. diff --git a/plugins/CuraEngineBackend/StartSliceJob.py b/plugins/CuraEngineBackend/StartSliceJob.py index bd42d81566..453907ffc0 100644 --- a/plugins/CuraEngineBackend/StartSliceJob.py +++ b/plugins/CuraEngineBackend/StartSliceJob.py @@ -205,10 +205,6 @@ class StartSliceJob(Job): for node in OneAtATimeIterator(self._scene.getRoot()): temp_list = [] - # Node can't be printed, so don't bother sending it. - if getattr(node, "_outside_buildarea", False): - continue - # Filter on current build plate build_plate_number = node.callDecoration("getBuildPlateNumber") if build_plate_number is not None and build_plate_number != self._build_plate_number: diff --git a/plugins/CuraEngineBackend/plugin.json b/plugins/CuraEngineBackend/plugin.json index b4e24af2a3..d87cb1b34a 100644 --- a/plugins/CuraEngineBackend/plugin.json +++ b/plugins/CuraEngineBackend/plugin.json @@ -2,7 +2,7 @@ "name": "CuraEngine Backend", "author": "Ultimaker B.V.", "description": "Provides the link to the CuraEngine slicing backend.", - "api": "7.3.0", + "api": "7.4.0", "version": "1.0.1", "i18n-catalog": "cura" } diff --git a/plugins/CuraProfileReader/plugin.json b/plugins/CuraProfileReader/plugin.json index 3f224d4b85..ad68c08a17 100644 --- a/plugins/CuraProfileReader/plugin.json +++ b/plugins/CuraProfileReader/plugin.json @@ -3,6 +3,6 @@ "author": "Ultimaker B.V.", "version": "1.0.1", "description": "Provides support for importing Cura profiles.", - "api": "7.3.0", + "api": "7.4.0", "i18n-catalog": "cura" } diff --git a/plugins/CuraProfileWriter/plugin.json b/plugins/CuraProfileWriter/plugin.json index 7f840577c1..6dd815ed21 100644 --- a/plugins/CuraProfileWriter/plugin.json +++ b/plugins/CuraProfileWriter/plugin.json @@ -3,6 +3,6 @@ "author": "Ultimaker B.V.", "version": "1.0.1", "description": "Provides support for exporting Cura profiles.", - "api": "7.3.0", + "api": "7.4.0", "i18n-catalog":"cura" } diff --git a/plugins/FirmwareUpdateChecker/plugin.json b/plugins/FirmwareUpdateChecker/plugin.json index 1e6a73f47b..61764e4d3c 100644 --- a/plugins/FirmwareUpdateChecker/plugin.json +++ b/plugins/FirmwareUpdateChecker/plugin.json @@ -3,6 +3,6 @@ "author": "Ultimaker B.V.", "version": "1.0.1", "description": "Checks for firmware updates.", - "api": "7.3.0", + "api": "7.4.0", "i18n-catalog": "cura" } diff --git a/plugins/FirmwareUpdater/plugin.json b/plugins/FirmwareUpdater/plugin.json index 72c795aab1..c59208a555 100644 --- a/plugins/FirmwareUpdater/plugin.json +++ b/plugins/FirmwareUpdater/plugin.json @@ -3,6 +3,6 @@ "author": "Ultimaker B.V.", "version": "1.0.1", "description": "Provides a machine actions for updating firmware.", - "api": "7.3.0", + "api": "7.4.0", "i18n-catalog": "cura" } diff --git a/plugins/GCodeGzReader/plugin.json b/plugins/GCodeGzReader/plugin.json index 73e777ef01..f7e63e5a9c 100644 --- a/plugins/GCodeGzReader/plugin.json +++ b/plugins/GCodeGzReader/plugin.json @@ -3,6 +3,6 @@ "author": "Ultimaker B.V.", "version": "1.0.1", "description": "Reads g-code from a compressed archive.", - "api": "7.3.0", + "api": "7.4.0", "i18n-catalog": "cura" } diff --git a/plugins/GCodeGzWriter/plugin.json b/plugins/GCodeGzWriter/plugin.json index 21a0ce715b..97a0be0c82 100644 --- a/plugins/GCodeGzWriter/plugin.json +++ b/plugins/GCodeGzWriter/plugin.json @@ -3,6 +3,6 @@ "author": "Ultimaker B.V.", "version": "1.0.1", "description": "Writes g-code to a compressed archive.", - "api": "7.3.0", + "api": "7.4.0", "i18n-catalog": "cura" } diff --git a/plugins/GCodeProfileReader/plugin.json b/plugins/GCodeProfileReader/plugin.json index c1725863fd..ebb124e401 100644 --- a/plugins/GCodeProfileReader/plugin.json +++ b/plugins/GCodeProfileReader/plugin.json @@ -3,6 +3,6 @@ "author": "Ultimaker B.V.", "version": "1.0.1", "description": "Provides support for importing profiles from g-code files.", - "api": "7.3.0", + "api": "7.4.0", "i18n-catalog": "cura" } diff --git a/plugins/GCodeReader/plugin.json b/plugins/GCodeReader/plugin.json index d244c9ba62..213c900890 100644 --- a/plugins/GCodeReader/plugin.json +++ b/plugins/GCodeReader/plugin.json @@ -3,6 +3,6 @@ "author": "Victor Larchenko, Ultimaker B.V.", "version": "1.0.1", "description": "Allows loading and displaying G-code files.", - "api": "7.3.0", + "api": "7.4.0", "i18n-catalog": "cura" } diff --git a/plugins/GCodeWriter/plugin.json b/plugins/GCodeWriter/plugin.json index 4502a34006..c924f3ebcd 100644 --- a/plugins/GCodeWriter/plugin.json +++ b/plugins/GCodeWriter/plugin.json @@ -3,6 +3,6 @@ "author": "Ultimaker B.V.", "version": "1.0.1", "description": "Writes g-code to a file.", - "api": "7.3.0", + "api": "7.4.0", "i18n-catalog": "cura" } diff --git a/plugins/ImageReader/plugin.json b/plugins/ImageReader/plugin.json index 4c978e190f..ee871f2694 100644 --- a/plugins/ImageReader/plugin.json +++ b/plugins/ImageReader/plugin.json @@ -3,6 +3,6 @@ "author": "Ultimaker B.V.", "version": "1.0.1", "description": "Enables ability to generate printable geometry from 2D image files.", - "api": "7.3.0", + "api": "7.4.0", "i18n-catalog": "cura" } diff --git a/plugins/LegacyProfileReader/plugin.json b/plugins/LegacyProfileReader/plugin.json index b24d56ff0f..90c5ccd4ca 100644 --- a/plugins/LegacyProfileReader/plugin.json +++ b/plugins/LegacyProfileReader/plugin.json @@ -3,6 +3,6 @@ "author": "Ultimaker B.V.", "version": "1.0.1", "description": "Provides support for importing profiles from legacy Cura versions.", - "api": "7.3.0", + "api": "7.4.0", "i18n-catalog": "cura" } diff --git a/plugins/MachineSettingsAction/plugin.json b/plugins/MachineSettingsAction/plugin.json index 87f00aeeb8..d9a01a80ae 100644 --- a/plugins/MachineSettingsAction/plugin.json +++ b/plugins/MachineSettingsAction/plugin.json @@ -3,6 +3,6 @@ "author": "fieldOfView, Ultimaker B.V.", "version": "1.0.1", "description": "Provides a way to change machine settings (such as build volume, nozzle size, etc.).", - "api": "7.3.0", + "api": "7.4.0", "i18n-catalog": "cura" } diff --git a/plugins/ModelChecker/plugin.json b/plugins/ModelChecker/plugin.json index 1e0e9be3af..664d7c1b9f 100644 --- a/plugins/ModelChecker/plugin.json +++ b/plugins/ModelChecker/plugin.json @@ -2,7 +2,7 @@ "name": "Model Checker", "author": "Ultimaker B.V.", "version": "1.0.1", - "api": "7.3.0", + "api": "7.4.0", "description": "Checks models and print configuration for possible printing issues and give suggestions.", "i18n-catalog": "cura" } diff --git a/plugins/MonitorStage/MonitorMain.qml b/plugins/MonitorStage/MonitorMain.qml index b24ed4ce1b..56f916dc25 100644 --- a/plugins/MonitorStage/MonitorMain.qml +++ b/plugins/MonitorStage/MonitorMain.qml @@ -99,7 +99,7 @@ Rectangle visible: isNetworkConfigured && !isConnected text: catalog.i18nc("@info", "Please make sure your printer has a connection:\n- Check if the printer is turned on.\n- Check if the printer is connected to the network.\n- Check if you are signed in to discover cloud-connected printers.") font: UM.Theme.getFont("medium") - color: UM.Theme.getColor("monitor_text_primary") + color: UM.Theme.getColor("text") wrapMode: Text.WordWrap lineHeight: UM.Theme.getSize("monitor_text_line_large").height lineHeightMode: Text.FixedHeight @@ -116,7 +116,7 @@ Rectangle visible: !isNetworkConfigured && isNetworkConfigurable text: catalog.i18nc("@info", "Please connect your printer to the network.") font: UM.Theme.getFont("medium") - color: UM.Theme.getColor("monitor_text_primary") + color: UM.Theme.getColor("text") wrapMode: Text.WordWrap width: contentWidth lineHeight: UM.Theme.getSize("monitor_text_line_large").height diff --git a/plugins/MonitorStage/plugin.json b/plugins/MonitorStage/plugin.json index 0e612e202b..a8100cb2a3 100644 --- a/plugins/MonitorStage/plugin.json +++ b/plugins/MonitorStage/plugin.json @@ -3,6 +3,6 @@ "author": "Ultimaker B.V.", "version": "1.0.1", "description": "Provides a monitor stage in Cura.", - "api": "7.3.0", + "api": "7.4.0", "i18n-catalog": "cura" } \ No newline at end of file diff --git a/plugins/PerObjectSettingsTool/plugin.json b/plugins/PerObjectSettingsTool/plugin.json index 4a0ad40e65..15db31401e 100644 --- a/plugins/PerObjectSettingsTool/plugin.json +++ b/plugins/PerObjectSettingsTool/plugin.json @@ -3,6 +3,6 @@ "author": "Ultimaker B.V.", "version": "1.0.1", "description": "Provides the Per Model Settings.", - "api": "7.3.0", + "api": "7.4.0", "i18n-catalog": "cura" } diff --git a/plugins/PostProcessingPlugin/PostProcessingPlugin.py b/plugins/PostProcessingPlugin/PostProcessingPlugin.py index 90f3d26cd6..075f947622 100644 --- a/plugins/PostProcessingPlugin/PostProcessingPlugin.py +++ b/plugins/PostProcessingPlugin/PostProcessingPlugin.py @@ -261,7 +261,11 @@ class PostProcessingPlugin(QObject, Extension): script_str = script_str.replace(r"\\\n", "\n").replace(r"\\\\", "\\\\") # Unescape escape sequences. script_parser = configparser.ConfigParser(interpolation=None) script_parser.optionxform = str # type: ignore # Don't transform the setting keys as they are case-sensitive. - script_parser.read_string(script_str) + try: + script_parser.read_string(script_str) + except configparser.Error as e: + Logger.error("Stored post-processing scripts have syntax errors: {err}".format(err = str(e))) + continue for script_name, settings in script_parser.items(): # There should only be one, really! Otherwise we can't guarantee the order or allow multiple uses of the same script. if script_name == "DEFAULT": # ConfigParser always has a DEFAULT section, but we don't fill it. Ignore this one. continue diff --git a/plugins/PostProcessingPlugin/__init__.py b/plugins/PostProcessingPlugin/__init__.py index 6ddecfac69..019627ebd5 100644 --- a/plugins/PostProcessingPlugin/__init__.py +++ b/plugins/PostProcessingPlugin/__init__.py @@ -7,6 +7,7 @@ # tries to create PyQt objects on a non-main thread. import Arcus # @UnusedImport import Savitar # @UnusedImport +import pynest2d # @UnusedImport from . import PostProcessingPlugin diff --git a/plugins/PostProcessingPlugin/plugin.json b/plugins/PostProcessingPlugin/plugin.json index 6e5c8f9b87..a71b5cda78 100644 --- a/plugins/PostProcessingPlugin/plugin.json +++ b/plugins/PostProcessingPlugin/plugin.json @@ -2,7 +2,7 @@ "name": "Post Processing", "author": "Ultimaker", "version": "2.2.1", - "api": "7.3.0", + "api": "7.4.0", "description": "Extension that allows for user created scripts for post processing", "catalog": "cura" } \ No newline at end of file diff --git a/plugins/PostProcessingPlugin/scripts/ChangeAtZ.py b/plugins/PostProcessingPlugin/scripts/ChangeAtZ.py index 78e0e71626..b4036f7ff2 100644 --- a/plugins/PostProcessingPlugin/scripts/ChangeAtZ.py +++ b/plugins/PostProcessingPlugin/scripts/ChangeAtZ.py @@ -889,7 +889,7 @@ class ChangeAtZProcessor: # set feedrate percentage if "speed" in values: - codes.append("M220 S" + str(values["speed"]) + " T1") + codes.append("M220 S" + str(values["speed"]) + "") # set print rate percentage if "printspeed" in values: @@ -1305,7 +1305,7 @@ class ChangeAtZProcessor: self.targetLayer = None self.targetZ = None self.layerHeight = None - self.lastValues = {} + self.lastValues = {"speed": 100} self.linearRetraction = True self.insideTargetLayer = False self.targetValuesInjected = False diff --git a/plugins/PostProcessingPlugin/scripts/DisplayProgressOnLCD.py b/plugins/PostProcessingPlugin/scripts/DisplayProgressOnLCD.py index a445fb3a6e..e39e69eff0 100644 --- a/plugins/PostProcessingPlugin/scripts/DisplayProgressOnLCD.py +++ b/plugins/PostProcessingPlugin/scripts/DisplayProgressOnLCD.py @@ -1,7 +1,7 @@ # Cura PostProcessingPlugin -# Author: Mathias Lyngklip Kjeldgaard, Alexander Gee +# Author: Mathias Lyngklip Kjeldgaard, Alexander Gee, Kimmo Toivanen # Date: July 31, 2019 -# Modified: May 22, 2020 +# Modified: Okt 22, 2020 # Description: This plugin displays progress on the LCD. It can output the estimated time remaining and the completion percentage. @@ -26,10 +26,31 @@ class DisplayProgressOnLCD(Script): "time_remaining": { "label": "Time Remaining", - "description": "When enabled, write Time Left: HHMMSS on the display using M117. This is updated every layer.", + "description": "Select to write remaining time to the display.Select to write remaining time on the display using M117 status line message (almost all printers) or using M73 command (Prusa and Marlin 2 if enabled).", "type": "bool", "default_value": false }, + "time_remaining_method": + { + "label": "Time Reporting Method", + "description": "How should remaining time be shown on the display? It could use a generic message command (M117, almost all printers), or a specialised time remaining command (M73, Prusa and Marlin 2).", + "type": "enum", + "options": { + "m117":"M117 - All printers", + "m73":"M73 - Prusa, Marlin 2" + }, + "enabled": "time_remaining", + "default_value": "m117" + }, + "update_frequency": + { + "label": "Update frequency", + "description": "Update remaining time for every layer or periodically every minute or faster.", + "type": "enum", + "options": {"0":"Every layer","15":"Every 15 seconds","30":"Every 30 seconds","60":"Every minute"}, + "default_value": "0", + "enabled": "time_remaining" + }, "percentage": { "label": "Percentage", @@ -46,34 +67,44 @@ class DisplayProgressOnLCD(Script): list_split = re.split(":", line) # Split at ":" so we can get the numerical value return float(list_split[1]) # Convert the numerical portion to a float - def outputTime(self, lines, line_index, time_left): + def outputTime(self, lines, line_index, time_left, mode): # Do some math to get the time left in seconds into the right format. (HH,MM,SS) + time_left = max(time_left, 0) m, s = divmod(time_left, 60) h, m = divmod(m, 60) # Create the string - current_time_string = "{:d}h{:02d}m{:02d}s".format(int(h), int(m), int(s)) - # And now insert that into the GCODE - lines.insert(line_index, "M117 Time Left {}".format(current_time_string)) + if mode == "m117": + current_time_string = "{:d}h{:02d}m{:02d}s".format(int(h), int(m), int(s)) + # And now insert that into the GCODE + lines.insert(line_index, "M117 Time Left {}".format(current_time_string)) + else: # Must be m73. + mins = int(60 * h + m + s / 30) + lines.insert(line_index, "M73 R{}".format(mins)) def execute(self, data): output_time = self.getSettingValueByKey("time_remaining") + output_time_method = self.getSettingValueByKey("time_remaining_method") + output_frequency = int(self.getSettingValueByKey("update_frequency")) output_percentage = self.getSettingValueByKey("percentage") line_set = {} if output_percentage or output_time: total_time = -1 previous_layer_end_percentage = 0 + previous_layer_end_time = 0 for layer in data: layer_index = data.index(layer) lines = layer.split("\n") for line in lines: - if line.startswith(";TIME:") and total_time == -1: + if (line.startswith(";TIME:") or line.startswith(";PRINT.TIME:")) and total_time == -1: # This line represents the total time required to print the gcode total_time = self.getTimeValue(line) line_index = lines.index(line) + # In the beginning we may have 2 M73 lines, but it makes logic less complicated if output_time: - self.outputTime(lines, line_index, total_time) + self.outputTime(lines, line_index, total_time, output_time_method) + if output_percentage: # Emit 0 percent to sure Marlin knows we are overriding the completion percentage lines.insert(line_index, "M73 P0") @@ -96,8 +127,34 @@ class DisplayProgressOnLCD(Script): line_index = lines.index(line) if output_time: - # Here we calculate remaining time - self.outputTime(lines, line_index, total_time - current_time) + if output_frequency == 0: + # Here we calculate remaining time + self.outputTime(lines, line_index, total_time - current_time, output_time_method) + else: + # Here we calculate remaining time and how many outputs are expected for the layer + layer_time_delta = int(current_time - previous_layer_end_time) + layer_step_delta = int((current_time - previous_layer_end_time) / output_frequency) + # If this layer represents less than 1 step then we don't need to emit anything, continue to the next layer + if layer_step_delta != 0: + # Grab the index of the current line and figure out how many lines represent one second + step = line_index / layer_time_delta + # Move new lines further as we add new lines above it + lines_added = 1 + # Run through layer in seconds + for seconds in range(1, layer_time_delta + 1): + # Time from start to decide when to update + line_time = int(previous_layer_end_time + seconds) + # Output every X seconds and after last layer + if line_time % output_frequency == 0 or line_time == total_time: + # Line to add the output + time_line_index = int((seconds * step) + lines_added) + + # Insert remaining time into the GCODE + self.outputTime(lines, time_line_index, total_time - line_time, output_time_method) + # Next line will be again lower + lines_added = lines_added + 1 + + previous_layer_end_time = int(current_time) if output_percentage: # Calculate percentage value this layer ends at diff --git a/plugins/PostProcessingPlugin/tests/TestPostProcessingPlugin.py b/plugins/PostProcessingPlugin/tests/TestPostProcessingPlugin.py index 70fda32692..53a6c30ccf 100644 --- a/plugins/PostProcessingPlugin/tests/TestPostProcessingPlugin.py +++ b/plugins/PostProcessingPlugin/tests/TestPostProcessingPlugin.py @@ -13,7 +13,7 @@ from ..PostProcessingPlugin import PostProcessingPlugin # not sure if needed sys.path.append(os.path.join(os.path.dirname(os.path.abspath(__file__)), "..")) -""" In this file, commnunity refers to regular Cura for makers.""" +""" In this file, community refers to regular Cura for makers.""" mock_plugin_registry = MagicMock() mock_plugin_registry.getPluginPath = MagicMock(return_value = "mocked_plugin_path") diff --git a/plugins/PrepareStage/plugin.json b/plugins/PrepareStage/plugin.json index 2b1be732e9..3a80523682 100644 --- a/plugins/PrepareStage/plugin.json +++ b/plugins/PrepareStage/plugin.json @@ -3,6 +3,6 @@ "author": "Ultimaker B.V.", "version": "1.0.1", "description": "Provides a prepare stage in Cura.", - "api": "7.3.0", + "api": "7.4.0", "i18n-catalog": "cura" } \ No newline at end of file diff --git a/plugins/PreviewStage/plugin.json b/plugins/PreviewStage/plugin.json index d381059656..d2badeeb7e 100644 --- a/plugins/PreviewStage/plugin.json +++ b/plugins/PreviewStage/plugin.json @@ -3,6 +3,6 @@ "author": "Ultimaker B.V.", "version": "1.0.1", "description": "Provides a preview stage in Cura.", - "api": "7.3.0", + "api": "7.4.0", "i18n-catalog": "cura" } \ No newline at end of file diff --git a/plugins/RemovableDriveOutputDevice/plugin.json b/plugins/RemovableDriveOutputDevice/plugin.json index 3b1091f07c..d862257e69 100644 --- a/plugins/RemovableDriveOutputDevice/plugin.json +++ b/plugins/RemovableDriveOutputDevice/plugin.json @@ -3,6 +3,6 @@ "author": "Ultimaker B.V.", "description": "Provides removable drive hotplugging and writing support.", "version": "1.0.1", - "api": "7.3.0", + "api": "7.4.0", "i18n-catalog": "cura" } diff --git a/plugins/SentryLogger/plugin.json b/plugins/SentryLogger/plugin.json index b31b0f6a25..3ba76d166e 100644 --- a/plugins/SentryLogger/plugin.json +++ b/plugins/SentryLogger/plugin.json @@ -3,6 +3,6 @@ "author": "Ultimaker B.V.", "version": "1.0.0", "description": "Logs certain events so that they can be used by the crash reporter", - "api": "7.3.0", + "api": "7.4.0", "i18n-catalog": "cura" } diff --git a/plugins/SimulationView/SimulationPass.py b/plugins/SimulationView/SimulationPass.py index f594fefbe5..47ae4c8857 100644 --- a/plugins/SimulationView/SimulationPass.py +++ b/plugins/SimulationView/SimulationPass.py @@ -112,7 +112,7 @@ class SimulationPass(RenderPass): elif isinstance(node, NozzleNode): nozzle_node = node - nozzle_node.setVisible(False) + nozzle_node.setVisible(False) # Don't set to true, we render it separately! elif getattr(node, "_outside_buildarea", False) and isinstance(node, SceneNode) and node.getMeshData() and node.isVisible(): disabled_batch.addItem(node.getWorldTransformation(copy=False), node.getMeshData()) @@ -189,7 +189,6 @@ class SimulationPass(RenderPass): # but the user is not using the layer slider, and the compatibility mode is not enabled if not self._switching_layers and not self._compatibility_mode and self._layer_view.getActivity() and nozzle_node is not None: if head_position is not None: - nozzle_node.setVisible(True) nozzle_node.setPosition(head_position) nozzle_batch = RenderBatch(self._nozzle_shader, type = RenderBatch.RenderType.Transparent) nozzle_batch.addItem(nozzle_node.getWorldTransformation(), mesh = nozzle_node.getMeshData()) diff --git a/plugins/SimulationView/plugin.json b/plugins/SimulationView/plugin.json index df1f2b8485..56275498ca 100644 --- a/plugins/SimulationView/plugin.json +++ b/plugins/SimulationView/plugin.json @@ -3,6 +3,6 @@ "author": "Ultimaker B.V.", "version": "1.0.1", "description": "Provides the Simulation view.", - "api": "7.3.0", + "api": "7.4.0", "i18n-catalog": "cura" } diff --git a/plugins/SliceInfoPlugin/SliceInfo.py b/plugins/SliceInfoPlugin/SliceInfo.py index 284389064c..61fc777290 100755 --- a/plugins/SliceInfoPlugin/SliceInfo.py +++ b/plugins/SliceInfoPlugin/SliceInfo.py @@ -1,4 +1,4 @@ -# Copyright (c) 2018 Ultimaker B.V. +# Copyright (c) 2020 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. import json @@ -116,6 +116,7 @@ class SliceInfo(QObject, Extension): machine_manager = self._application.getMachineManager() print_information = self._application.getPrintInformation() + user_profile = self._application.getCuraAPI().account.userProfile global_stack = machine_manager.activeMachine @@ -124,6 +125,8 @@ class SliceInfo(QObject, Extension): data["schema_version"] = 0 data["cura_version"] = self._application.getVersion() data["cura_build_type"] = ApplicationMetadata.CuraBuildType + data["organization_id"] = user_profile.get("organization_id", None) if user_profile else None + data["subscriptions"] = user_profile.get("subscriptions", []) if user_profile else [] active_mode = self._application.getPreferences().getValue("cura/active_mode") if active_mode == 0: diff --git a/plugins/SliceInfoPlugin/example_data.html b/plugins/SliceInfoPlugin/example_data.html index 103eb55a6a..b349ec328d 100644 --- a/plugins/SliceInfoPlugin/example_data.html +++ b/plugins/SliceInfoPlugin/example_data.html @@ -1,12 +1,17 @@ - Cura Version: 4.0
+ Cura Version: 4.8
Operating System: Windows 10
Language: en_US
Machine Type: Ultimaker S5
Intent Profile: Default
Quality Profile: Fast
- Using Custom Settings: No + Using Custom Settings: No
+ Organization ID (if any): ABCDefGHIjKlMNOpQrSTUvYxWZ0-1234567890abcDE=
+ Subscriptions (if any): +

Extruder 1: