diff --git a/cura/BuildVolume.py b/cura/BuildVolume.py index f071bf4057..efc306c32d 100644 --- a/cura/BuildVolume.py +++ b/cura/BuildVolume.py @@ -36,6 +36,7 @@ class BuildVolume(SceneNode): self._disallowed_area_mesh = None self.setCalculateBoundingBox(False) + self._volume_aabb = None self._active_container_stack = None Application.getInstance().globalContainerStackChanged.connect(self._onGlobalContainerStackChanged) @@ -99,7 +100,7 @@ class BuildVolume(SceneNode): mb.addLine(Vector(min_w, max_h, min_d), Vector(min_w, max_h, max_d), color = self.VolumeOutlineColor) mb.addLine(Vector(max_w, max_h, min_d), Vector(max_w, max_h, max_d), color = self.VolumeOutlineColor) - self.setMeshData(mb.getData()) + self.setMeshData(mb.build()) mb = MeshBuilder() mb.addQuad( @@ -108,10 +109,10 @@ class BuildVolume(SceneNode): Vector(max_w, min_h - 0.2, max_d), Vector(min_w, min_h - 0.2, max_d) ) - self._grid_mesh = mb.getData() for n in range(0, 6): - v = self._grid_mesh.getVertex(n) - self._grid_mesh.setVertexUVCoordinates(n, v[0], v[2]) + v = mb.getVertex(n) + mb.setVertexUVCoordinates(n, v[0], v[2]) + self._grid_mesh = mb.build() disallowed_area_height = 0.1 disallowed_area_size = 0 @@ -136,11 +137,11 @@ class BuildVolume(SceneNode): size = 0 disallowed_area_size = max(size, disallowed_area_size) - self._disallowed_area_mesh = mb.getData() + self._disallowed_area_mesh = mb.build() else: self._disallowed_area_mesh = None - self._aabb = AxisAlignedBox(minimum = Vector(min_w, min_h - 1.0, min_d), maximum = Vector(max_w, max_h, max_d)) + self._volume_aabb = AxisAlignedBox(minimum = Vector(min_w, min_h - 1.0, min_d), maximum = Vector(max_w, max_h, max_d)) skirt_size = 0.0 @@ -158,6 +159,9 @@ class BuildVolume(SceneNode): Application.getInstance().getController().getScene()._maximum_bounds = scale_to_max_bounds + def getBoundingBox(self): + return self._volume_aabb + def _onGlobalContainerStackChanged(self): if self._active_container_stack: self._active_container_stack.propertyChanged.disconnect(self._onSettingPropertyChanged) diff --git a/cura/ConvexHullDecorator.py b/cura/ConvexHullDecorator.py index 39cdc36232..9fccd46988 100644 --- a/cura/ConvexHullDecorator.py +++ b/cura/ConvexHullDecorator.py @@ -1,116 +1,217 @@ from UM.Scene.SceneNodeDecorator import SceneNodeDecorator from UM.Application import Application +from UM.Math.Polygon import Polygon +from . import ConvexHullNode + +import numpy ## The convex hull decorator is a scene node decorator that adds the convex hull functionality to a scene node. # If a scene node has a convex hull decorator, it will have a shadow in which other objects can not be printed. class ConvexHullDecorator(SceneNodeDecorator): def __init__(self): super().__init__() - self._convex_hull = None - - # In case of printing all at once this is the same as the convex hull. - # For one at the time this is the area without the head. - self._convex_hull_boundary = None - - # In case of printing all at once this is the same as the convex hull. - # For one at the time this is area with intersection of mirrored head - self._convex_hull_head = None - - # In case of printing all at once this is the same as the convex hull. - # For one at the time this is area with intersection of full head - self._convex_hull_head_full = None self._convex_hull_node = None - self._convex_hull_job = None - - # Keep track of the previous parent so we can clear its convex hull when the object is reparented - self._parent_node = None + self._init2DConvexHullCache() self._global_stack = None Application.getInstance().globalContainerStackChanged.connect(self._onGlobalStackChanged) + Application.getInstance().getController().toolOperationStarted.connect(self._onChanged) + Application.getInstance().getController().toolOperationStopped.connect(self._onChanged) + self._onGlobalStackChanged() - #Application.getInstance().getMachineManager().activeProfileChanged.connect(self._onActiveProfileChanged) - #Application.getInstance().getMachineManager().activeMachineInstanceChanged.connect(self._onActiveMachineInstanceChanged) - #self._onActiveProfileChanged() def setNode(self, node): + previous_node = self._node + if previous_node is not None and node is not previous_node: + previous_node.transformationChanged.connect(self._onChanged) + previous_node.parentChanged.connect(self._onChanged) + super().setNode(node) - self._parent_node = node.getParent() - node.parentChanged.connect(self._onParentChanged) + + self._node.transformationChanged.connect(self._onChanged) + self._node.parentChanged.connect(self._onChanged) + + self._onChanged() ## Force that a new (empty) object is created upon copy. def __deepcopy__(self, memo): - copy = ConvexHullDecorator() - return copy + return ConvexHullDecorator() - ## Get the unmodified convex hull of the node + ## Get the unmodified 2D projected convex hull of the node def getConvexHull(self): - return self._convex_hull + if self._node is None: + return None + + hull = self._compute2DConvexHull() + if self._global_stack and self._node: + if self._global_stack.getProperty("print_sequence", "value") == "one_at_a_time" and not self._node.getParent().callDecoration("isGroup"): + hull = hull.getMinkowskiHull(Polygon(numpy.array(self._global_stack.getProperty("machine_head_polygon"), numpy.float32))) + return hull ## Get the convex hull of the node with the full head size def getConvexHullHeadFull(self): - if not self._convex_hull_head_full: - return self.getConvexHull() - return self._convex_hull_head_full + if self._node is None: + return None + + return self._compute2DConvexHeadFull() ## Get convex hull of the object + head size # In case of printing all at once this is the same as the convex hull. # For one at the time this is area with intersection of mirrored head def getConvexHullHead(self): - if not self._convex_hull_head: - return self.getConvexHull() - return self._convex_hull_head + if self._node is None: + return None + + if self._global_stack: + if self._global_stack.getProperty("print_sequence", "value") == "one_at_a_time" and not self._node.getParent().callDecoration("isGroup"): + return self._compute2DConvexHeadMin() + return None ## Get convex hull of the node # In case of printing all at once this is the same as the convex hull. # For one at the time this is the area without the head. def getConvexHullBoundary(self): - if not self._convex_hull_boundary: - return self.getConvexHull() - return self._convex_hull_boundary + if self._node is None: + return None - def setConvexHullBoundary(self, hull): - self._convex_hull_boundary = hull + if self._global_stack: + if self._global_stack("print_sequence") == "one_at_a_time" and not self._node.getParent().callDecoration("isGroup"): + # Printing one at a time and it's not an object in a group + return self._compute2DConvexHull() + return None - def setConvexHullHeadFull(self, hull): - self._convex_hull_head_full = hull + def recomputeConvexHull(self): + controller = Application.getInstance().getController() + root = controller.getScene().getRoot() + if self._node is None or controller.isToolOperationActive() or not self.__isDescendant(root, self._node): + if self._convex_hull_node: + self._convex_hull_node.setParent(None) + self._convex_hull_node = None + return - def setConvexHullHead(self, hull): - self._convex_hull_head = hull - - def setConvexHull(self, hull): - self._convex_hull = hull - if not hull and self._convex_hull_node: + convex_hull = self.getConvexHull() + if self._convex_hull_node: + if self._convex_hull_node.getHull() == convex_hull: + return self._convex_hull_node.setParent(None) - self._convex_hull_node = None - - def getConvexHullJob(self): - return self._convex_hull_job - - def setConvexHullJob(self, job): - self._convex_hull_job = job - - def getConvexHullNode(self): - return self._convex_hull_node - - def setConvexHullNode(self, node): - self._convex_hull_node = node + hull_node = ConvexHullNode.ConvexHullNode(self._node, convex_hull, root) + self._convex_hull_node = hull_node def _onSettingValueChanged(self, key, property_name): if key == "print_sequence" and property_name == "value": self._onChanged() - def _onChanged(self, *args): - if self._convex_hull_job: - self._convex_hull_job.cancel() - self.setConvexHull(None) + def _init2DConvexHullCache(self): + # Cache for the group code path in _compute2DConvexHull() + self._2d_convex_hull_group_child_polygon = None + self._2d_convex_hull_group_result = None - def _onParentChanged(self, node): - # Force updating the convex hull of the parent group if the object is in a group - if self._parent_node and self._parent_node.callDecoration("isGroup"): - self._parent_node.callDecoration("setConvexHull", None) - self._parent_node = self.getNode().getParent() + # Cache for the mesh code path in _compute2DConvexHull() + self._2d_convex_hull_mesh = None + self._2d_convex_hull_mesh_world_transform = None + self._2d_convex_hull_mesh_result = None + + def _compute2DConvexHull(self): + if self._node.callDecoration("isGroup"): + points = numpy.zeros((0, 2), dtype=numpy.int32) + for child in self._node.getChildren(): + child_hull = child.callDecoration("_compute2DConvexHull") + if child_hull: + points = numpy.append(points, child_hull.getPoints(), axis = 0) + + if points.size < 3: + return None + child_polygon = Polygon(points) + + # Check the cache + if child_polygon == self._2d_convex_hull_group_child_polygon: + return self._2d_convex_hull_group_result + + # First, calculate the normal convex hull around the points + convex_hull = child_polygon.getConvexHull() + + # Then, do a Minkowski hull with a simple 1x1 quad to outset and round the normal convex hull. + # This is done because of rounding errors. + rounded_hull = self._roundHull(convex_hull) + + # Store the result in the cache + self._2d_convex_hull_group_child_polygon = child_polygon + self._2d_convex_hull_group_result = rounded_hull + + return rounded_hull + + else: + rounded_hull = None + if self._node.getMeshData(): + mesh = self._node.getMeshData() + world_transform = self._node.getWorldTransformation() + + # Check the cache + if mesh is self._2d_convex_hull_mesh and world_transform == self._2d_convex_hull_mesh_world_transform: + return self._2d_convex_hull_mesh_result + + vertex_data = mesh.getConvexHullTransformedVertices(world_transform) + # Don't use data below 0. + # TODO; We need a better check for this as this gives poor results for meshes with long edges. + vertex_data = vertex_data[vertex_data[:,1] >= 0] + + if len(vertex_data) >= 4: + # Round the vertex data to 1/10th of a mm, then remove all duplicate vertices + # This is done to greatly speed up further convex hull calculations as the convex hull + # becomes much less complex when dealing with highly detailed models. + vertex_data = numpy.round(vertex_data, 1) + + vertex_data = vertex_data[:, [0, 2]] # Drop the Y components to project to 2D. + + # Grab the set of unique points. + # + # This basically finds the unique rows in the array by treating them as opaque groups of bytes + # which are as long as the 2 float64s in each row, and giving this view to numpy.unique() to munch. + # See http://stackoverflow.com/questions/16970982/find-unique-rows-in-numpy-array + vertex_byte_view = numpy.ascontiguousarray(vertex_data).view( + numpy.dtype((numpy.void, vertex_data.dtype.itemsize * vertex_data.shape[1]))) + _, idx = numpy.unique(vertex_byte_view, return_index=True) + vertex_data = vertex_data[idx] # Select the unique rows by index. + + hull = Polygon(vertex_data) + + if len(vertex_data) >= 4: + # First, calculate the normal convex hull around the points + convex_hull = hull.getConvexHull() + + # Then, do a Minkowski hull with a simple 1x1 quad to outset and round the normal convex hull. + # This is done because of rounding errors. + rounded_hull = convex_hull.getMinkowskiHull(Polygon(numpy.array([[-0.5, -0.5], [-0.5, 0.5], [0.5, 0.5], [0.5, -0.5]], numpy.float32))) + + # Store the result in the cache + self._2d_convex_hull_mesh = mesh + self._2d_convex_hull_mesh_world_transform = world_transform + self._2d_convex_hull_mesh_result = rounded_hull + + return rounded_hull + + def _getHeadAndFans(self): + return Polygon(numpy.array(self._global_stack.getProperty("machine_head_with_fans_polygon"), numpy.float32)) + + def _compute2DConvexHeadFull(self): + return self._compute2DConvexHull().getMinkowskiHull(self._getHeadAndFans()) + + def _compute2DConvexHeadMin(self): + headAndFans = self._getHeadAndFans() + mirrored = headAndFans.mirror([0, 0], [0, 1]).mirror([0, 0], [1, 0]) # Mirror horizontally & vertically. + head_and_fans = self._getHeadAndFans().intersectionConvexHulls(mirrored) + + # Min head hull is used for the push free + min_head_hull = self._compute2DConvexHull().getMinkowskiHull(head_and_fans) + return min_head_hull + + def _roundHull(self, convex_hull): + return convex_hull.getMinkowskiHull(Polygon(numpy.array([[-0.5, -0.5], [-0.5, 0.5], [0.5, 0.5], [0.5, -0.5]], numpy.float32))) + + def _onChanged(self, *args): + self.recomputeConvexHull() def _onGlobalStackChanged(self): if self._global_stack: @@ -124,3 +225,11 @@ class ConvexHullDecorator(SceneNodeDecorator): self._global_stack.containersChanged.connect(self._onChanged) self._onChanged() + + ## Returns true if node is a descendent or the same as the root node. + def __isDescendant(self, root, node): + if node is None: + return False + if root is node: + return True + return self.__isDescendant(root, node.getParent()) diff --git a/cura/ConvexHullJob.py b/cura/ConvexHullJob.py deleted file mode 100644 index 449b233b93..0000000000 --- a/cura/ConvexHullJob.py +++ /dev/null @@ -1,110 +0,0 @@ -# Copyright (c) 2015 Ultimaker B.V. -# Cura is released under the terms of the AGPLv3 or higher. - -from UM.Job import Job -from UM.Application import Application -from UM.Math.Polygon import Polygon - -import numpy -import copy -from . import ConvexHullNode - -## Job to async calculate the convex hull of a node. -class ConvexHullJob(Job): - def __init__(self, node): - super().__init__() - - self._node = node - - def run(self): - if not self._node: - return - ## If the scene node is a group, use the hull of the children to calculate its hull. - if self._node.callDecoration("isGroup"): - hull = Polygon(numpy.zeros((0, 2), dtype=numpy.int32)) - for child in self._node.getChildren(): - child_hull = child.callDecoration("getConvexHull") - if child_hull: - hull.setPoints(numpy.append(hull.getPoints(), child_hull.getPoints(), axis = 0)) - - if hull.getPoints().size < 3: - self._node.callDecoration("setConvexHull", None) - self._node.callDecoration("setConvexHullJob", None) - return - - Job.yieldThread() - - else: - if not self._node.getMeshData(): - return - mesh = self._node.getMeshData() - vertex_data = mesh.getTransformed(self._node.getWorldTransformation()).getVertices() - # Don't use data below 0. - # TODO; We need a better check for this as this gives poor results for meshes with long edges. - vertex_data = vertex_data[vertex_data[:,1] >= 0] - - # Round the vertex data to 1/10th of a mm, then remove all duplicate vertices - # This is done to greatly speed up further convex hull calculations as the convex hull - # becomes much less complex when dealing with highly detailed models. - vertex_data = numpy.round(vertex_data, 1) - - vertex_data = vertex_data[:, [0, 2]] # Drop the Y components to project to 2D. - - # Grab the set of unique points. - # - # This basically finds the unique rows in the array by treating them as opaque groups of bytes - # which are as long as the 2 float64s in each row, and giving this view to numpy.unique() to munch. - # See http://stackoverflow.com/questions/16970982/find-unique-rows-in-numpy-array - vertex_byte_view = numpy.ascontiguousarray(vertex_data).view(numpy.dtype((numpy.void, vertex_data.dtype.itemsize * vertex_data.shape[1]))) - _, idx = numpy.unique(vertex_byte_view, return_index=True) - vertex_data = vertex_data[idx] # Select the unique rows by index. - - hull = Polygon(vertex_data) - - # First, calculate the normal convex hull around the points - hull = hull.getConvexHull() - - # Then, do a Minkowski hull with a simple 1x1 quad to outset and round the normal convex hull. - # This is done because of rounding errors. - hull = hull.getMinkowskiHull(Polygon(numpy.array([[-0.5, -0.5], [-0.5, 0.5], [0.5, 0.5], [0.5, -0.5]], numpy.float32))) - - global_stack = Application.getInstance().getGlobalContainerStack() - if global_stack: - if global_stack.getProperty("print_sequence", "value")== "one_at_a_time" and not self._node.getParent().callDecoration("isGroup"): - # Printing one at a time and it's not an object in a group - self._node.callDecoration("setConvexHullBoundary", copy.deepcopy(hull)) - head_and_fans = Polygon(numpy.array(global_stack.getProperty("machine_head_with_fans_polygon", "value"), numpy.float32)) - - # Full head hull is used to actually check the order. - full_head_hull = hull.getMinkowskiHull(head_and_fans) - self._node.callDecoration("setConvexHullHeadFull", full_head_hull) - mirrored = copy.deepcopy(head_and_fans) - mirrored.mirror([0, 0], [0, 1]) #Mirror horizontally. - mirrored.mirror([0, 0], [1, 0]) #Mirror vertically. - head_and_fans = head_and_fans.intersectionConvexHulls(mirrored) - - # Min head hull is used for the push free - min_head_hull = hull.getMinkowskiHull(head_and_fans) - self._node.callDecoration("setConvexHullHead", min_head_hull) - hull = hull.getMinkowskiHull(Polygon(numpy.array(global_stack.getProperty("machine_head_polygon","value"),numpy.float32))) - else: - self._node.callDecoration("setConvexHullHead", None) - if self._node.getParent() is None: # Node was already deleted before job is done. - self._node.callDecoration("setConvexHullNode",None) - self._node.callDecoration("setConvexHull", None) - self._node.callDecoration("setConvexHullJob", None) - return - - hull_node = ConvexHullNode.ConvexHullNode(self._node, hull, Application.getInstance().getController().getScene().getRoot()) - self._node.callDecoration("setConvexHullNode", hull_node) - self._node.callDecoration("setConvexHull", hull) - self._node.callDecoration("setConvexHullJob", None) - - if self._node.getParent() and self._node.getParent().callDecoration("isGroup"): - job = self._node.getParent().callDecoration("getConvexHullJob") - if job: - job.cancel() - self._node.getParent().callDecoration("setConvexHull", None) - hull_node = self._node.getParent().callDecoration("getConvexHullNode") - if hull_node: - hull_node.setParent(None) diff --git a/cura/ConvexHullNode.py b/cura/ConvexHullNode.py index 905aeb16d4..be571d111e 100644 --- a/cura/ConvexHullNode.py +++ b/cura/ConvexHullNode.py @@ -9,7 +9,6 @@ from UM.Mesh.MeshBuilder import MeshBuilder # To create a mesh to display the c from UM.View.GL.OpenGL import OpenGL - class ConvexHullNode(SceneNode): ## Convex hull node is a special type of scene node that is used to display a 2D area, to indicate the # location an object uses on the buildplate. This area (or area's in case of one at a time printing) is @@ -31,21 +30,23 @@ class ConvexHullNode(SceneNode): # The node this mesh is "watching" self._node = node - self._node.transformationChanged.connect(self._onNodePositionChanged) - self._node.parentChanged.connect(self._onNodeParentChanged) self._node.decoratorsChanged.connect(self._onNodeDecoratorsChanged) self._onNodeDecoratorsChanged(self._node) self._convex_hull_head_mesh = None self._hull = hull - hull_mesh = self.createHullMesh(self._hull.getPoints()) - if hull_mesh: - self.setMeshData(hull_mesh) + if self._hull: + hull_mesh = self.createHullMesh(self._hull.getPoints()) + if hull_mesh: + self.setMeshData(hull_mesh) convex_hull_head = self._node.callDecoration("getConvexHullHead") if convex_hull_head: self._convex_hull_head_mesh = self.createHullMesh(convex_hull_head.getPoints()) + def getHull(self): + return self._hull + ## Actually create the mesh from the hullpoints # /param hull_points list of xy values # /return meshData @@ -62,7 +63,7 @@ class ConvexHullNode(SceneNode): mesh_builder.addFace(point_first, point_previous, point_new, color = self._color) point_previous = point_new # Prepare point_previous for the next triangle. - return mesh_builder.getData() + return mesh_builder.build() def getWatchedNode(self): return self._node @@ -73,24 +74,13 @@ class ConvexHullNode(SceneNode): self._shader.setUniformValue("u_color", self._color) if self.getParent(): - renderer.queueNode(self, transparent = True, shader = self._shader, backface_cull = True, sort = -8) - if self._convex_hull_head_mesh: - renderer.queueNode(self, shader = self._shader, transparent = True, mesh = self._convex_hull_head_mesh, backface_cull = True, sort = -8) + if self.getMeshData(): + renderer.queueNode(self, transparent = True, shader = self._shader, backface_cull = True, sort = -8) + if self._convex_hull_head_mesh: + renderer.queueNode(self, shader = self._shader, transparent = True, mesh = self._convex_hull_head_mesh, backface_cull = True, sort = -8) return True - def _onNodePositionChanged(self, node): - if node.callDecoration("getConvexHull"): - node.callDecoration("setConvexHull", None) - node.callDecoration("setConvexHullNode", None) - self.setParent(None) # Garbage collection should delete this node after a while. - - def _onNodeParentChanged(self, node): - if node.getParent(): - self.setParent(self._original_parent) - else: - self.setParent(None) - def _onNodeDecoratorsChanged(self, node): self._color = Color(35, 35, 35, 0.5) diff --git a/cura/CuraApplication.py b/cura/CuraApplication.py index 5f5880e3d5..12818e1e87 100644 --- a/cura/CuraApplication.py +++ b/cura/CuraApplication.py @@ -49,7 +49,6 @@ from PyQt5.QtCore import pyqtSlot, QUrl, pyqtSignal, pyqtProperty, QEvent, Q_ENU from PyQt5.QtGui import QColor, QIcon from PyQt5.QtQml import qmlRegisterUncreatableType, qmlRegisterSingletonType, qmlRegisterType -import ast #For literal eval of extruder setting types. import platform import sys import os.path @@ -122,7 +121,8 @@ class CuraApplication(QtApplication): self._i18n_catalog = None self._previous_active_tool = None self._platform_activity = False - self._scene_bounding_box = AxisAlignedBox() + self._scene_bounding_box = AxisAlignedBox.Null + self._job_name = None self._center_after_select = False self._camera_animation = None @@ -362,7 +362,8 @@ class CuraApplication(QtApplication): self.showSplashMessage(self._i18n_catalog.i18nc("@info:progress", "Loading interface...")) - ExtruderManager.ExtruderManager.getInstance() #Initialise extruder so as to listen to global container stack changes before the first global container stack is set. + # Initialise extruder so as to listen to global container stack changes before the first global container stack is set. + ExtruderManager.ExtruderManager.getInstance() qmlRegisterSingletonType(MachineManagerModel.MachineManagerModel, "Cura", 1, 0, "MachineManager", MachineManagerModel.createMachineManagerModel) @@ -468,12 +469,14 @@ class CuraApplication(QtApplication): count += 1 if not scene_bounding_box: - scene_bounding_box = copy.deepcopy(node.getBoundingBox()) + scene_bounding_box = node.getBoundingBox() else: - scene_bounding_box += node.getBoundingBox() + other_bb = node.getBoundingBox() + if other_bb is not None: + scene_bounding_box = scene_bounding_box + node.getBoundingBox() if not scene_bounding_box: - scene_bounding_box = AxisAlignedBox() + scene_bounding_box = AxisAlignedBox.Null if repr(self._scene_bounding_box) != repr(scene_bounding_box): self._scene_bounding_box = scene_bounding_box @@ -738,7 +741,6 @@ class CuraApplication(QtApplication): # Add all individual nodes to the selection Selection.add(child) - child.callDecoration("setConvexHull", None) op.push() # Note: The group removes itself from the scene once all its children have left it, diff --git a/cura/Layer.py b/cura/Layer.py index a95fbf64ef..904e5528a3 100644 --- a/cura/Layer.py +++ b/cura/Layer.py @@ -96,4 +96,4 @@ class Layer: builder.addQuad(point1, point2, point3, point4, color = poly_color) - return builder.getData() \ No newline at end of file + return builder.build() diff --git a/cura/LayerData.py b/cura/LayerData.py index 4aa7d1f4d4..ad5326373e 100644 --- a/cura/LayerData.py +++ b/cura/LayerData.py @@ -1,66 +1,25 @@ # Copyright (c) 2015 Ultimaker B.V. # Cura is released under the terms of the AGPLv3 or higher. -from .Layer import Layer -from .LayerPolygon import LayerPolygon from UM.Mesh.MeshData import MeshData -import numpy - - +## Class to holds the layer mesh and information about the layers. +# Immutable, use LayerDataBuilder to create one of these. class LayerData(MeshData): - def __init__(self): - super().__init__() - self._layers = {} - self._element_counts = {} - - def addLayer(self, layer): - if layer not in self._layers: - self._layers[layer] = Layer(layer) - - def addPolygon(self, layer, polygon_type, data, line_width): - if layer not in self._layers: - self.addLayer(layer) - - p = LayerPolygon(self, polygon_type, data, line_width) - self._layers[layer].polygons.append(p) + def __init__(self, vertices = None, normals = None, indices = None, colors = None, uvs = None, file_name = None, + center_position = None, layers=None, element_counts=None): + super().__init__(vertices=vertices, normals=normals, indices=indices, colors=colors, uvs=uvs, + file_name=file_name, center_position=center_position) + self._layers = layers + self._element_counts = element_counts def getLayer(self, layer): if layer in self._layers: return self._layers[layer] + else: + return None def getLayers(self): return self._layers def getElementCounts(self): return self._element_counts - - def setLayerHeight(self, layer, height): - if layer not in self._layers: - self.addLayer(layer) - - self._layers[layer].setHeight(height) - - def setLayerThickness(self, layer, thickness): - if layer not in self._layers: - self.addLayer(layer) - - self._layers[layer].setThickness(thickness) - - def build(self): - vertex_count = 0 - for layer, data in self._layers.items(): - vertex_count += data.vertexCount() - - vertices = numpy.empty((vertex_count, 3), numpy.float32) - colors = numpy.empty((vertex_count, 4), numpy.float32) - indices = numpy.empty((vertex_count, 2), numpy.int32) - - offset = 0 - for layer, data in self._layers.items(): - offset = data.build(offset, vertices, colors, indices) - self._element_counts[layer] = data.elementCount - - self.clear() - self.addVertices(vertices) - self.addColors(colors) - self.addIndices(indices.flatten()) diff --git a/cura/LayerDataBuilder.py b/cura/LayerDataBuilder.py new file mode 100644 index 0000000000..7e8e0e636b --- /dev/null +++ b/cura/LayerDataBuilder.py @@ -0,0 +1,72 @@ +# Copyright (c) 2015 Ultimaker B.V. +# Cura is released under the terms of the AGPLv3 or higher. + +from .Layer import Layer +from .LayerPolygon import LayerPolygon +from UM.Mesh.MeshBuilder import MeshBuilder +from .LayerData import LayerData + +import numpy + +## Builder class for constructing a LayerData object +class LayerDataBuilder(MeshBuilder): + def __init__(self): + super().__init__() + self._layers = {} + self._element_counts = {} + + def addLayer(self, layer): + if layer not in self._layers: + self._layers[layer] = Layer(layer) + + def addPolygon(self, layer, polygon_type, data, line_width): + if layer not in self._layers: + self.addLayer(layer) + + p = LayerPolygon(self, polygon_type, data, line_width) + self._layers[layer].polygons.append(p) + + def getLayer(self, layer): + if layer in self._layers: + return self._layers[layer] + + def getLayers(self): + return self._layers + + def getElementCounts(self): + return self._element_counts + + def setLayerHeight(self, layer, height): + if layer not in self._layers: + self.addLayer(layer) + + self._layers[layer].setHeight(height) + + def setLayerThickness(self, layer, thickness): + if layer not in self._layers: + self.addLayer(layer) + + self._layers[layer].setThickness(thickness) + + def build(self): + vertex_count = 0 + for layer, data in self._layers.items(): + vertex_count += data.vertexCount() + + vertices = numpy.empty((vertex_count, 3), numpy.float32) + colors = numpy.empty((vertex_count, 4), numpy.float32) + indices = numpy.empty((vertex_count, 2), numpy.int32) + + offset = 0 + for layer, data in self._layers.items(): + offset = data.build(offset, vertices, colors, indices) + self._element_counts[layer] = data.elementCount + + self.addVertices(vertices) + self.addColors(colors) + self.addIndices(indices.flatten()) + + return LayerData(vertices=self.getVertices(), normals=self.getNormals(), indices=self.getIndices(), + colors=self.getColors(), uvs=self.getUVCoordinates(), file_name=self.getFileName(), + center_position=self.getCenterPosition(), layers=self._layers, + element_counts=self._element_counts) diff --git a/cura/PlatformPhysics.py b/cura/PlatformPhysics.py index 78f2b4938b..c43d0d09d7 100644 --- a/cura/PlatformPhysics.py +++ b/cura/PlatformPhysics.py @@ -14,7 +14,6 @@ from UM.Preferences import Preferences from cura.ConvexHullDecorator import ConvexHullDecorator from . import PlatformPhysicsOperation -from . import ConvexHullJob from . import ZOffsetDecorator import copy @@ -27,7 +26,6 @@ class PlatformPhysics: self._controller.toolOperationStarted.connect(self._onToolOperationStarted) self._controller.toolOperationStopped.connect(self._onToolOperationStopped) self._build_volume = volume - self._enabled = True self._change_timer = QTimer() @@ -46,16 +44,13 @@ class PlatformPhysics: root = self._controller.getScene().getRoot() for node in BreadthFirstIterator(root): - if node is root or type(node) is not SceneNode: + if node is root or type(node) is not SceneNode or node.getBoundingBox() is None: continue bbox = node.getBoundingBox() - if not bbox or not bbox.isValid(): - self._change_timer.start() - continue - build_volume_bounding_box = copy.deepcopy(self._build_volume.getBoundingBox()) - build_volume_bounding_box.setBottom(-9001) # Ignore intersections with the bottom + # Ignore intersections with the bottom + build_volume_bounding_box = self._build_volume.getBoundingBox().set(bottom=-9001) node._outside_buildarea = False # Mark the node as outside the build volume if the bounding box test fails. @@ -67,9 +62,9 @@ class PlatformPhysics: if not (node.getParent() and node.getParent().callDecoration("isGroup")): #If an object is grouped, don't move it down z_offset = node.callDecoration("getZOffset") if node.getDecorator(ZOffsetDecorator.ZOffsetDecorator) else 0 if bbox.bottom > 0: - move_vector.setY(-bbox.bottom + z_offset) + move_vector = move_vector.set(y=-bbox.bottom + z_offset) elif bbox.bottom < z_offset: - move_vector.setY((-bbox.bottom) - z_offset) + move_vector = move_vector.set(y=(-bbox.bottom) - z_offset) #if not Float.fuzzyCompare(bbox.bottom, 0.0): # pass#move_vector.setY(-bbox.bottom) @@ -77,14 +72,9 @@ class PlatformPhysics: # If there is no convex hull for the node, start calculating it and continue. if not node.getDecorator(ConvexHullDecorator): node.addDecorator(ConvexHullDecorator()) - - if not node.callDecoration("getConvexHull"): - if not node.callDecoration("getConvexHullJob"): - job = ConvexHullJob.ConvexHullJob(node) - job.start() - node.callDecoration("setConvexHullJob", job) - - elif Preferences.getInstance().getValue("physics/automatic_push_free"): + node.callDecoration("recomputeConvexHull") + + if Preferences.getInstance().getValue("physics/automatic_push_free"): # Check for collisions between convex hulls for other_node in BreadthFirstIterator(root): # Ignore root, ourselves and anything that is not a normal SceneNode. @@ -125,8 +115,7 @@ class PlatformPhysics: if overlap is None: continue - move_vector.setX(overlap[0] * 1.1) - move_vector.setZ(overlap[1] * 1.1) + move_vector = move_vector.set(x=overlap[0] * 1.1, z=overlap[1] * 1.1) convex_hull = node.callDecoration("getConvexHull") if convex_hull: if not convex_hull.isValid(): @@ -139,7 +128,7 @@ class PlatformPhysics: node._outside_buildarea = True - if move_vector != Vector(): + if not Vector.Null.equals(move_vector, epsilon=1e-5): op = PlatformPhysicsOperation.PlatformPhysicsOperation(node, move_vector) op.push() diff --git a/cura/PlatformPhysicsOperation.py b/cura/PlatformPhysicsOperation.py index 5d2089e8af..fa58e45d9c 100644 --- a/cura/PlatformPhysicsOperation.py +++ b/cura/PlatformPhysicsOperation.py @@ -28,4 +28,4 @@ class PlatformPhysicsOperation(Operation): return group def __repr__(self): - return "PlatformPhysicsOperation(t = {0})".format(self._position) + return "PlatformPhysicsOperation(new_position = {0})".format(self._new_position) diff --git a/plugins/CuraEngineBackend/ProcessSlicedLayersJob.py b/plugins/CuraEngineBackend/ProcessSlicedLayersJob.py index 46b2d24bc5..d23f71e874 100644 --- a/plugins/CuraEngineBackend/ProcessSlicedLayersJob.py +++ b/plugins/CuraEngineBackend/ProcessSlicedLayersJob.py @@ -12,7 +12,7 @@ from UM.i18n import i18nCatalog from UM.Math.Vector import Vector -from cura import LayerData +from cura import LayerDataBuilder from cura import LayerDataDecorator import numpy @@ -63,7 +63,7 @@ class ProcessSlicedLayersJob(Job): return mesh = MeshData() - layer_data = LayerData.LayerData() + layer_data = LayerDataBuilder.LayerDataBuilder() layer_count = len(self._layers) # Find the minimum layer number @@ -115,7 +115,7 @@ class ProcessSlicedLayersJob(Job): self._progress.setProgress(progress) # We are done processing all the layers we got from the engine, now create a mesh out of the data - layer_data.build() + layer_mesh = layer_data.build() if self._abort_requested: if self._progress: @@ -124,7 +124,7 @@ class ProcessSlicedLayersJob(Job): # Add LayerDataDecorator to scene node to indicate that the node has layer data decorator = LayerDataDecorator.LayerDataDecorator() - decorator.setLayerData(layer_data) + decorator.setLayerData(layer_mesh) new_node.addDecorator(decorator) new_node.setMeshData(mesh) diff --git a/plugins/ImageReader/ImageReader.py b/plugins/ImageReader/ImageReader.py index 0d6c12b13d..9d70dde8e1 100644 --- a/plugins/ImageReader/ImageReader.py +++ b/plugins/ImageReader/ImageReader.py @@ -7,7 +7,7 @@ from PyQt5.QtGui import QImage, qRed, qGreen, qBlue from PyQt5.QtCore import Qt from UM.Mesh.MeshReader import MeshReader -from UM.Mesh.MeshData import MeshData +from UM.Mesh.MeshBuilder import MeshBuilder from UM.Scene.SceneNode import SceneNode from UM.Math.Vector import Vector from UM.Job import Job @@ -48,13 +48,9 @@ class ImageReader(MeshReader): return self._generateSceneNode(file_name, size, self._ui.peak_height, self._ui.base_height, self._ui.smoothing, 512, self._ui.image_color_invert) def _generateSceneNode(self, file_name, xz_size, peak_height, base_height, blur_iterations, max_size, image_color_invert): - mesh = None # TODO: @UnusedVariable - scene_node = None # TODO: @UnusedVariable - scene_node = SceneNode() - mesh = MeshData() - scene_node.setMeshData(mesh) + mesh = MeshBuilder() img = QImage(file_name) @@ -76,9 +72,9 @@ class ImageReader(MeshReader): scale_vector = Vector(xz_size, peak_height, xz_size) if width > height: - scale_vector.setZ(scale_vector.z * aspect) + scale_vector = scale_vector.set(z=scale_vector.z * aspect) elif height > width: - scale_vector.setX(scale_vector.x / aspect) + scale_vector = scale_vector.set(x=scale_vector.x / aspect) if width > max_size or height > max_size: scale_factor = max_size / width @@ -173,8 +169,8 @@ class ImageReader(MeshReader): geo_height = height_minus_one * texel_height # bottom - mesh.addFace(0, 0, 0, 0, 0, geo_height, geo_width, 0, geo_height) - mesh.addFace(geo_width, 0, geo_height, geo_width, 0, 0, 0, 0, 0) + mesh.addFaceByPoints(0, 0, 0, 0, 0, geo_height, geo_width, 0, geo_height) + mesh.addFaceByPoints(geo_width, 0, geo_height, geo_width, 0, 0, 0, 0, 0) # north and south walls for n in range(0, width_minus_one): @@ -187,11 +183,11 @@ class ImageReader(MeshReader): hs0 = height_data[height_minus_one, n] hs1 = height_data[height_minus_one, n + 1] - mesh.addFace(x, 0, 0, nx, 0, 0, nx, hn1, 0) - mesh.addFace(nx, hn1, 0, x, hn0, 0, x, 0, 0) + mesh.addFaceByPoints(x, 0, 0, nx, 0, 0, nx, hn1, 0) + mesh.addFaceByPoints(nx, hn1, 0, x, hn0, 0, x, 0, 0) - mesh.addFace(x, 0, geo_height, nx, 0, geo_height, nx, hs1, geo_height) - mesh.addFace(nx, hs1, geo_height, x, hs0, geo_height, x, 0, geo_height) + mesh.addFaceByPoints(x, 0, geo_height, nx, 0, geo_height, nx, hs1, geo_height) + mesh.addFaceByPoints(nx, hs1, geo_height, x, hs0, geo_height, x, 0, geo_height) # west and east walls for n in range(0, height_minus_one): @@ -204,12 +200,14 @@ class ImageReader(MeshReader): he0 = height_data[n, width_minus_one] he1 = height_data[n + 1, width_minus_one] - mesh.addFace(0, 0, y, 0, 0, ny, 0, hw1, ny) - mesh.addFace(0, hw1, ny, 0, hw0, y, 0, 0, y) + mesh.addFaceByPoints(0, 0, y, 0, 0, ny, 0, hw1, ny) + mesh.addFaceByPoints(0, hw1, ny, 0, hw0, y, 0, 0, y) - mesh.addFace(geo_width, 0, y, geo_width, 0, ny, geo_width, he1, ny) - mesh.addFace(geo_width, he1, ny, geo_width, he0, y, geo_width, 0, y) + mesh.addFaceByPoints(geo_width, 0, y, geo_width, 0, ny, geo_width, he1, ny) + mesh.addFaceByPoints(geo_width, he1, ny, geo_width, he0, y, geo_width, 0, y) mesh.calculateNormals(fast=True) + scene_node.setMeshData(mesh.build()) + return scene_node diff --git a/plugins/LayerView/LayerView.py b/plugins/LayerView/LayerView.py index 50ab7f968e..cd7a17a357 100644 --- a/plugins/LayerView/LayerView.py +++ b/plugins/LayerView/LayerView.py @@ -8,7 +8,7 @@ from UM.Event import Event, KeyEvent from UM.Signal import Signal from UM.Scene.Selection import Selection from UM.Math.Color import Color -from UM.Mesh.MeshData import MeshData +from UM.Mesh.MeshBuilder import MeshBuilder from UM.Job import Job from UM.Preferences import Preferences @@ -240,7 +240,7 @@ class _CreateTopLayersJob(Job): if self._cancel or not layer_data: return - layer_mesh = MeshData() + layer_mesh = MeshBuilder() for i in range(self._solid_layers): layer_number = self._layer_number - i if layer_number < 0: @@ -275,7 +275,7 @@ class _CreateTopLayersJob(Job): if not jump_mesh or jump_mesh.getVertices() is None: jump_mesh = None - self.setResult({ "layers": layer_mesh, "jumps": jump_mesh }) + self.setResult({ "layers": layer_mesh.build(), "jumps": jump_mesh }) def cancel(self): self._cancel = True