Browse Source

Merge branch 'master' of github.com:ultimaker/Cura into feature_material_editing

* 'master' of github.com:ultimaker/Cura: (110 commits)
  Rearrange MachineActions on Machines page
  Skip containers that can not be serialized
  Make PerObjectSettingVisiblityHandler inherit SettingVisiblityHandler and some other cleanup
  Use the right property for the property provider
  Clean up indentation
  Remove unused code
  Fix expanded settings for Per Object settings tool
  Use the expanded categories from Cura to expand the proper categories on startup
  Starting UMOCheckup before connection was established now works correctly
  Fixed layout
  Added missing decorator CURA-1385
  Restored accidental delete
  Removed extraneous space
  Refactoring; Renaming firstRunWizard to machineActionsWizard
  Added BedLevel as supported action to UMO
  Refactoring (Renaming variables so they are more clear & update documentation)
  Remove some trailing spaces CURA-1615
  GCodeProfileReader: Removing useless containername
  Reenable Per Object Settings tool in simple mode if the current printer has multiextrusion
  Fix minor codereview issues
  ...
Arjen Hiemstra 8 years ago
parent
commit
f6866d703d
10 changed files with 289 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. 19 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)
 

+ 19 - 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
@@ -364,10 +367,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()
@@ -384,6 +389,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:
@@ -470,12 +481,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
@@ -740,7 +753,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,

+ 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