Browse Source

Merge branch 'master' into feature_zhop_over_other_material

Tim Kuipers 8 years ago
parent
commit
3e67291e41
10 changed files with 293 additions and 290 deletions
  1. 6 0
      CMakeLists.txt
  2. 10 6
      cura/BuildVolume.py
  3. 178 69
      cura/ConvexHullDecorator.py
  4. 0 110
      cura/ConvexHullJob.py
  5. 12 22
      cura/ConvexHullNode.py
  6. 23 7
      cura/CuraApplication.py
  7. 22 22
      cura/ExtruderManager.py
  8. 31 2
      cura/ExtrudersModel.py
  9. 1 1
      cura/Layer.py
  10. 10 51
      cura/LayerData.py

+ 6 - 0
CMakeLists.txt

@@ -6,6 +6,12 @@ include(GNUInstallDirs)
 
 set(URANIUM_SCRIPTS_DIR "${CMAKE_SOURCE_DIR}/../uranium/scripts" CACHE DIRECTORY "The location of the scripts directory of the Uranium repository")
 
+# Tests
+# Note that we use exit 0 here to not mark the build as a failure on test failure
+add_custom_target(tests)
+add_custom_command(TARGET tests POST_BUILD COMMAND "PYTHONPATH=${CMAKE_SOURCE_DIR}/../Uranium/:${CMAKE_SOURCE_DIR}" ${PYTHON_EXECUTABLE} -m pytest -r a --junitxml=${CMAKE_BINARY_DIR}/junit.xml ${CMAKE_SOURCE_DIR} || exit 0)
+
+
 set(CURA_VERSION "master" CACHE STRING "Version name of Cura")
 set(CURA_BUILDTYPE "" CACHE STRING "Build type of Cura, eg. 'PPA'")
 configure_file(cura/CuraVersion.py.in CuraVersion.py @ONLY)

+ 10 - 6
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)

+ 178 - 69
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", "value"), 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
-
-    def setConvexHullBoundary(self, hull):
-        self._convex_hull_boundary = hull
-
-    def setConvexHullHeadFull(self, hull):
-        self._convex_hull_head_full = hull
+        if self._node is None:
+            return None
 
-    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:
+        if self._global_stack:
+            if self._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
+                return self._compute2DConvexHull()
+        return None
+
+    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
+
+        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
+
+        # 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", "value"), 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 _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()
+    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())

+ 0 - 110
cura/ConvexHullJob.py

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

+ 12 - 22
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)
 

+ 23 - 7
cura/CuraApplication.py

@@ -44,12 +44,12 @@ from . import ZOffsetDecorator
 from . import CuraSplashScreen
 from . import MachineManagerModel
 from . import ContainerSettingsModel
+from . import MachineActionManager
 
 from PyQt5.QtCore import pyqtSlot, QUrl, pyqtSignal, pyqtProperty, QEvent, Q_ENUMS
 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
@@ -100,6 +100,8 @@ class CuraApplication(QtApplication):
         SettingDefinition.addSupportedProperty("settable_globally", DefinitionPropertyType.Any, default = True)
         SettingDefinition.addSettingType("extruder", int, str, UM.Settings.Validator)
 
+        self._machine_action_manager = MachineActionManager.MachineActionManager()
+
         super().__init__(name = "cura", version = CuraVersion, buildtype = CuraBuildType)
 
         self.setWindowIcon(QIcon(Resources.getPath(Resources.Images, "cura-icon.png")))
@@ -122,7 +124,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,10 +365,12 @@ 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)
 
+        qmlRegisterSingletonType(MachineActionManager.MachineActionManager, "Cura", 1, 0, "MachineActionManager", self.getMachineActionManager)
         self.setMainQml(Resources.getPath(self.ResourceTypes.QmlFiles, "Cura.qml"))
         self._qml_import_paths.append(Resources.getPath(self.ResourceTypes.QmlFiles))
         self.initializeEngine()
@@ -382,6 +387,12 @@ class CuraApplication(QtApplication):
 
             self.exec_()
 
+    ##  Get the machine action manager
+    #   We ignore any *args given to this, as we also register the machine manager as qml singleton.
+    #   It wants to give this function an engine and script engine, but we don't care about that.
+    def getMachineActionManager(self, *args):
+        return self._machine_action_manager
+
     ##   Handle Qt events
     def event(self, event):
         if event.type() == QEvent.FileOpen:
@@ -468,12 +479,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 +751,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,
@@ -797,3 +809,7 @@ class CuraApplication(QtApplication):
 
     def _addProfileWriter(self, profile_writer):
         pass
+
+    @pyqtSlot("QSize")
+    def setMinimumWindowSize(self, size):
+        self.getMainWindow().setMinimumSize(size)

+ 22 - 22
cura/ExtruderManager.py

@@ -111,7 +111,7 @@ class ExtruderManager(QObject):
     ##  Creates a container stack for an extruder train.
     #
     #   The container stack has an extruder definition at the bottom, which is
-    #   linked to a machine definition. Then it has a nozzle profile, a material
+    #   linked to a machine definition. Then it has a variant profile, a material
     #   profile, a quality profile and a user profile, in that order.
     #
     #   The resulting container stack is added to the registry.
@@ -136,31 +136,31 @@ class ExtruderManager(QObject):
         container_stack.addMetaDataEntry("position", position)
         container_stack.addContainer(extruder_definition)
 
-        #Find the nozzle to use for this extruder.
-        nozzle = container_registry.getEmptyInstanceContainer()
-        if machine_definition.getMetaDataEntry("has_nozzles", default = "False") == "True":
-            #First add any nozzle. Later, overwrite with preference if the preference is valid.
-            nozzles = container_registry.findInstanceContainers(machine = machine_id, type = "nozzle")
-            if len(nozzles) >= 1:
-                nozzle = nozzles[0]
-            preferred_nozzle_id = machine_definition.getMetaDataEntry("preferred_nozzle")
-            if preferred_nozzle_id:
-                preferred_nozzles = container_registry.findInstanceContainers(id = preferred_nozzle_id, type = "nozzle")
-                if len(preferred_nozzles) >= 1:
-                    nozzle = preferred_nozzles[0]
+        #Find the variant to use for this extruder.
+        variant = container_registry.getEmptyInstanceContainer()
+        if machine_definition.getMetaDataEntry("has_variants"):
+            #First add any variant. Later, overwrite with preference if the preference is valid.
+            variants = container_registry.findInstanceContainers(definition = machine_id, type = "variant")
+            if len(variants) >= 1:
+                variant = variants[0]
+            preferred_variant_id = machine_definition.getMetaDataEntry("preferred_variant")
+            if preferred_variant_id:
+                preferred_variants = container_registry.findInstanceContainers(id = preferred_variant_id, type = "variant")
+                if len(preferred_variants) >= 1:
+                    variant = preferred_variants[0]
                 else:
-                    UM.Logger.log("w", "The preferred nozzle \"%s\" of machine %s doesn't exist or is not a nozzle profile.", preferred_nozzle_id, machine_id)
-                    #And leave it at the default nozzle.
-        container_stack.addContainer(nozzle)
+                    UM.Logger.log("w", "The preferred variant \"%s\" of machine %s doesn't exist or is not a variant profile.", preferred_variant_id, machine_id)
+                    #And leave it at the default variant.
+        container_stack.addContainer(variant)
 
-        #Find a material to use for this nozzle.
+        #Find a material to use for this variant.
         material = container_registry.getEmptyInstanceContainer()
-        if machine_definition.getMetaDataEntry("has_materials", default = "False") == "True":
+        if machine_definition.getMetaDataEntry("has_materials"):
             #First add any material. Later, overwrite with preference if the preference is valid.
-            if machine_definition.getMetaDataEntry("has_nozzle_materials", default = "False") == "True":
-                materials = container_registry.findInstanceContainers(type = "material", machine = machine_id, nozzle = nozzle.getId())
+            if machine_definition.getMetaDataEntry("has_variant_materials", default = "False") == "True":
+                materials = container_registry.findInstanceContainers(type = "material", definition = machine_id, variant = variant.getId())
             else:
-                materials = container_registry.findInstanceContainers(type = "material", machine = machine_id)
+                materials = container_registry.findInstanceContainers(type = "material", definition = machine_id)
             if len(materials) >= 1:
                 material = materials[0]
             preferred_material_id = machine_definition.getMetaDataEntry("preferred_material")
@@ -175,7 +175,7 @@ class ExtruderManager(QObject):
 
         #Find a quality to use for this extruder.
         quality = container_registry.getEmptyInstanceContainer()
-    
+
         #First add any quality. Later, overwrite with preference if the preference is valid.
         qualities = container_registry.findInstanceContainers(type = "quality")
         if len(qualities) >= 1:

+ 31 - 2
cura/ExtrudersModel.py

@@ -46,12 +46,17 @@ class ExtrudersModel(UM.Qt.ListModel.ListModel):
 
         self._add_global = False
 
+        self._active_extruder_stack = None
+
         #Listen to changes.
         manager = cura.ExtruderManager.ExtruderManager.getInstance()
         manager.extrudersChanged.connect(self._updateExtruders) #When the list of extruders changes in general.
-        UM.Application.globalContainerStackChanged.connect(self._updateExtruders) #When the current machine changes.
+        UM.Application.getInstance().globalContainerStackChanged.connect(self._updateExtruders) #When the current machine changes.
         self._updateExtruders()
 
+        manager.activeExtruderChanged.connect(self._onActiveExtruderChanged)
+        self._onActiveExtruderChanged()
+
     def setAddGlobal(self, add):
         if add != self._add_global:
             self._add_global = add
@@ -63,6 +68,26 @@ class ExtrudersModel(UM.Qt.ListModel.ListModel):
     def addGlobal(self):
         return self._add_global
 
+    def _onActiveExtruderChanged(self):
+        manager = cura.ExtruderManager.ExtruderManager.getInstance()
+        active_extruder_stack = manager.getActiveExtruderStack()
+        if self._active_extruder_stack != active_extruder_stack:
+            if self._active_extruder_stack:
+                self._active_extruder_stack.containersChanged.disconnect(self._onExtruderStackContainersChanged)
+
+            if active_extruder_stack:
+                # Update the model when the material container is changed
+                active_extruder_stack.containersChanged.connect(self._onExtruderStackContainersChanged)
+            self._active_extruder_stack = active_extruder_stack
+
+
+    def _onExtruderStackContainersChanged(self, container):
+        # The ExtrudersModel needs to be updated when the material-name or -color changes, because the user identifies extruders by material-name
+        if container.getMetaDataEntry("type") == "material":
+            self._updateExtruders()
+
+    modelChanged = pyqtSignal()
+
     ##  Update the list of extruders.
     #
     #   This should be called whenever the list of extruders changes.
@@ -85,7 +110,10 @@ class ExtrudersModel(UM.Qt.ListModel.ListModel):
             self.appendItem(item)
 
         for extruder in manager.getMachineExtruders(global_container_stack.getBottom().getId()):
+            extruder_name = extruder.getName()
             material = extruder.findContainer({ "type": "material" })
+            if material:
+                extruder_name = "%s (%s)" % (material.getName(), extruder_name)
             position = extruder.getBottom().getMetaDataEntry("position", default = "0") #Position in the definition.
             try:
                 position = int(position)
@@ -95,10 +123,11 @@ class ExtrudersModel(UM.Qt.ListModel.ListModel):
             colour = material.getMetaDataEntry("color_code", default = default_colour) if material else default_colour
             item = { #Construct an item with only the relevant information.
                 "id": extruder.getId(),
-                "name": extruder.getName(),
+                "name": extruder_name,
                 "colour": colour,
                 "index": position
             }
             self.appendItem(item)
 
         self.sort(lambda item: item["index"])
+        self.modelChanged.emit()

+ 1 - 1
cura/Layer.py

@@ -96,4 +96,4 @@ class Layer:
 
                 builder.addQuad(point1, point2, point3, point4, color = poly_color)
 
-        return builder.getData()
+        return builder.build()

+ 10 - 51
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())

Some files were not shown because too many files changed in this diff