diff --git a/cura/Arrange.py b/cura/Arrange.py index 305729d763..2f77ec9a7f 100755 --- a/cura/Arrange.py +++ b/cura/Arrange.py @@ -40,7 +40,7 @@ class Arrange: # \param fixed_nodes Scene nodes to be placed @classmethod def create(cls, scene_root = None, fixed_nodes = None, scale = 0.5, x = 220, y = 220): - arranger = Arrange(x, y, x / 2, y / 2, scale = scale) + arranger = Arrange(x, y, x // 2, y // 2, scale = scale) arranger.centerFirst() if fixed_nodes is None: diff --git a/cura/ArrangeObjectsAllBuildPlatesJob.py b/cura/ArrangeObjectsAllBuildPlatesJob.py index eacd18d5ad..7991ac39f0 100644 --- a/cura/ArrangeObjectsAllBuildPlatesJob.py +++ b/cura/ArrangeObjectsAllBuildPlatesJob.py @@ -112,7 +112,6 @@ class ArrangeObjectsAllBuildPlatesJob(Job): # start_priority = 0 while try_placement: - Logger.log("d", "start_priority %s", start_priority) # 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(): arrange_array.add() diff --git a/cura/ConvexHullNode.py b/cura/ConvexHullNode.py index cc4720c197..c6ff80670d 100644 --- a/cura/ConvexHullNode.py +++ b/cura/ConvexHullNode.py @@ -6,7 +6,6 @@ from UM.Scene.SceneNode import SceneNode from UM.Resources import Resources from UM.Math.Color import Color from UM.Mesh.MeshBuilder import MeshBuilder # To create a mesh to display the convex hull with. - from UM.View.GL.OpenGL import OpenGL @@ -65,7 +64,7 @@ class ConvexHullNode(SceneNode): ConvexHullNode.shader.setUniformValue("u_diffuseColor", self._color) ConvexHullNode.shader.setUniformValue("u_opacity", 0.6) - if self.getParent(): + if self.getParent() and self.getParent().callDecoration("getBuildPlateNumber") == Application.getInstance().activeBuildPlate: if self.getMeshData(): renderer.queueNode(self, transparent = True, shader = ConvexHullNode.shader, backface_cull = True, sort = -8) if self._convex_hull_head_mesh: diff --git a/cura/CuraApplication.py b/cura/CuraApplication.py index 39e4b38824..c1c894c735 100755 --- a/cura/CuraApplication.py +++ b/cura/CuraApplication.py @@ -33,6 +33,7 @@ from UM.Operations.AddSceneNodeOperation import AddSceneNodeOperation from UM.Operations.RemoveSceneNodeOperation import RemoveSceneNodeOperation from UM.Operations.GroupedOperation import GroupedOperation from UM.Operations.SetTransformOperation import SetTransformOperation + from cura.Arrange import Arrange from cura.ShapeArray import ShapeArray from cura.ConvexHullDecorator import ConvexHullDecorator @@ -41,6 +42,7 @@ from cura.SliceableObjectDecorator import SliceableObjectDecorator from cura.BlockSlicingDecorator import BlockSlicingDecorator # research from cura.Scene.BuildPlateDecorator import BuildPlateDecorator +from cura.Scene.CuraSceneNode import CuraSceneNode from cura.ArrangeObjectsJob import ArrangeObjectsJob from cura.ArrangeObjectsAllBuildPlatesJob import ArrangeObjectsAllBuildPlatesJob @@ -307,11 +309,13 @@ class CuraApplication(QtApplication): preferences.addPreference("cura/asked_dialog_on_project_save", False) preferences.addPreference("cura/choice_on_profile_override", "always_ask") preferences.addPreference("cura/choice_on_open_project", "always_ask") + preferences.addPreference("cura/arrange_objects_on_load", True) preferences.addPreference("cura/currency", "€") preferences.addPreference("cura/material_settings", "{}") preferences.addPreference("view/invert_zoom", False) + preferences.addPreference("view/filter_current_build_plate", False) self._need_to_show_user_agreement = not Preferences.getInstance().getValue("general/accepted_user_agreement") @@ -896,7 +900,7 @@ class CuraApplication(QtApplication): scene_bounding_box = None is_block_slicing_node = False for node in DepthFirstIterator(self.getController().getScene().getRoot()): - if type(node) is not SceneNode or (not node.getMeshData() and not node.callDecoration("getLayerData")): + if not issubclass(type(node), SceneNode) or (not node.getMeshData() and not node.callDecoration("getLayerData")): continue if node.callDecoration("isBlockSlicing"): is_block_slicing_node = True @@ -1013,7 +1017,7 @@ class CuraApplication(QtApplication): Selection.clear() for node in DepthFirstIterator(self.getController().getScene().getRoot()): - if type(node) is not SceneNode: + if not issubclass(type(node), SceneNode): continue if not node.getMeshData() and not node.callDecoration("isGroup"): continue # Node that doesnt have a mesh and is not a group. @@ -1021,6 +1025,9 @@ class CuraApplication(QtApplication): continue # Grouped nodes don't need resetting as their parent (the group) is resetted) if not node.isSelectable(): continue # i.e. node with layer data + if not node.callDecoration("isSliceable"): + continue # i.e. node with layer data + Selection.add(node) ## Delete all nodes containing mesh data in the scene. @@ -1032,7 +1039,7 @@ class CuraApplication(QtApplication): nodes = [] for node in DepthFirstIterator(self.getController().getScene().getRoot()): - if type(node) is not SceneNode: + if not issubclass(type(node), SceneNode): continue if (not node.getMeshData() and not node.callDecoration("getLayerData")) and not node.callDecoration("isGroup"): continue # Node that doesnt have a mesh and is not a group. @@ -1054,7 +1061,7 @@ class CuraApplication(QtApplication): Logger.log("i", "Resetting all scene translations") nodes = [] for node in DepthFirstIterator(self.getController().getScene().getRoot()): - if type(node) is not SceneNode: + if not issubclass(type(node), SceneNode): continue if not node.getMeshData() and not node.callDecoration("isGroup"): continue # Node that doesnt have a mesh and is not a group. @@ -1082,13 +1089,13 @@ class CuraApplication(QtApplication): Logger.log("i", "Resetting all scene transformations") nodes = [] for node in DepthFirstIterator(self.getController().getScene().getRoot()): - if type(node) is not SceneNode: + if not issubclass(type(node), SceneNode): continue if not node.getMeshData() and not node.callDecoration("isGroup"): continue # Node that doesnt have a mesh and is not a group. if node.getParent() and node.getParent().callDecoration("isGroup"): continue # Grouped nodes don't need resetting as their parent (the group) is resetted) - if not node.isSelectable(): + if not node.callDecoration("isSliceable"): continue # i.e. node with layer data nodes.append(node) @@ -1109,7 +1116,27 @@ class CuraApplication(QtApplication): def arrangeObjectsToAllBuildPlates(self): nodes = [] for node in DepthFirstIterator(self.getController().getScene().getRoot()): - if type(node) is not SceneNode: + if not issubclass(type(node), SceneNode): + continue + if not node.getMeshData() and not node.callDecoration("isGroup"): + continue # Node that doesnt have a mesh and is not a group. + if node.getParent() and node.getParent().callDecoration("isGroup"): + continue # Grouped nodes don't need resetting as their parent (the group) is resetted) + if not node.callDecoration("isSliceable"): + continue # i.e. node with layer data + # Skip nodes that are too big + if node.getBoundingBox().width < self._volume.getBoundingBox().width or node.getBoundingBox().depth < self._volume.getBoundingBox().depth: + nodes.append(node) + job = ArrangeObjectsAllBuildPlatesJob(nodes) + job.start() + self.setActiveBuildPlate(0) + + # Single build plate + @pyqtSlot() + def arrangeAll(self): + nodes = [] + for node in DepthFirstIterator(self.getController().getScene().getRoot()): + if not issubclass(type(node), SceneNode): continue if not node.getMeshData() and not node.callDecoration("isGroup"): continue # Node that doesnt have a mesh and is not a group. @@ -1117,24 +1144,7 @@ class CuraApplication(QtApplication): continue # Grouped nodes don't need resetting as their parent (the group) is resetted) if not node.isSelectable(): continue # i.e. node with layer data - # Skip nodes that are too big - if node.getBoundingBox().width < self._volume.getBoundingBox().width or node.getBoundingBox().depth < self._volume.getBoundingBox().depth: - nodes.append(node) - job = ArrangeObjectsAllBuildPlatesJob(nodes) - job.start() - - # Single build plate - @pyqtSlot() - def arrangeAll(self): - nodes = [] - for node in DepthFirstIterator(self.getController().getScene().getRoot()): - if type(node) is not SceneNode: - continue - if not node.getMeshData() and not node.callDecoration("isGroup"): - continue # Node that doesnt have a mesh and is not a group. - if node.getParent() and node.getParent().callDecoration("isGroup"): - continue # Grouped nodes don't need resetting as their parent (the group) is resetted) - if not node.isSelectable(): + if not node.callDecoration("isSliceable"): continue # i.e. node with layer data if node.callDecoration("getBuildPlateNumber") == self._active_build_plate: # Skip nodes that are too big @@ -1150,7 +1160,7 @@ class CuraApplication(QtApplication): # What nodes are on the build plate and are not being moved fixed_nodes = [] for node in DepthFirstIterator(self.getController().getScene().getRoot()): - if type(node) is not SceneNode: + if not issubclass(type(node), SceneNode): continue if not node.getMeshData() and not node.callDecoration("isGroup"): continue # Node that doesnt have a mesh and is not a group. @@ -1158,6 +1168,8 @@ class CuraApplication(QtApplication): continue # Grouped nodes don't need resetting as their parent (the group) is resetted) if not node.isSelectable(): continue # i.e. node with layer data + if not node.callDecoration("isSliceable"): + continue # i.e. node with layer data if node in nodes: # exclude selected node from fixed_nodes continue fixed_nodes.append(node) @@ -1176,7 +1188,7 @@ class CuraApplication(QtApplication): Logger.log("i", "Reloading all loaded mesh data.") nodes = [] for node in DepthFirstIterator(self.getController().getScene().getRoot()): - if type(node) is not SceneNode or not node.getMeshData(): + if not issubclass(type(node), SceneNode) or not node.getMeshData(): continue nodes.append(node) @@ -1267,7 +1279,7 @@ class CuraApplication(QtApplication): @pyqtSlot() def groupSelected(self): # Create a group-node - group_node = SceneNode() + group_node = CuraSceneNode() group_decorator = GroupDecorator() group_node.addDecorator(group_decorator) group_node.addDecorator(ConvexHullDecorator()) @@ -1413,11 +1425,15 @@ class CuraApplication(QtApplication): min_offset = 8 self.fileLoaded.emit(filename) + arrange_objects_on_load = Preferences.getInstance().getValue("cura/arrange_objects_on_load") + target_build_plate = self.activeBuildPlate if arrange_objects_on_load else -1 + + for original_node in nodes: + node = CuraSceneNode() # We want our own CuraSceneNode + node.setMeshData(original_node.getMeshData()) - for node in nodes: node.setSelectable(True) node.setName(os.path.basename(filename)) - node.addDecorator(BuildPlateDecorator()) extension = os.path.splitext(filename)[1] if extension.lower() in self._non_sliceable_extensions: @@ -1442,20 +1458,23 @@ class CuraApplication(QtApplication): if not child.getDecorator(ConvexHullDecorator): child.addDecorator(ConvexHullDecorator()) - if node.callDecoration("isSliceable"): - # Only check position if it's not already blatantly obvious that it won't fit. - if 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) + if arrange_objects_on_load: + if node.callDecoration("isSliceable"): + # Only check position if it's not already blatantly obvious that it won't fit. + if 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) - # 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 + # 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 - # Step is for skipping tests to make it a lot faster. it also makes the outcome somewhat rougher - node, _ = arranger.findNodePlacement(node, offset_shape_arr, hull_shape_arr, step = 10) + # Step is for skipping tests to make it a lot faster. it also makes the outcome somewhat rougher + node, _ = arranger.findNodePlacement(node, offset_shape_arr, hull_shape_arr, step = 10) + + node.addDecorator(BuildPlateDecorator(target_build_plate)) op = AddSceneNodeOperation(node, scene.getRoot()) op.push() @@ -1494,6 +1513,8 @@ class CuraApplication(QtApplication): #### research - hacky place for these kind of thing @pyqtSlot(int) def setActiveBuildPlate(self, nr): + if nr == self._active_build_plate: + return Logger.log("d", "Select build plate: %s" % nr) self._active_build_plate = nr diff --git a/cura/ObjectManager.py b/cura/ObjectManager.py index fc6d343252..a148c05f28 100644 --- a/cura/ObjectManager.py +++ b/cura/ObjectManager.py @@ -7,23 +7,35 @@ from UM.Scene.SceneNode import SceneNode from UM.Scene.Selection import Selection from PyQt5.QtCore import Qt from PyQt5.QtWidgets import QApplication +#from cura.Scene.CuraSceneNode import CuraSceneNode +from UM.Preferences import Preferences class ObjectManager(ListModel): def __init__(self): super().__init__() self._last_selected_index = 0 - Application.getInstance().getController().getScene().sceneChanged.connect(self._update) + Application.getInstance().getController().getScene().sceneChanged.connect(self._update_scene_changed) + Preferences.getInstance().preferenceChanged.connect(self._update) + Application.getInstance().activeBuildPlateChanged.connect(self._update) def _update(self, *args): nodes = [] + filter_current_build_plate = Preferences.getInstance().getValue("view/filter_current_build_plate") + active_build_plate_number = Application.getInstance().activeBuildPlate for node in DepthFirstIterator(Application.getInstance().getController().getScene().getRoot()): - if type(node) is not SceneNode or (not node.getMeshData() and not node.callDecoration("getLayerData")): + if not issubclass(type(node), SceneNode) or (not node.getMeshData() and not node.callDecoration("getLayerData")): + continue + if not node.callDecoration("isSliceable"): + continue + node_build_plate_number = node.callDecoration("getBuildPlateNumber") + if filter_current_build_plate and node_build_plate_number != active_build_plate_number: continue nodes.append({ "name": node.getName(), "isSelected": Selection.isSelected(node), - "buildPlateNumber": node.callDecoration("getBuildPlateNumber"), + "isOutsideBuildArea": node.isOutsideBuildArea(), + "buildPlateNumber": node_build_plate_number, "node": node }) nodes = sorted(nodes, key=lambda n: n["name"]) @@ -31,6 +43,12 @@ class ObjectManager(ListModel): self.itemsChanged.emit() + def _update_scene_changed(self, *args): + # if args and type(args[0]) is not CuraSceneNode: + # Logger.log("d", " ascdf %s", args) + # return + self._update(*args) + ## Either select or deselect an item @pyqtSlot(int) def changeSelection(self, index): @@ -63,6 +81,11 @@ class ObjectManager(ListModel): self._last_selected_index = index + # testing + for node in DepthFirstIterator(Application.getInstance().getController().getScene().getRoot()): + if node.callDecoration("getLayerData"): + Logger.log("d", " ##### NODE: %s", node) + @staticmethod def createObjectManager(): return ObjectManager() diff --git a/cura/Scene/BuildPlateDecorator.py b/cura/Scene/BuildPlateDecorator.py index b0a14e41f4..2c886c7444 100644 --- a/cura/Scene/BuildPlateDecorator.py +++ b/cura/Scene/BuildPlateDecorator.py @@ -6,11 +6,14 @@ from UM.Logger import Logger class BuildPlateDecorator(SceneNodeDecorator): def __init__(self, build_plate_number = -1): super().__init__() + self._build_plate_number = None + self._previous_build_plate_number = None self.setBuildPlateNumber(build_plate_number) def setBuildPlateNumber(self, nr): # Make sure that groups are set correctly # setBuildPlateForSelection in CuraActions makes sure that no single childs are set. + self._previous_build_plate_number = self._build_plate_number self._build_plate_number = nr if self._node and self._node.callDecoration("isGroup"): for child in self._node.getChildren(): @@ -19,5 +22,9 @@ class BuildPlateDecorator(SceneNodeDecorator): def getBuildPlateNumber(self): return self._build_plate_number + # Used to determine from what build plate the node moved. + def getPreviousBuildPlateNumber(self): + return self._previous_build_plate_number + def __deepcopy__(self, memo): return BuildPlateDecorator() diff --git a/cura/Scene/CuraSceneNode.py b/cura/Scene/CuraSceneNode.py new file mode 100644 index 0000000000..ccec76b53d --- /dev/null +++ b/cura/Scene/CuraSceneNode.py @@ -0,0 +1,40 @@ +from UM.Application import Application +from UM.Logger import Logger +from UM.Scene.SceneNode import SceneNode +from copy import deepcopy + + +## 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. +class CuraSceneNode(SceneNode): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self._outside_buildarea = True + + def setOutsideBuildArea(self, new_value): + self._outside_buildarea = new_value + + def isOutsideBuildArea(self): + return self._outside_buildarea or self.callDecoration("getBuildPlateNumber") < 0 + + def isVisible(self): + return super().isVisible() and self.callDecoration("getBuildPlateNumber") == Application.getInstance().activeBuildPlate + + def isSelectable(self) -> bool: + return super().isSelectable() and self.callDecoration("getBuildPlateNumber") == Application.getInstance().activeBuildPlate + + ## Taken from SceneNode, but replaced SceneNode with CuraSceneNode + def __deepcopy__(self, memo): + copy = CuraSceneNode() + copy.setTransformation(self.getLocalTransformation()) + copy.setMeshData(self._mesh_data) + copy.setVisible(deepcopy(self._visible, memo)) + copy._selectable = deepcopy(self._selectable, memo) + copy._name = deepcopy(self._name, memo) + for decorator in self._decorators: + copy.addDecorator(deepcopy(decorator, memo)) + + for child in self._children: + copy.addChild(deepcopy(child, memo)) + self.calculateBoundingBoxMesh() + return copy diff --git a/plugins/3MFReader/ThreeMFReader.py b/plugins/3MFReader/ThreeMFReader.py index a34bf771d7..8c4ef9d1ae 100755 --- a/plugins/3MFReader/ThreeMFReader.py +++ b/plugins/3MFReader/ThreeMFReader.py @@ -15,7 +15,8 @@ from cura.Settings.SettingOverrideDecorator import SettingOverrideDecorator from UM.Application import Application from cura.Settings.ExtruderManager import ExtruderManager from cura.QualityManager import QualityManager -from UM.Scene.SceneNode import SceneNode +#from UM.Scene.SceneNode import SceneNode +from cura.Scene.CuraSceneNode import CuraSceneNode as SceneNode from cura.SliceableObjectDecorator import SliceableObjectDecorator from cura.ZOffsetDecorator import ZOffsetDecorator diff --git a/plugins/CuraEngineBackend/CuraEngineBackend.py b/plugins/CuraEngineBackend/CuraEngineBackend.py index 914aa1dee0..67d3fe8c42 100755 --- a/plugins/CuraEngineBackend/CuraEngineBackend.py +++ b/plugins/CuraEngineBackend/CuraEngineBackend.py @@ -69,9 +69,10 @@ class CuraEngineBackend(QObject, Backend): # Workaround to disable layer view processing if layer view is not active. self._layer_view_active = False Application.getInstance().getController().activeViewChanged.connect(self._onActiveViewChanged) + Application.getInstance().activeBuildPlateChanged.connect(self._onActiveViewChanged) self._onActiveViewChanged() self._stored_layer_data = [] - self._stored_optimized_layer_data = [] + self._stored_optimized_layer_data = {} # key is build plate number, then arrays are stored until they go to the ProcessSlicesLayersJob self._scene = Application.getInstance().getController().getScene() self._scene.sceneChanged.connect(self._onSceneChanged) @@ -104,12 +105,14 @@ class CuraEngineBackend(QObject, Backend): self._message_handlers["cura.proto.SlicingFinished"] = self._onSlicingFinishedMessage self._start_slice_job = None + self._start_slice_job_build_plate = None self._slicing = False # Are we currently slicing? self._restart = False # Back-end is currently restarting? self._tool_active = False # 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._process_layers_job = None # The currently active job to process layers, or None if it is not processing layers. - self._need_slicing = False + # self._need_slicing = False + self._build_plates_to_be_sliced = [] # what needs slicing? self._engine_is_fresh = True # Is the newly started engine used before or not? self._backend_log_max_lines = 20000 # Maximum number of lines to buffer @@ -189,8 +192,9 @@ class CuraEngineBackend(QObject, Backend): ## Perform a slice of the scene. def slice(self): + Logger.log("d", "starting to slice again!") self._slice_start_time = time() - if not self._need_slicing: + if not self._build_plates_to_be_sliced: self.processingProgress.emit(1.0) self.backendStateChange.emit(BackendState.Done) Logger.log("w", "Slice unnecessary, nothing has changed that needs reslicing.") @@ -199,7 +203,6 @@ class CuraEngineBackend(QObject, Backend): Application.getInstance().getPrintInformation().setToZeroPrintInformation() self._stored_layer_data = [] - self._stored_optimized_layer_data = [] if self._process is None: self._createSocket() @@ -215,6 +218,9 @@ class CuraEngineBackend(QObject, Backend): slice_message = self._socket.createMessage("cura.proto.Slice") self._start_slice_job = StartSliceJob.StartSliceJob(slice_message) + self._start_slice_job_build_plate = self._build_plates_to_be_sliced.pop(0) + self._stored_optimized_layer_data[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.finished.connect(self._onStartSliceCompleted) @@ -223,7 +229,8 @@ class CuraEngineBackend(QObject, Backend): def _terminate(self): self._slicing = False self._stored_layer_data = [] - self._stored_optimized_layer_data = [] + if self._start_slice_job_build_plate in self._stored_optimized_layer_data: + del self._stored_optimized_layer_data[self._start_slice_job_build_plate] if self._start_slice_job is not None: self._start_slice_job.cancel() @@ -315,10 +322,13 @@ class CuraEngineBackend(QObject, Backend): 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")) self._error_message.show() - self.backendStateChange.emit(BackendState.Error) + #self.backendStateChange.emit(BackendState.Error) else: - self.backendStateChange.emit(BackendState.NotStarted) + #self.backendStateChange.emit(BackendState.NotStarted) + pass + self._invokeSlice() return + # Preparation completed, send it to the backend. self._socket.sendMessage(job.getSliceMessage()) @@ -360,27 +370,34 @@ class CuraEngineBackend(QObject, Backend): # # \param source The scene node that was changed. def _onSceneChanged(self, source): - if type(source) is not SceneNode: + Logger.log("d", " ##### scene changed: %s", source) + if not issubclass(type(source), SceneNode): return root_scene_nodes_changed = False + build_plates_changed = set() if source == self._scene.getRoot(): num_objects = 0 for node in DepthFirstIterator(self._scene.getRoot()): # Only count sliceable objects if node.callDecoration("isSliceable"): num_objects += 1 + build_plates_changed.add(node.callDecoration("getBuildPlateNumber")) + build_plates_changed.add(node.callDecoration("getPreviousBuildPlateNumber")) if num_objects != self._last_num_objects: self._last_num_objects = num_objects root_scene_nodes_changed = True - else: - return + # else: + # return # ?? + build_plates_changed.discard(None) + build_plates_changed.discard(-1) # object not on build plate + Logger.log("d", " #### build plates changed: %s", build_plates_changed) - if not source.callDecoration("isGroup") and not root_scene_nodes_changed: - if source.getMeshData() is None: - return - if source.getMeshData().getVertices() is None: - return + # if not source.callDecoration("isGroup") and not root_scene_nodes_changed: + # if source.getMeshData() is None: + # return + # if source.getMeshData().getVertices() is None: + # return if self._tool_active: # do it later, each source only has to be done once @@ -388,9 +405,24 @@ class CuraEngineBackend(QObject, Backend): self._postponed_scene_change_sources.append(source) return - self.needsSlicing() - self.stopSlicing() - self._onChanged() + if build_plates_changed: + Logger.log("d", " going to reslice") + self.stopSlicing() + for build_plate_number in build_plates_changed: + if build_plate_number not in self._build_plates_to_be_sliced: + self._build_plates_to_be_sliced.append(build_plate_number) + self.processingProgress.emit(0.0) + self.backendStateChange.emit(BackendState.NotStarted) + if not self._use_timer: + # With manually having to slice, we want to clear the old invalid layer data. + self._clearLayerData(build_plates_changed) + + self._invokeSlice() + + # #self.needsSlicing() + # self.stopSlicing() + # #self._onChanged() + # self._invokeSlice() ## Called when an error occurs in the socket connection towards the engine. # @@ -410,16 +442,24 @@ class CuraEngineBackend(QObject, Backend): Logger.log("w", "A socket error caused the connection to be reset") ## Remove old layer data (if any) - def _clearLayerData(self): + def _clearLayerData(self, build_plate_numbers = set()): for node in DepthFirstIterator(self._scene.getRoot()): if node.callDecoration("getLayerData"): - node.getParent().removeChild(node) - break + if node.callDecoration("getBuildPlateNumber") in build_plate_numbers or not build_plate_numbers: + node.getParent().removeChild(node) - ## Convenient function: set need_slicing, emit state and clear layer data + def markSliceAll(self): + if 0 not in self._build_plates_to_be_sliced: + self._build_plates_to_be_sliced.append(0) + if 1 not in self._build_plates_to_be_sliced: + self._build_plates_to_be_sliced.append(1) + if 2 not in self._build_plates_to_be_sliced: + self._build_plates_to_be_sliced.append(2) + + ## Convenient function: mark everything to slice, emit state and clear layer data def needsSlicing(self): self.stopSlicing() - self._need_slicing = True + self.markSliceAll() self.processingProgress.emit(0.0) self.backendStateChange.emit(BackendState.NotStarted) if not self._use_timer: @@ -441,7 +481,7 @@ class CuraEngineBackend(QObject, Backend): def _onStackErrorCheckFinished(self): self._is_error_check_scheduled = False - if not self._slicing and self._need_slicing: + if not self._slicing and self._build_plates_to_be_sliced: #self._need_slicing: self.needsSlicing() self._onChanged() @@ -455,7 +495,7 @@ class CuraEngineBackend(QObject, Backend): # # \param message The protobuf message containing sliced layer data. def _onOptimizedLayerMessage(self, message): - self._stored_optimized_layer_data.append(message) + self._stored_optimized_layer_data[self._start_slice_job_build_plate].append(message) ## Called when a progress message is received from the engine. # @@ -464,6 +504,16 @@ class CuraEngineBackend(QObject, Backend): self.processingProgress.emit(message.amount) self.backendStateChange.emit(BackendState.Processing) + # testing + def _invokeSlice(self): + if self._use_timer: + # if the error check is scheduled, wait for the error check finish signal to trigger auto-slice, + # otherwise business as usual + if self._is_error_check_scheduled: + self._change_timer.stop() + else: + self._change_timer.start() + ## Called when the engine sends a message that slicing is finished. # # \param message The protobuf message signalling that slicing is finished. @@ -481,13 +531,20 @@ class CuraEngineBackend(QObject, Backend): self._scene.gcode_list[self._scene.gcode_list.index(line)] = replaced self._slicing = False - self._need_slicing = False + #self._need_slicing = False Logger.log("d", "Slicing took %s seconds", time() - self._slice_start_time ) - if self._layer_view_active and (self._process_layers_job is None or not self._process_layers_job.isRunning()): - self._process_layers_job = ProcessSlicedLayersJob.ProcessSlicedLayersJob(self._stored_optimized_layer_data) - self._process_layers_job.finished.connect(self._onProcessLayersFinished) - self._process_layers_job.start() - self._stored_optimized_layer_data = [] + + # See if we need to process the sliced layers job. + active_build_plate = Application.getInstance().activeBuildPlate + if self._layer_view_active and (self._process_layers_job is None or not self._process_layers_job.isRunning()) and active_build_plate == self._start_slice_job_build_plate: + self._startProcessSlicedLayersJob(active_build_plate) + self._start_slice_job_build_plate = None + + Logger.log("d", "See if there is more to slice...") + # Somehow this results in an Arcus Error + # self.slice() + # Testing call slice again, allow backend to restart by using the timer + self._invokeSlice() ## Called when a g-code message is received from the engine. # @@ -584,19 +641,26 @@ class CuraEngineBackend(QObject, Backend): source = self._postponed_scene_change_sources.pop(0) self._onSceneChanged(source) + def _startProcessSlicedLayersJob(self, build_plate_number): + self._process_layers_job = ProcessSlicedLayersJob.ProcessSlicedLayersJob(self._stored_optimized_layer_data[build_plate_number]) + self._process_layers_job.setBuildPlate(build_plate_number) + self._process_layers_job.finished.connect(self._onProcessLayersFinished) + self._process_layers_job.start() + del self._stored_optimized_layer_data[build_plate_number] + ## Called when the user changes the active view mode. def _onActiveViewChanged(self): - if Application.getInstance().getController().getActiveView(): - view = Application.getInstance().getController().getActiveView() + application = Application.getInstance() + view = application.getController().getActiveView() + if view: + active_build_plate = application.activeBuildPlate if view.getPluginId() == "LayerView": # 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 # if we are slicing, there is no need to re-calculate the data as it will be invalid in a moment. - if self._stored_optimized_layer_data and not self._slicing: - self._process_layers_job = ProcessSlicedLayersJob.ProcessSlicedLayersJob(self._stored_optimized_layer_data) - self._process_layers_job.finished.connect(self._onProcessLayersFinished) - self._process_layers_job.start() - self._stored_optimized_layer_data = [] + # TODO: what build plate I am slicing + if active_build_plate in self._stored_optimized_layer_data and not self._slicing: + self._startProcessSlicedLayersJob(active_build_plate) else: self._layer_view_active = False diff --git a/plugins/CuraEngineBackend/ProcessSlicedLayersJob.py b/plugins/CuraEngineBackend/ProcessSlicedLayersJob.py index a352564bc2..1e56f2dd35 100644 --- a/plugins/CuraEngineBackend/ProcessSlicedLayersJob.py +++ b/plugins/CuraEngineBackend/ProcessSlicedLayersJob.py @@ -17,10 +17,12 @@ from UM.Logger import Logger from UM.Math.Vector import Vector +from cura.Scene.BuildPlateDecorator import BuildPlateDecorator from cura.Settings.ExtruderManager import ExtruderManager from cura import LayerDataBuilder from cura import LayerDataDecorator from cura import LayerPolygon +# from cura.Scene.CuraSceneNode import CuraSceneNode import numpy from time import time @@ -49,6 +51,7 @@ class ProcessSlicedLayersJob(Job): self._scene = Application.getInstance().getController().getScene() self._progress_message = Message(catalog.i18nc("@info:status", "Processing Layers"), 0, False, -1) self._abort_requested = False + self._build_plate_number = None ## Aborts the processing of layers. # @@ -59,7 +62,11 @@ class ProcessSlicedLayersJob(Job): def abort(self): self._abort_requested = True + def setBuildPlate(self, new_value): + self._build_plate_number = new_value + def run(self): + Logger.log("d", "########## Processing new layer for [%s]..." % self._build_plate_number) start_time = time() if Application.getInstance().getController().getActiveView().getPluginId() == "LayerView": self._progress_message.show() @@ -72,16 +79,18 @@ class ProcessSlicedLayersJob(Job): Application.getInstance().getController().activeViewChanged.connect(self._onActiveViewChanged) new_node = SceneNode() + new_node.addDecorator(BuildPlateDecorator(self._build_plate_number)) - ## Remove old layer data (if any) - for node in DepthFirstIterator(self._scene.getRoot()): - if node.callDecoration("getLayerData"): - node.getParent().removeChild(node) - break - if self._abort_requested: - if self._progress_message: - self._progress_message.hide() - return + # ## Remove old layer data (if any) + # for node in DepthFirstIterator(self._scene.getRoot()): + # if node.callDecoration("getLayerData") and node.callDecoration("getBuildPlateNumber") == self._build_plate_number: + # Logger.log("d", " # Removing: %s", node) + # node.getParent().removeChild(node) + # #break + # if self._abort_requested: + # if self._progress_message: + # self._progress_message.hide() + # return # Force garbage collection. # For some reason, Python has a tendency to keep the layer data diff --git a/plugins/CuraEngineBackend/StartSliceJob.py b/plugins/CuraEngineBackend/StartSliceJob.py index 607914f5c5..f6abe94702 100644 --- a/plugins/CuraEngineBackend/StartSliceJob.py +++ b/plugins/CuraEngineBackend/StartSliceJob.py @@ -10,12 +10,13 @@ from UM.Job import Job from UM.Application import Application from UM.Logger import Logger -from UM.Scene.SceneNode import SceneNode +#from UM.Scene.SceneNode import SceneNode from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator from UM.Settings.Validator import ValidatorState from UM.Settings.SettingRelation import RelationType +from cura.Scene.CuraSceneNode import CuraSceneNode as SceneNode from cura.OneAtATimeIterator import OneAtATimeIterator from cura.Settings.ExtruderManager import ExtruderManager @@ -58,10 +59,14 @@ class StartSliceJob(Job): self._scene = Application.getInstance().getController().getScene() self._slice_message = slice_message self._is_cancelled = False + self._build_plate_number = None def getSliceMessage(self): return self._slice_message + def setBuildPlate(self, build_plate_number): + self._build_plate_number = build_plate_number + ## Check if a stack has any errors. ## returns true if it has errors, false otherwise. def _checkStackForErrors(self, stack): @@ -78,6 +83,10 @@ class StartSliceJob(Job): ## Runs the job that initiates the slicing. def run(self): + if self._build_plate_number is None: + self.setResult(StartJobResult.Error) + return + stack = Application.getInstance().getGlobalContainerStack() if not stack: self.setResult(StartJobResult.Error) @@ -141,14 +150,12 @@ class StartSliceJob(Job): for node in DepthFirstIterator(self._scene.getRoot()): if type(node) is SceneNode and node.getMeshData() and node.getMeshData().getVertices() is not None: - # temp hack to filter on build plate 0 - if (node.callDecoration("getBuildPlateNumber") == 0): - - if not getattr(node, "_outside_buildarea", False)\ - or (node.callDecoration("getStack") and any(node.callDecoration("getStack").getProperty(setting, "value") for setting in self._not_printed_mesh_settings)): + if (node.callDecoration("getBuildPlateNumber") == self._build_plate_number): + if not getattr(node, "_outside_buildarea", False) or (node.callDecoration("getStack") and any(node.callDecoration("getStack").getProperty(setting, "value") for setting in self._not_printed_mesh_settings)): temp_list.append(node) Job.yieldThread() + Logger.log("d", " objects to be sliced: %s", temp_list) if temp_list: object_groups.append(temp_list) diff --git a/plugins/ImageReader/ImageReader.py b/plugins/ImageReader/ImageReader.py index b419c0b496..2529abf2d8 100644 --- a/plugins/ImageReader/ImageReader.py +++ b/plugins/ImageReader/ImageReader.py @@ -8,12 +8,14 @@ from PyQt5.QtCore import Qt from UM.Mesh.MeshReader import MeshReader from UM.Mesh.MeshBuilder import MeshBuilder -from UM.Scene.SceneNode import SceneNode +#from UM.Scene.SceneNode import SceneNode from UM.Math.Vector import Vector from UM.Job import Job from UM.Logger import Logger from .ImageReaderUI import ImageReaderUI +from cura.Scene.CuraSceneNode import CuraSceneNode as SceneNode + class ImageReader(MeshReader): def __init__(self): diff --git a/plugins/LayerView/LayerPass.py b/plugins/LayerView/LayerPass.py index 963c8c75c8..2b0e82d4b3 100755 --- a/plugins/LayerView/LayerPass.py +++ b/plugins/LayerView/LayerPass.py @@ -66,13 +66,14 @@ class LayerPass(RenderPass): self.bind() tool_handle_batch = RenderBatch(self._tool_handle_shader, type = RenderBatch.RenderType.Overlay) + active_build_plate = Application.getInstance().activeBuildPlate for node in DepthFirstIterator(self._scene.getRoot()): if isinstance(node, ToolHandle): tool_handle_batch.addItem(node.getWorldTransformation(), mesh = node.getSolidMesh()) - elif isinstance(node, SceneNode) and (node.getMeshData() or node.callDecoration("isBlockSlicing")) and node.isVisible(): + elif issubclass(type(node), SceneNode) and (node.getMeshData() or node.callDecoration("isBlockSlicing")) and node.isVisible() and node.callDecoration("getBuildPlateNumber") == active_build_plate: layer_data = node.callDecoration("getLayerData") if not layer_data: continue diff --git a/plugins/SolidView/SolidView.py b/plugins/SolidView/SolidView.py index d37fbb7c9d..699cec23f5 100644 --- a/plugins/SolidView/SolidView.py +++ b/plugins/SolidView/SolidView.py @@ -75,7 +75,7 @@ class SolidView(View): for node in DepthFirstIterator(scene.getRoot()): if not node.render(renderer): - if node.getMeshData() and node.isVisible() and (node.callDecoration("getBuildPlateNumber") == activeBuildPlateNumber): + if node.getMeshData() and node.isVisible(): # and (node.callDecoration("getBuildPlateNumber") == activeBuildPlateNumber): uniforms = {} shade_factor = 1.0 diff --git a/plugins/X3DReader/X3DReader.py b/plugins/X3DReader/X3DReader.py index e4a59dcdaa..a0d530c78c 100644 --- a/plugins/X3DReader/X3DReader.py +++ b/plugins/X3DReader/X3DReader.py @@ -11,7 +11,8 @@ from UM.Math.Matrix import Matrix from UM.Math.Vector import Vector from UM.Mesh.MeshBuilder import MeshBuilder from UM.Mesh.MeshReader import MeshReader -from UM.Scene.SceneNode import SceneNode +#from UM.Scene.SceneNode import SceneNode +from cura.Scene.CuraSceneNode import CuraSceneNode as SceneNode MYPY = False try: @@ -19,63 +20,63 @@ try: import xml.etree.cElementTree as ET except ImportError: import xml.etree.ElementTree as ET - + # TODO: preserve the structure of scenes that contain several objects -# Use CADPart, for example, to distinguish between separate objects - +# Use CADPart, for example, to distinguish between separate objects + DEFAULT_SUBDIV = 16 # Default subdivision factor for spheres, cones, and cylinders EPSILON = 0.000001 class Shape: - + # Expects verts in MeshBuilder-ready format, as a n by 3 mdarray # with vertices stored in rows def __init__(self, verts, faces, index_base, name): self.verts = verts self.faces = faces # Those are here for debugging purposes only - self.index_base = index_base + self.index_base = index_base self.name = name - + class X3DReader(MeshReader): def __init__(self): super().__init__() self._supported_extensions = [".x3d"] self._namespaces = {} - + # Main entry point # Reads the file, returns a SceneNode (possibly with nested ones), or None def read(self, file_name): try: self.defs = {} self.shapes = [] - + tree = ET.parse(file_name) xml_root = tree.getroot() - + if xml_root.tag != "X3D": return None - scale = 1000 # Default X3D unit it one meter, while Cura's is one millimeters + scale = 1000 # Default X3D unit it one meter, while Cura's is one millimeters if xml_root[0].tag == "head": for head_node in xml_root[0]: if head_node.tag == "unit" and head_node.attrib.get("category") == "length": scale *= float(head_node.attrib["conversionFactor"]) - break + break xml_scene = xml_root[1] else: xml_scene = xml_root[0] - + if xml_scene.tag != "Scene": return None - + self.transform = Matrix() self.transform.setByScaleFactor(scale) self.index_base = 0 - + # Traverse the scene tree, populate the shapes list self.processChildNodes(xml_scene) - + if self.shapes: builder = MeshBuilder() builder.setVertices(numpy.concatenate([shape.verts for shape in self.shapes])) @@ -95,20 +96,20 @@ class X3DReader(MeshReader): else: return None - + except Exception: Logger.logException("e", "Exception in X3D reader") return None return node - + # ------------------------- XML tree traversal - + def processNode(self, xml_node): xml_node = self.resolveDefUse(xml_node) if xml_node is None: return - + tag = xml_node.tag if tag in ("Group", "StaticGroup", "CADAssembly", "CADFace", "CADLayer", "Collision"): self.processChildNodes(xml_node) @@ -120,8 +121,8 @@ class X3DReader(MeshReader): self.processTransform(xml_node) elif tag == "Shape": self.processShape(xml_node) - - + + def processShape(self, xml_node): # Find the geometry and the appearance inside the Shape geometry = appearance = None @@ -130,21 +131,21 @@ class X3DReader(MeshReader): appearance = self.resolveDefUse(sub_node) elif sub_node.tag in self.geometry_importers and not geometry: geometry = self.resolveDefUse(sub_node) - - # TODO: appearance is completely ignored. At least apply the material color... + + # TODO: appearance is completely ignored. At least apply the material color... if not geometry is None: try: - self.verts = self.faces = [] # Safeguard + self.verts = self.faces = [] # Safeguard self.geometry_importers[geometry.tag](self, geometry) m = self.transform.getData() verts = m.dot(self.verts)[:3].transpose() - + self.shapes.append(Shape(verts, self.faces, self.index_base, geometry.tag)) self.index_base += len(verts) - + except Exception: Logger.logException("e", "Exception in X3D reader while reading %s", geometry.tag) - + # Returns the referenced node if the node has USE, the same node otherwise. # May return None is USE points at a nonexistent node # In X3DOM, when both DEF and USE are in the same node, DEF is ignored. @@ -155,34 +156,34 @@ class X3DReader(MeshReader): if USE: return self.defs.get(USE, None) - DEF = node.attrib.get("DEF") + DEF = node.attrib.get("DEF") if DEF: - self.defs[DEF] = node + self.defs[DEF] = node return node - + def processChildNodes(self, node): for c in node: self.processNode(c) Job.yieldThread() - + # Since this is a grouping node, will recurse down the tree. # According to the spec, the final transform matrix is: # T * C * R * SR * S * -SR * -C # Where SR corresponds to the rotation matrix to scaleOrientation - # C and SR are rather exotic. S, slightly less so. + # C and SR are rather exotic. S, slightly less so. def processTransform(self, node): rot = readRotation(node, "rotation", (0, 0, 1, 0)) # (angle, axisVactor) tuple trans = readVector(node, "translation", (0, 0, 0)) # Vector scale = readVector(node, "scale", (1, 1, 1)) # Vector center = readVector(node, "center", (0, 0, 0)) # Vector scale_orient = readRotation(node, "scaleOrientation", (0, 0, 1, 0)) # (angle, axisVactor) tuple - - # Store the previous transform; in Cura, the default matrix multiplication is in place + + # Store the previous transform; in Cura, the default matrix multiplication is in place prev = Matrix(self.transform.getData()) # It's deep copy, I've checked - + # The rest of transform manipulation will be applied in place got_center = (center.x != 0 or center.y != 0 or center.z != 0) - + T = self.transform if trans.x != 0 or trans.y != 0 or trans.z !=0: T.translate(trans) @@ -202,13 +203,13 @@ class X3DReader(MeshReader): T.rotateByAxis(-scale_orient[0], scale_orient[1]) if got_center: T.translate(-center) - + self.processChildNodes(node) self.transform = prev - + # ------------------------- Geometry importers # They are supposed to fill the self.verts and self.faces arrays, the caller will do the rest - + # Primitives def processGeometryBox(self, node): @@ -228,14 +229,14 @@ class X3DReader(MeshReader): self.addVertex(-dx, -dy, dz) self.addVertex(-dx, -dy, -dz) self.addVertex(dx, -dy, -dz) - + self.addQuad(0, 1, 2, 3) # +y self.addQuad(4, 0, 3, 7) # +x self.addQuad(7, 3, 2, 6) # -z self.addQuad(6, 2, 1, 5) # -x self.addQuad(5, 1, 0, 4) # +z self.addQuad(7, 6, 5, 4) # -y - + # The sphere is subdivided into nr rings and ns segments def processGeometrySphere(self, node): r = readFloat(node, "radius", 0.5) @@ -247,16 +248,16 @@ class X3DReader(MeshReader): (nr, ns) = subdiv else: nr = ns = DEFAULT_SUBDIV - + lau = pi / nr # Unit angle of latitude (rings) for the given tesselation lou = 2 * pi / ns # Unit angle of longitude (segments) - + self.reserveFaceAndVertexCount(ns*(nr*2 - 2), 2 + (nr - 1)*ns) - + # +y and -y poles self.addVertex(0, r, 0) self.addVertex(0, -r, 0) - + # The non-polar vertices go from x=0, negative z plane counterclockwise - # to -x, to +z, to +x, back to -z for ring in range(1, nr): @@ -264,12 +265,12 @@ class X3DReader(MeshReader): self.addVertex(-r*sin(lou * seg) * sin(lau * ring), r*cos(lau * ring), -r*cos(lou * seg) * sin(lau * ring)) - + vb = 2 + (nr - 2) * ns # First vertex index for the bottom cap - + # Faces go in order: top cap, sides, bottom cap. # Sides go by ring then by segment. - + # Caps # Top cap face vertices go in order: down right up # (starting from +y pole) @@ -277,7 +278,7 @@ class X3DReader(MeshReader): for seg in range(ns): self.addTri(0, seg + 2, (seg + 1) % ns + 2) self.addTri(1, vb + (seg + 1) % ns, vb + seg) - + # Sides # Side face vertices go in order: down right upleft, downright up left for ring in range(nr - 2): @@ -288,24 +289,24 @@ class X3DReader(MeshReader): for seg in range(ns): nseg = (seg + 1) % ns self.addQuad(tvb + seg, bvb + seg, bvb + nseg, tvb + nseg) - + def processGeometryCone(self, node): r = readFloat(node, "bottomRadius", 1) height = readFloat(node, "height", 2) bottom = readBoolean(node, "bottom", True) side = readBoolean(node, "side", True) n = readInt(node, "subdivision", DEFAULT_SUBDIV) - + d = height / 2 angle = 2 * pi / n - + self.reserveFaceAndVertexCount((n if side else 0) + (n-2 if bottom else 0), n+1) - + # Vertex 0 is the apex, vertices 1..n are the bottom self.addVertex(0, d, 0) for i in range(n): self.addVertex(-r * sin(angle * i), -d, -r * cos(angle * i)) - + # Side face vertices go: up down right if side: for i in range(n): @@ -313,7 +314,7 @@ class X3DReader(MeshReader): if bottom: for i in range(2, n): self.addTri(1, i, i+1) - + def processGeometryCylinder(self, node): r = readFloat(node, "radius", 1) height = readFloat(node, "height", 2) @@ -321,13 +322,13 @@ class X3DReader(MeshReader): side = readBoolean(node, "side", True) top = readBoolean(node, "top", True) n = readInt(node, "subdivision", DEFAULT_SUBDIV) - + nn = n * 2 angle = 2 * pi / n hh = height/2 - + self.reserveFaceAndVertexCount((nn if side else 0) + (n - 2 if top else 0) + (n - 2 if bottom else 0), nn) - + # The seam is at x=0, z=-r, vertices go ccw - # to pos x, to neg z, to neg x, back to neg z for i in range(n): @@ -335,18 +336,18 @@ class X3DReader(MeshReader): rc = -r * cos(angle * i) self.addVertex(rs, hh, rc) self.addVertex(rs, -hh, rc) - + if side: for i in range(n): ni = (i + 1) % n self.addQuad(ni * 2 + 1, ni * 2, i * 2, i * 2 + 1) - + for i in range(2, nn-3, 2): if top: self.addTri(0, i, i+2) if bottom: self.addTri(1, i+1, i+3) - + # Semi-primitives def processGeometryElevationGrid(self, node): @@ -356,21 +357,21 @@ class X3DReader(MeshReader): nz = readInt(node, "zDimension", 0) height = readFloatArray(node, "height", False) ccw = readBoolean(node, "ccw", True) - + if nx <= 0 or nz <= 0 or len(height) < nx*nz: return # That's weird, the wording of the standard suggests grids with zero quads are somehow valid - + self.reserveFaceAndVertexCount(2*(nx-1)*(nz-1), nx*nz) - + for z in range(nz): for x in range(nx): self.addVertex(x * dx, height[z*nx + x], z * dz) - + for z in range(1, nz): for x in range(1, nx): self.addTriFlip((z - 1)*nx + x - 1, z*nx + x, (z - 1)*nx + x, ccw) self.addTriFlip((z - 1)*nx + x - 1, z*nx + x - 1, z*nx + x, ccw) - + def processGeometryExtrusion(self, node): ccw = readBoolean(node, "ccw", True) begin_cap = readBoolean(node, "beginCap", True) @@ -384,23 +385,23 @@ class X3DReader(MeshReader): # This converts X3D's axis/angle rotation to a 3x3 numpy matrix def toRotationMatrix(rot): (x, y, z) = rot[:3] - a = rot[3] + a = rot[3] s = sin(a) c = cos(a) t = 1-c return numpy.array(( (x * x * t + c, x * y * t - z*s, x * z * t + y * s), (x * y * t + z*s, y * y * t + c, y * z * t - x * s), - (x * z * t - y * s, y * z * t + x * s, z * z * t + c))) - + (x * z * t - y * s, y * z * t + x * s, z * z * t + c))) + orient = [toRotationMatrix(orient[i:i+4]) if orient[i+3] != 0 else None for i in range(0, len(orient), 4)] - + scale = readFloatArray(node, "scale", None) if scale: scale = [numpy.array(((scale[i], 0, 0), (0, 1, 0), (0, 0, scale[i+1]))) if scale[i] != 1 or scale[i+1] != 1 else None for i in range(0, len(scale), 2)] - - + + # Special treatment for the closed spine and cross section. # Let's save some memory by not creating identical but distinct vertices; # later we'll introduce conditional logic to link the last vertex with @@ -413,14 +414,14 @@ class X3DReader(MeshReader): ncf = nc if crossClosed else nc - 1 # Face count along the cross; for closed cross, it's the same as the # respective vertex count - + spine_closed = spine[0] == spine[-1] if spine_closed: spine = spine[:-1] ns = len(spine) spine = [Vector(*s) for s in spine] nsf = ns if spine_closed else ns - 1 - + # This will be used for fallback, where the current spine point joins # two collinear spine segments. No need to recheck the case of the # closed spine/last-to-first point juncture; if there's an angle there, @@ -442,7 +443,7 @@ class X3DReader(MeshReader): if v.cross(orig_y).length() > EPSILON: # Spine at angle with global y - rotate the z accordingly a = v.cross(orig_y) # Axis of rotation to get to the Z - (x, y, z) = a.normalized().getData() + (x, y, z) = a.normalized().getData() s = a.length()/v.length() c = sqrt(1-s*s) t = 1-c @@ -452,7 +453,7 @@ class X3DReader(MeshReader): (x * z * t + y * s, y * z * t - x * s, z * z * t + c))) orig_z = Vector(*m.dot(orig_z.getData())) return orig_z - + self.reserveFaceAndVertexCount(2*nsf*ncf + (nc - 2 if begin_cap else 0) + (nc - 2 if end_cap else 0), ns*nc) z = None @@ -482,151 +483,151 @@ class X3DReader(MeshReader): y = spt - sprev # If there's more than one point in the spine, z is already set. # One point in the spline is an error anyway. - + z = z.normalized() y = y.normalized() x = y.cross(z) # Already normalized m = numpy.array(((x.x, y.x, z.x), (x.y, y.y, z.y), (x.z, y.z, z.z))) - + # Columns are the unit vectors for the xz plane for the cross-section if orient: mrot = orient[i] if len(orient) > 1 else orient[0] if not mrot is None: m = m.dot(mrot) # Tested against X3DOM, the result matches, still not sure :( - + if scale: mscale = scale[i] if len(scale) > 1 else scale[0] if not mscale is None: m = m.dot(mscale) - + # First the cross-section 2-vector is scaled, # then rotated (which may make it a 3-vector), # then applied to the xz plane unit vectors - + sptv3 = numpy.array(spt.getData()[:3]) for cpt in cross: v = sptv3 + m.dot(cpt) self.addVertex(*v) - + if begin_cap: self.addFace([x for x in range(nc - 1, -1, -1)], ccw) - + # Order of edges in the face: forward along cross, forward along spine, # backward along cross, backward along spine, flipped if now ccw. # This order is assumed later in the texture coordinate assignment; # please don't change without syncing. - + for s in range(ns - 1): for c in range(ncf): self.addQuadFlip(s * nc + c, s * nc + (c + 1) % nc, (s + 1) * nc + (c + 1) % nc, (s + 1) * nc + c, ccw) - + if spine_closed: # The faces between the last and the first spine points b = (ns - 1) * nc for c in range(ncf): self.addQuadFlip(b + c, b + (c + 1) % nc, (c + 1) % nc, c, ccw) - + if end_cap: self.addFace([(ns - 1) * nc + x for x in range(0, nc)], ccw) - + # Triangle meshes # Helper for numerous nodes with a Coordinate subnode holding vertices # That all triangle meshes and IndexedFaceSet - # num_faces can be a function, in case the face count is a function of vertex count + # num_faces can be a function, in case the face count is a function of vertex count def startCoordMesh(self, node, num_faces): ccw = readBoolean(node, "ccw", True) self.readVertices(node) # This will allocate and fill the vertex array if hasattr(num_faces, "__call__"): num_faces = num_faces(self.getVertexCount()) self.reserveFaceCount(num_faces) - + return ccw - + def processGeometryIndexedTriangleSet(self, node): index = readIntArray(node, "index", []) num_faces = len(index) // 3 ccw = int(self.startCoordMesh(node, num_faces)) - + for i in range(0, num_faces*3, 3): self.addTri(index[i + 1 - ccw], index[i + ccw], index[i+2]) - + def processGeometryIndexedTriangleStripSet(self, node): strips = readIndex(node, "index") ccw = int(self.startCoordMesh(node, sum([len(strip) - 2 for strip in strips]))) - + for strip in strips: sccw = ccw # Running CCW value, reset for each strip for i in range(len(strip) - 2): self.addTri(strip[i + 1 - sccw], strip[i + sccw], strip[i+2]) sccw = 1 - sccw - + def processGeometryIndexedTriangleFanSet(self, node): fans = readIndex(node, "index") ccw = int(self.startCoordMesh(node, sum([len(fan) - 2 for fan in fans]))) - + for fan in fans: for i in range(1, len(fan) - 1): self.addTri(fan[0], fan[i + 1 - ccw], fan[i + ccw]) - + def processGeometryTriangleSet(self, node): ccw = int(self.startCoordMesh(node, lambda num_vert: num_vert // 3)) for i in range(0, self.getVertexCount(), 3): self.addTri(i + 1 - ccw, i + ccw, i+2) - + def processGeometryTriangleStripSet(self, node): strips = readIntArray(node, "stripCount", []) ccw = int(self.startCoordMesh(node, sum([n-2 for n in strips]))) - + vb = 0 for n in strips: sccw = ccw - for i in range(n-2): + for i in range(n-2): self.addTri(vb + i + 1 - sccw, vb + i + sccw, vb + i + 2) sccw = 1 - sccw vb += n - + def processGeometryTriangleFanSet(self, node): fans = readIntArray(node, "fanCount", []) ccw = int(self.startCoordMesh(node, sum([n-2 for n in fans]))) - + vb = 0 for n in fans: - for i in range(1, n-1): + for i in range(1, n-1): self.addTri(vb, vb + i + 1 - ccw, vb + i + ccw) vb += n - + # Quad geometries from the CAD module, might be relevant for printing - + def processGeometryQuadSet(self, node): ccw = self.startCoordMesh(node, lambda num_vert: 2*(num_vert // 4)) for i in range(0, self.getVertexCount(), 4): self.addQuadFlip(i, i+1, i+2, i+3, ccw) - + def processGeometryIndexedQuadSet(self, node): index = readIntArray(node, "index", []) num_quads = len(index) // 4 ccw = self.startCoordMesh(node, num_quads*2) - + for i in range(0, num_quads*4, 4): self.addQuadFlip(index[i], index[i+1], index[i+2], index[i+3], ccw) - + # 2D polygon geometries # Won't work for now, since Cura expects every mesh to have a nontrivial convex hull # The only way around that is merging meshes. - + def processGeometryDisk2D(self, node): innerRadius = readFloat(node, "innerRadius", 0) outerRadius = readFloat(node, "outerRadius", 1) n = readInt(node, "subdivision", DEFAULT_SUBDIV) - + angle = 2 * pi / n - + self.reserveFaceAndVertexCount(n*4 if innerRadius else n-2, n*2 if innerRadius else n) - + for i in range(n): s = sin(angle * i) c = cos(angle * i) @@ -635,11 +636,11 @@ class X3DReader(MeshReader): self.addVertex(innerRadius*c, innerRadius*s, 0) ni = (i+1) % n self.addQuad(2*i, 2*ni, 2*ni+1, 2*i+1) - + if not innerRadius: for i in range(2, n): self.addTri(0, i-1, i) - + def processGeometryRectangle2D(self, node): (x, y) = readFloatArray(node, "size", (2, 2)) self.reserveFaceAndVertexCount(2, 4) @@ -648,7 +649,7 @@ class X3DReader(MeshReader): self.addVertex(x/2, y/2, 0) self.addVertex(-x/2, y/2, 0) self.addQuad(0, 1, 2, 3) - + def processGeometryTriangleSet2D(self, node): verts = readFloatArray(node, "vertices", ()) num_faces = len(verts) // 6; @@ -656,25 +657,25 @@ class X3DReader(MeshReader): self.reserveFaceAndVertexCount(num_faces, num_faces * 3) for vert in verts: self.addVertex(*vert) - + # The front face is on the +Z side, so CCW is a variable for i in range(0, num_faces*3, 3): a = Vector(*verts[i+2]) - Vector(*verts[i]) b = Vector(*verts[i+1]) - Vector(*verts[i]) self.addTriFlip(i, i+1, i+2, a.x*b.y > a.y*b.x) - + # General purpose polygon mesh def processGeometryIndexedFaceSet(self, node): faces = readIndex(node, "coordIndex") ccw = self.startCoordMesh(node, sum([len(face) - 2 for face in faces])) - + for face in faces: if len(face) == 3: self.addTriFlip(face[0], face[1], face[2], ccw) elif len(face) > 3: self.addFace(face, ccw) - + geometry_importers = { "IndexedFaceSet": processGeometryIndexedFaceSet, "IndexedTriangleSet": processGeometryIndexedTriangleSet, @@ -695,7 +696,7 @@ class X3DReader(MeshReader): "Cylinder": processGeometryCylinder, "Cone": processGeometryCone } - + # Parses the Coordinate.@point field, fills the verts array. def readVertices(self, node): for c in node: @@ -704,9 +705,9 @@ class X3DReader(MeshReader): if not c is None: pt = c.attrib.get("point") if pt: - # allow the list of float values in 'point' attribute to - # be separated by commas or whitespace as per spec of - # XML encoding of X3D + # allow the list of float values in 'point' attribute to + # be separated by commas or whitespace as per spec of + # XML encoding of X3D # Ref ISO/IEC 19776-1:2015 : Section 5.1.2 co = [float(x) for vec in pt.split(',') for x in vec.split()] num_verts = len(co) // 3 @@ -715,57 +716,57 @@ class X3DReader(MeshReader): # Group by three for i in range(num_verts): self.verts[:3,i] = co[3*i:3*i+3] - + # Mesh builder helpers - + def reserveFaceAndVertexCount(self, num_faces, num_verts): # Unlike the Cura MeshBuilder, we use 4-vectors stored as columns for easier transform self.verts = numpy.zeros((4, num_verts), dtype=numpy.float32) self.verts[3,:] = numpy.ones((num_verts), dtype=numpy.float32) self.num_verts = 0 self.reserveFaceCount(num_faces) - + def reserveFaceCount(self, num_faces): self.faces = numpy.zeros((num_faces, 3), dtype=numpy.int32) self.num_faces = 0 - + def getVertexCount(self): return self.verts.shape[1] - + def addVertex(self, x, y, z): self.verts[0, self.num_verts] = x self.verts[1, self.num_verts] = y self.verts[2, self.num_verts] = z self.num_verts += 1 - + # Indices are 0-based for this shape, but they won't be zero-based in the merged mesh def addTri(self, a, b, c): self.faces[self.num_faces, 0] = self.index_base + a self.faces[self.num_faces, 1] = self.index_base + b self.faces[self.num_faces, 2] = self.index_base + c self.num_faces += 1 - + def addTriFlip(self, a, b, c, ccw): if ccw: self.addTri(a, b, c) else: self.addTri(b, a, c) - + # Needs to be convex, but not necessaily planar # Assumed ccw, cut along the ac diagonal def addQuad(self, a, b, c, d): self.addTri(a, b, c) self.addTri(c, d, a) - + def addQuadFlip(self, a, b, c, d, ccw): if ccw: self.addTri(a, b, c) self.addTri(c, d, a) else: self.addTri(a, c, b) - self.addTri(c, a, d) - - + self.addTri(c, a, d) + + # Arbitrary polygon triangulation. # Doesn't assume convexity and doesn't check the "convex" flag in the file. # Works by the "cutting of ears" algorithm: @@ -776,13 +777,13 @@ class X3DReader(MeshReader): def addFace(self, indices, ccw): # Resolve indices to coordinates for faster math face = [Vector(data=self.verts[0:3, i]) for i in indices] - + # Need a normal to the plane so that we can know which vertices form inner angles normal = findOuterNormal(face) - + if not normal: # Couldn't find an outer edge, non-planar polygon maybe? return - + # Find the vertex with the smallest inner angle and no points inside, cut off. Repeat until done n = len(face) vi = [i for i in range(n)] # We'll be using this to kick vertices from the face @@ -807,17 +808,17 @@ class X3DReader(MeshReader): if pointInsideTriangle(vx, next, prev, nextXprev): no_points_inside = False break - + if no_points_inside: max_cos = cos i_min = i - + self.addTriFlip(indices[vi[(i_min + n - 1) % n]], indices[vi[i_min]], indices[vi[(i_min + 1) % n]], ccw) vi.pop(i_min) n -= 1 self.addTriFlip(indices[vi[0]], indices[vi[1]], indices[vi[2]], ccw) - + # ------------------------------------------------------------ # X3D field parsers # ------------------------------------------------------------ @@ -844,7 +845,7 @@ def readInt(node, attr, default): if not s: return default return int(s, 0) - + def readBoolean(node, attr, default): s = node.attrib.get(attr) if not s: @@ -873,8 +874,8 @@ def readIndex(node, attr): chunk.append(v[i]) if chunk: chunks.append(chunk) - return chunks - + return chunks + # Given a face as a sequence of vectors, returns a normal to the polygon place that forms a right triple # with a vector along the polygon sequence and a vector backwards def findOuterNormal(face): @@ -894,25 +895,25 @@ def findOuterNormal(face): if rejection.dot(prev_rejection) < -EPSILON: # points on both sides of the edge - not an outer one is_outer = False break - elif rejection.length() > prev_rejection.length(): # Pick a greater rejection for numeric stability + elif rejection.length() > prev_rejection.length(): # Pick a greater rejection for numeric stability prev_rejection = rejection - + if is_outer: # Found an outer edge, prev_rejection is the rejection inside the face. Generate a normal. return edge.cross(prev_rejection) return False -# Given two *collinear* vectors a and b, returns the coefficient that takes b to a. +# Given two *collinear* vectors a and b, returns the coefficient that takes b to a. # No error handling. -# For stability, taking the ration between the biggest coordinates would be better... +# For stability, taking the ration between the biggest coordinates would be better... def ratio(a, b): if b.x > EPSILON or b.x < -EPSILON: return a.x / b.x elif b.y > EPSILON or b.y < -EPSILON: return a.y / b.y else: - return a.z / b.z - + return a.z / b.z + def pointInsideTriangle(vx, next, prev, nextXprev): vxXprev = vx.cross(prev) r = ratio(vxXprev, nextXprev) @@ -921,4 +922,4 @@ def pointInsideTriangle(vx, next, prev, nextXprev): vxXnext = vx.cross(next); s = -ratio(vxXnext, nextXprev) return s > 0 and (s + r) < 1 - + diff --git a/resources/qml/Cura.qml b/resources/qml/Cura.qml index 03d0ce9ecd..29d8b439a8 100644 --- a/resources/qml/Cura.qml +++ b/resources/qml/Cura.qml @@ -409,6 +409,7 @@ UM.MainWindow { id: objectsList; visible: false; + //z: -10; anchors { top: objectsButton.top; diff --git a/resources/qml/ObjectsList.qml b/resources/qml/ObjectsList.qml index 4a7c84c41e..cde7f065fa 100644 --- a/resources/qml/ObjectsList.qml +++ b/resources/qml/ObjectsList.qml @@ -55,7 +55,7 @@ Rectangle //anchors.right: parent.right width: parent.width - 2 * UM.Theme.getSize("default_margin").width - 30 text: Cura.ObjectManager.getItem(index).name; - color: Cura.ObjectManager.getItem(index).isSelected ? palette.highlightedText : palette.text + color: Cura.ObjectManager.getItem(index).isSelected ? palette.highlightedText : (Cura.ObjectManager.getItem(index).isOutsideBuildArea ? palette.mid : palette.text) elide: Text.ElideRight } @@ -95,7 +95,7 @@ Rectangle topMargin: UM.Theme.getSize("default_margin").height; left: parent.left; leftMargin: UM.Theme.getSize("default_margin").height; - bottom: buildPlateSelection.top; + bottom: filterBuildPlateCheckbox.top; bottomMargin: UM.Theme.getSize("default_margin").height; } @@ -115,6 +115,25 @@ Rectangle } } + + CheckBox + { + id: filterBuildPlateCheckbox + checked: boolCheck(UM.Preferences.getValue("view/filter_current_build_plate")) + onClicked: UM.Preferences.setValue("view/filter_current_build_plate", checked) + + text: catalog.i18nc("@option:check","Filter active build plate"); + + anchors + { + left: parent.left; + topMargin: UM.Theme.getSize("default_margin").height; + bottomMargin: UM.Theme.getSize("default_margin").height; + leftMargin: UM.Theme.getSize("default_margin").height; + bottom: buildPlateSelection.top; + } + } + ListModel { id: buildPlatesModel diff --git a/resources/qml/Preferences/GeneralPage.qml b/resources/qml/Preferences/GeneralPage.qml index ad6c2ce050..c74fb5720d 100644 --- a/resources/qml/Preferences/GeneralPage.qml +++ b/resources/qml/Preferences/GeneralPage.qml @@ -304,7 +304,7 @@ UM.PreferencesPage text: catalog.i18nc("@option:check","Slice automatically"); } } - + Item { //: Spacer @@ -451,6 +451,20 @@ UM.PreferencesPage text: catalog.i18nc("@label","Opening and saving files") } + UM.TooltipArea { + width: childrenRect.width + height: childrenRect.height + text: catalog.i18nc("@info:tooltip","Should newly loaded models be arranged on the build palte?") + + CheckBox + { + id: arrangeOnLoadCheckbox + text: catalog.i18nc("@option:check","Arrange objects on load") + checked: boolCheck(UM.Preferences.getValue("cura/arrange_objects_on_load")) + onCheckedChanged: UM.Preferences.setValue("cura/arrange_objects_on_load", checked) + } + } + UM.TooltipArea { width: childrenRect.width height: childrenRect.height