Browse Source

Merge branch 'master' into CURA-6329_add_crystallinity_setting

Ghostkeeper 5 years ago
parent
commit
adc6f79c9c

+ 43 - 0
.github/ISSUE_TEMPLATE/bug-report.md

@@ -0,0 +1,43 @@
+---
+name: Bug report
+about: Create a report to help us fix issues.
+title: ''
+labels: 'Type: Bug'
+assignees: ''
+
+---
+
+<!--
+The following template is useful for filing new issues. Processing an issue will go much faster when this is filled out, and issues which do not use this template WILL BE REMOVED.
+
+Before filing, PLEASE check if the issue already exists (either open or closed) by using the search bar on the issues page. If it does, comment there. Even if it's closed, we can reopen it based on your comment.
+
+Also, please note the application version in the title of the issue. For example: "[3.2.1] Cannot connect to 3rd-party printer". Please do not write things like "Request:" or "[BUG]" in the title; this is what labels are for.
+
+It is also helpful to attach a project (.3mf or .curaproject) file and Cura log file so we can debug issues quicker. Information about how to find the log file can be found at https://github.com/Ultimaker/Cura#logging-issues 
+
+To upload a project, try changing the extension to e.g. .curaproject.3mf.zip so that GitHub accepts uploading the file. Otherwise, we recommend http://wetransfer.com, but other file hosts like Google Drive or Dropbox work well too.
+
+Thank you for using Cura!
+-->
+
+**Application version**
+<!-- The version of the application this issue occurs with -->
+
+**Platform**
+<!-- Information about the operating system the issue occurs on. Include at least the operating system. In the case of visual glitches/issues, also include information about your graphics drivers and GPU. -->
+
+**Printer**
+<!-- Which printer was selected in Cura? If possible, please attach project file as .curaproject.3mf.zip -->
+
+**Reproduction steps**
+<!-- How did you encounter the bug? -->
+
+**Actual results**
+<!-- What happens after the above steps have been followed -->
+
+**Expected results**
+<!-- What should happen after the above steps have been followed -->
+
+**Additional information**
+<!-- Extra information relevant to the issue, like screenshots -->

+ 22 - 0
.github/ISSUE_TEMPLATE/feature_request.md

@@ -0,0 +1,22 @@
+---
+name: Feature request
+about: Suggest an idea for this project
+title: ''
+labels: 'Type: New Feature'
+assignees: ''
+
+---
+
+**Is your feature request related to a problem? Please describe.**
+<!-- A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] -->
+
+**Describe the solution you'd like**
+<!--A clear and concise description of what you want to happen. If possible, describe why you think this is a good solution.-->
+
+**Describe alternatives you've considered**
+<!-- A clear and concise description of any alternative solutions or features you've considered. Again, if possible, think about why these alternatives are not working out. -->
+
+**Affected users and/or printers**
+<!-- Who do you think will benefit from this? Is everyone going to benefit from these changes? Only a few people? --> 
+**Additional context**
+<!-- Add any other context or screenshots about the feature request here. -->

+ 312 - 255
cura/BuildVolume.py

@@ -1,6 +1,6 @@
-# Copyright (c) 2018 Ultimaker B.V.
+# Copyright (c) 2019 Ultimaker B.V.
 # Cura is released under the terms of the LGPLv3 or higher.
-from UM.Scene.Camera import Camera
+from UM.Mesh.MeshData import MeshData
 from cura.Scene.CuraSceneNode import CuraSceneNode
 from cura.Settings.ExtruderManager import ExtruderManager
 from UM.Application import Application #To modify the maximum zoom level.
@@ -20,13 +20,20 @@ from UM.Signal import Signal
 from PyQt5.QtCore import QTimer
 from UM.View.RenderBatch import RenderBatch
 from UM.View.GL.OpenGL import OpenGL
+from cura.Settings.GlobalStack import GlobalStack
+
 catalog = i18nCatalog("cura")
 
 import numpy
 import math
 import copy
 
-from typing import List, Optional
+from typing import List, Optional, TYPE_CHECKING, Any, Set, cast, Iterable, Dict
+
+if TYPE_CHECKING:
+    from cura.CuraApplication import CuraApplication
+    from cura.Settings.ExtruderStack import ExtruderStack
+    from UM.Settings.ContainerStack import ContainerStack
 
 # Radius of disallowed area in mm around prime. I.e. how much distance to keep from prime position.
 PRIME_CLEARANCE = 6.5
@@ -36,45 +43,46 @@ PRIME_CLEARANCE = 6.5
 class BuildVolume(SceneNode):
     raftThicknessChanged = Signal()
 
-    def __init__(self, application, parent = None):
+    def __init__(self, application: "CuraApplication", parent: Optional[SceneNode] = None) -> None:
         super().__init__(parent)
         self._application = application
         self._machine_manager = self._application.getMachineManager()
 
-        self._volume_outline_color = None
-        self._x_axis_color = None
-        self._y_axis_color = None
-        self._z_axis_color = None
-        self._disallowed_area_color = None
-        self._error_area_color = None
+        self._volume_outline_color = None  # type: Optional[Color]
+        self._x_axis_color = None  # type: Optional[Color]
+        self._y_axis_color = None  # type: Optional[Color]
+        self._z_axis_color = None  # type: Optional[Color]
+        self._disallowed_area_color = None  # type: Optional[Color]
+        self._error_area_color = None  # type: Optional[Color]
 
-        self._width = 0 #type: float
-        self._height = 0 #type: float
-        self._depth = 0 #type: float
-        self._shape = "" #type: str
+        self._width = 0  # type: float
+        self._height = 0  # type: float
+        self._depth = 0  # type: float
+        self._shape = ""  # type: str
 
         self._shader = None
 
-        self._origin_mesh = None
+        self._origin_mesh = None  # type: Optional[MeshData]
         self._origin_line_length = 20
         self._origin_line_width = 0.5
 
-        self._grid_mesh = None
+        self._grid_mesh = None   # type: Optional[MeshData]
         self._grid_shader = None
 
-        self._disallowed_areas = []
-        self._disallowed_areas_no_brim = []
-        self._disallowed_area_mesh = None
+        self._disallowed_areas = []  # type: List[Polygon]
+        self._disallowed_areas_no_brim = []  # type: List[Polygon]
+        self._disallowed_area_mesh = None  # type: Optional[MeshData]
+        self._disallowed_area_size = 0.
 
-        self._error_areas = []
-        self._error_mesh = None
+        self._error_areas = []  # type: List[Polygon]
+        self._error_mesh = None  # type: Optional[MeshData]
 
         self.setCalculateBoundingBox(False)
-        self._volume_aabb = None
+        self._volume_aabb = None  # type: Optional[AxisAlignedBox]
 
         self._raft_thickness = 0.0
         self._extra_z_clearance = 0.0
-        self._adhesion_type = None
+        self._adhesion_type = None  # type: Any
         self._platform = Platform(self)
 
         self._build_volume_message = Message(catalog.i18nc("@info:status",
@@ -82,7 +90,7 @@ class BuildVolume(SceneNode):
             " \"Print Sequence\" setting to prevent the gantry from colliding"
             " with printed models."), title = catalog.i18nc("@info:title", "Build Volume"))
 
-        self._global_container_stack = None
+        self._global_container_stack = None  # type: Optional[GlobalStack]
 
         self._stack_change_timer = QTimer()
         self._stack_change_timer.setInterval(100)
@@ -100,7 +108,7 @@ class BuildVolume(SceneNode):
         self._application.getController().getScene().sceneChanged.connect(self._onSceneChanged)
 
         #Objects loaded at the moment. We are connected to the property changed events of these objects.
-        self._scene_objects = set()
+        self._scene_objects = set()  # type: Set[SceneNode]
 
         self._scene_change_timer = QTimer()
         self._scene_change_timer.setInterval(100)
@@ -124,8 +132,8 @@ class BuildVolume(SceneNode):
         # Enable and disable extruder
         self._machine_manager.extruderChanged.connect(self.updateNodeBoundaryCheck)
 
-        # list of settings which were updated
-        self._changed_settings_since_last_rebuild = []
+        # List of settings which were updated
+        self._changed_settings_since_last_rebuild = []  # type: List[str]
 
     def _onSceneChanged(self, source):
         if self._global_container_stack:
@@ -165,16 +173,13 @@ class BuildVolume(SceneNode):
             active_extruder_changed.connect(self._updateDisallowedAreasAndRebuild)
 
     def setWidth(self, width: float) -> None:
-        if width is not None:
-            self._width = width
+        self._width = width
 
     def setHeight(self, height: float) -> None:
-        if height is not None:
-            self._height = height
+        self._height = height
 
     def setDepth(self, depth: float) -> None:
-        if depth is not None:
-            self._depth = depth
+        self._depth = depth
 
     def setShape(self, shape: str) -> None:
         if shape:
@@ -222,13 +227,18 @@ class BuildVolume(SceneNode):
     ##  For every sliceable node, update node._outside_buildarea
     #
     def updateNodeBoundaryCheck(self):
+        if not self._global_container_stack:
+            return
+
         root = self._application.getController().getScene().getRoot()
-        nodes = list(BreadthFirstIterator(root))
-        group_nodes = []
+        nodes = cast(List[SceneNode], list(cast(Iterable, BreadthFirstIterator(root))))
+        group_nodes = []  # type: List[SceneNode]
 
         build_volume_bounding_box = self.getBoundingBox()
         if build_volume_bounding_box:
             # It's over 9000!
+            # We set this to a very low number, as we do allow models to intersect the build plate.
+            # This means the model gets cut off at the build plate.
             build_volume_bounding_box = build_volume_bounding_box.set(bottom=-9001)
         else:
             # No bounding box. This is triggered when running Cura from command line with a model for the first time
@@ -241,6 +251,9 @@ class BuildVolume(SceneNode):
                 group_nodes.append(node)  # Keep list of affected group_nodes
 
             if node.callDecoration("isSliceable") or node.callDecoration("isGroup"):
+                if not isinstance(node, CuraSceneNode):
+                    continue
+
                 if node.collidesWithBbox(build_volume_bounding_box):
                     node.setOutsideBuildArea(True)
                     continue
@@ -248,7 +261,11 @@ class BuildVolume(SceneNode):
                 if node.collidesWithArea(self.getDisallowedAreas()):
                     node.setOutsideBuildArea(True)
                     continue
-
+                # If the entire node is below the build plate, still mark it as outside.
+                node_bounding_box = node.getBoundingBox()
+                if node_bounding_box and node_bounding_box.top < 0:
+                    node.setOutsideBuildArea(True)
+                    continue
                 # Mark the node as outside build volume if the set extruder is disabled
                 extruder_position = node.callDecoration("getActiveExtruderPosition")
                 if extruder_position not in self._global_container_stack.extruders:
@@ -274,8 +291,8 @@ class BuildVolume(SceneNode):
                 child_node.setOutsideBuildArea(group_node.isOutsideBuildArea())
 
     ##  Update the outsideBuildArea of a single node, given bounds or current build volume
-    def checkBoundsAndUpdate(self, node: CuraSceneNode, bounds: Optional[AxisAlignedBox] = None):
-        if not isinstance(node, CuraSceneNode):
+    def checkBoundsAndUpdate(self, node: CuraSceneNode, bounds: Optional[AxisAlignedBox] = None) -> None:
+        if not isinstance(node, CuraSceneNode) or self._global_container_stack is None:
             return
 
         if bounds is None:
@@ -307,32 +324,43 @@ class BuildVolume(SceneNode):
 
             node.setOutsideBuildArea(False)
 
-    ##  Recalculates the build volume & disallowed areas.
-    def rebuild(self):
-        if not self._width or not self._height or not self._depth:
-            return
-
-        if not self._engine_ready:
-            return
-
-        if not self._volume_outline_color:
-            theme = self._application.getTheme()
-            self._volume_outline_color = Color(*theme.getColor("volume_outline").getRgb())
-            self._x_axis_color = Color(*theme.getColor("x_axis").getRgb())
-            self._y_axis_color = Color(*theme.getColor("y_axis").getRgb())
-            self._z_axis_color = Color(*theme.getColor("z_axis").getRgb())
-            self._disallowed_area_color = Color(*theme.getColor("disallowed_area").getRgb())
-            self._error_area_color = Color(*theme.getColor("error_area").getRgb())
+    def _buildGridMesh(self, min_w: float, max_w: float, min_h: float, max_h: float, min_d: float, max_d:float, z_fight_distance: float) -> MeshData:
+        mb = MeshBuilder()
+        if self._shape != "elliptic":
+            # Build plate grid mesh
+            mb.addQuad(
+                Vector(min_w, min_h - z_fight_distance, min_d),
+                Vector(max_w, min_h - z_fight_distance, min_d),
+                Vector(max_w, min_h - z_fight_distance, max_d),
+                Vector(min_w, min_h - z_fight_distance, max_d)
+            )
 
-        min_w = -self._width / 2
-        max_w = self._width / 2
-        min_h = 0.0
-        max_h = self._height
-        min_d = -self._depth / 2
-        max_d = self._depth / 2
+            for n in range(0, 6):
+                v = mb.getVertex(n)
+                mb.setVertexUVCoordinates(n, v[0], v[2])
+            return mb.build()
+        else:
+            aspect = 1.0
+            scale_matrix = Matrix()
+            if self._width != 0:
+                # Scale circular meshes by aspect ratio if width != height
+                aspect = self._depth / self._width
+                scale_matrix.compose(scale=Vector(1, 1, aspect))
+            mb.addVertex(0, min_h - z_fight_distance, 0)
+            mb.addArc(max_w, Vector.Unit_Y, center=Vector(0, min_h - z_fight_distance, 0))
+            sections = mb.getVertexCount() - 1  # Center point is not an arc section
+            indices = []
+            for n in range(0, sections - 1):
+                indices.append([0, n + 2, n + 1])
+            mb.addIndices(numpy.asarray(indices, dtype=numpy.int32))
+            mb.calculateNormals()
 
-        z_fight_distance = 0.2 # Distance between buildplate and disallowed area meshes to prevent z-fighting
+            for n in range(0, mb.getVertexCount()):
+                v = mb.getVertex(n)
+                mb.setVertexUVCoordinates(n, v[0], v[2] * aspect)
+            return mb.build().getTransformed(scale_matrix)
 
+    def _buildMesh(self, min_w: float, max_w: float, min_h: float, max_h: float, min_d: float, max_d:float, z_fight_distance: float) -> MeshData:
         if self._shape != "elliptic":
             # Outline 'cube' of the build volume
             mb = MeshBuilder()
@@ -351,25 +379,10 @@ class BuildVolume(SceneNode):
             mb.addLine(Vector(min_w, max_h, min_d), Vector(min_w, max_h, max_d), color = self._volume_outline_color)
             mb.addLine(Vector(max_w, max_h, min_d), Vector(max_w, max_h, max_d), color = self._volume_outline_color)
 
-            self.setMeshData(mb.build())
-
-            # Build plate grid mesh
-            mb = MeshBuilder()
-            mb.addQuad(
-                Vector(min_w, min_h - z_fight_distance, min_d),
-                Vector(max_w, min_h - z_fight_distance, min_d),
-                Vector(max_w, min_h - z_fight_distance, max_d),
-                Vector(min_w, min_h - z_fight_distance, max_d)
-            )
-
-            for n in range(0, 6):
-                v = mb.getVertex(n)
-                mb.setVertexUVCoordinates(n, v[0], v[2])
-            self._grid_mesh = mb.build()
+            return mb.build()
 
         else:
             # Bottom and top 'ellipse' of the build volume
-            aspect = 1.0
             scale_matrix = Matrix()
             if self._width != 0:
                 # Scale circular meshes by aspect ratio if width != height
@@ -378,101 +391,133 @@ class BuildVolume(SceneNode):
             mb = MeshBuilder()
             mb.addArc(max_w, Vector.Unit_Y, center = (0, min_h - z_fight_distance, 0), color = self._volume_outline_color)
             mb.addArc(max_w, Vector.Unit_Y, center = (0, max_h, 0),  color = self._volume_outline_color)
-            self.setMeshData(mb.build().getTransformed(scale_matrix))
-
-            # Build plate grid mesh
-            mb = MeshBuilder()
-            mb.addVertex(0, min_h - z_fight_distance, 0)
-            mb.addArc(max_w, Vector.Unit_Y, center = Vector(0, min_h - z_fight_distance, 0))
-            sections = mb.getVertexCount() - 1 # Center point is not an arc section
-            indices = []
-            for n in range(0, sections - 1):
-                indices.append([0, n + 2, n + 1])
-            mb.addIndices(numpy.asarray(indices, dtype = numpy.int32))
-            mb.calculateNormals()
-
-            for n in range(0, mb.getVertexCount()):
-                v = mb.getVertex(n)
-                mb.setVertexUVCoordinates(n, v[0], v[2] * aspect)
-            self._grid_mesh = mb.build().getTransformed(scale_matrix)
-
-        # Indication of the machine origin
-        if self._global_container_stack.getProperty("machine_center_is_zero", "value"):
-            origin = (Vector(min_w, min_h, min_d) + Vector(max_w, min_h, max_d)) / 2
-        else:
-            origin = Vector(min_w, min_h, max_d)
+            return mb.build().getTransformed(scale_matrix)
 
+    def _buildOriginMesh(self, origin: Vector) -> MeshData:
         mb = MeshBuilder()
         mb.addCube(
-            width = self._origin_line_length,
-            height = self._origin_line_width,
-            depth = self._origin_line_width,
-            center = origin + Vector(self._origin_line_length / 2, 0, 0),
-            color = self._x_axis_color
+            width=self._origin_line_length,
+            height=self._origin_line_width,
+            depth=self._origin_line_width,
+            center=origin + Vector(self._origin_line_length / 2, 0, 0),
+            color=self._x_axis_color
         )
         mb.addCube(
-            width = self._origin_line_width,
-            height = self._origin_line_length,
-            depth = self._origin_line_width,
-            center = origin + Vector(0, self._origin_line_length / 2, 0),
-            color = self._y_axis_color
+            width=self._origin_line_width,
+            height=self._origin_line_length,
+            depth=self._origin_line_width,
+            center=origin + Vector(0, self._origin_line_length / 2, 0),
+            color=self._y_axis_color
         )
         mb.addCube(
-            width = self._origin_line_width,
-            height = self._origin_line_width,
-            depth = self._origin_line_length,
-            center = origin - Vector(0, 0, self._origin_line_length / 2),
-            color = self._z_axis_color
+            width=self._origin_line_width,
+            height=self._origin_line_width,
+            depth=self._origin_line_length,
+            center=origin - Vector(0, 0, self._origin_line_length / 2),
+            color=self._z_axis_color
         )
-        self._origin_mesh = mb.build()
+        return mb.build()
 
-        disallowed_area_height = 0.1
-        disallowed_area_size = 0
-        if self._disallowed_areas:
-            mb = MeshBuilder()
-            color = self._disallowed_area_color
-            for polygon in self._disallowed_areas:
-                points = polygon.getPoints()
-                if len(points) == 0:
-                    continue
+    def _updateColors(self):
+        theme = self._application.getTheme()
+        if theme is None:
+            return
+        self._volume_outline_color = Color(*theme.getColor("volume_outline").getRgb())
+        self._x_axis_color = Color(*theme.getColor("x_axis").getRgb())
+        self._y_axis_color = Color(*theme.getColor("y_axis").getRgb())
+        self._z_axis_color = Color(*theme.getColor("z_axis").getRgb())
+        self._disallowed_area_color = Color(*theme.getColor("disallowed_area").getRgb())
+        self._error_area_color = Color(*theme.getColor("error_area").getRgb())
+
+    def _buildErrorMesh(self, min_w: float, max_w: float, min_h: float, max_h: float, min_d: float, max_d: float, disallowed_area_height: float) -> Optional[MeshData]:
+        if not self._error_areas:
+            return None
+        mb = MeshBuilder()
+        for error_area in self._error_areas:
+            color = self._error_area_color
+            points = error_area.getPoints()
+            first = Vector(self._clamp(points[0][0], min_w, max_w), disallowed_area_height,
+                           self._clamp(points[0][1], min_d, max_d))
+            previous_point = Vector(self._clamp(points[0][0], min_w, max_w), disallowed_area_height,
+                                    self._clamp(points[0][1], min_d, max_d))
+            for point in points:
+                new_point = Vector(self._clamp(point[0], min_w, max_w), disallowed_area_height,
+                                   self._clamp(point[1], min_d, max_d))
+                mb.addFace(first, previous_point, new_point, color=color)
+                previous_point = new_point
+        return mb.build()
+
+    def _buildDisallowedAreaMesh(self, min_w: float, max_w: float, min_h: float, max_h: float, min_d: float, max_d: float, disallowed_area_height: float) -> Optional[MeshData]:
+        if not self._disallowed_areas:
+            return None
 
-                first = Vector(self._clamp(points[0][0], min_w, max_w), disallowed_area_height, self._clamp(points[0][1], min_d, max_d))
-                previous_point = Vector(self._clamp(points[0][0], min_w, max_w), disallowed_area_height, self._clamp(points[0][1], min_d, max_d))
-                for point in points:
-                    new_point = Vector(self._clamp(point[0], min_w, max_w), disallowed_area_height, self._clamp(point[1], min_d, max_d))
-                    mb.addFace(first, previous_point, new_point, color = color)
-                    previous_point = new_point
-
-                # Find the largest disallowed area to exclude it from the maximum scale bounds.
-                # This is a very nasty hack. This pretty much only works for UM machines.
-                # This disallowed area_size needs a -lot- of rework at some point in the future: TODO
-                if numpy.min(points[:, 1]) >= 0: # This filters out all areas that have points to the left of the centre. This is done to filter the skirt area.
-                    size = abs(numpy.max(points[:, 1]) - numpy.min(points[:, 1]))
-                else:
-                    size = 0
-                disallowed_area_size = max(size, disallowed_area_size)
+        mb = MeshBuilder()
+        color = self._disallowed_area_color
+        for polygon in self._disallowed_areas:
+            points = polygon.getPoints()
+            if len(points) == 0:
+                continue
 
-            self._disallowed_area_mesh = mb.build()
-        else:
-            self._disallowed_area_mesh = None
+            first = Vector(self._clamp(points[0][0], min_w, max_w), disallowed_area_height,
+                           self._clamp(points[0][1], min_d, max_d))
+            previous_point = Vector(self._clamp(points[0][0], min_w, max_w), disallowed_area_height,
+                                    self._clamp(points[0][1], min_d, max_d))
+            for point in points:
+                new_point = Vector(self._clamp(point[0], min_w, max_w), disallowed_area_height,
+                                   self._clamp(point[1], min_d, max_d))
+                mb.addFace(first, previous_point, new_point, color=color)
+                previous_point = new_point
+
+            # Find the largest disallowed area to exclude it from the maximum scale bounds.
+            # This is a very nasty hack. This pretty much only works for UM machines.
+            # This disallowed area_size needs a -lot- of rework at some point in the future: TODO
+            if numpy.min(points[:,
+                         1]) >= 0:  # This filters out all areas that have points to the left of the centre. This is done to filter the skirt area.
+                size = abs(numpy.max(points[:, 1]) - numpy.min(points[:, 1]))
+            else:
+                size = 0
+            self._disallowed_area_size = max(size, self._disallowed_area_size)
+        return mb.build()
 
-        if self._error_areas:
-            mb = MeshBuilder()
-            for error_area in self._error_areas:
-                color = self._error_area_color
-                points = error_area.getPoints()
-                first = Vector(self._clamp(points[0][0], min_w, max_w), disallowed_area_height,
-                               self._clamp(points[0][1], min_d, max_d))
-                previous_point = Vector(self._clamp(points[0][0], min_w, max_w), disallowed_area_height,
-                                        self._clamp(points[0][1], min_d, max_d))
-                for point in points:
-                    new_point = Vector(self._clamp(point[0], min_w, max_w), disallowed_area_height,
-                                       self._clamp(point[1], min_d, max_d))
-                    mb.addFace(first, previous_point, new_point, color=color)
-                    previous_point = new_point
-            self._error_mesh = mb.build()
+    ##  Recalculates the build volume & disallowed areas.
+    def rebuild(self) -> None:
+        if not self._width or not self._height or not self._depth:
+            return
+
+        if not self._engine_ready:
+            return
+
+        if not self._global_container_stack:
+            return
+
+        if not self._volume_outline_color:
+            self._updateColors()
+
+        min_w = -self._width / 2
+        max_w = self._width / 2
+        min_h = 0.0
+        max_h = self._height
+        min_d = -self._depth / 2
+        max_d = self._depth / 2
+
+        z_fight_distance = 0.2  # Distance between buildplate and disallowed area meshes to prevent z-fighting
+
+        self._grid_mesh = self._buildGridMesh(min_w, max_w, min_h, max_h, min_d, max_d, z_fight_distance)
+        self.setMeshData(self._buildMesh(min_w, max_w, min_h, max_h, min_d, max_d, z_fight_distance))
+
+        # Indication of the machine origin
+        if self._global_container_stack.getProperty("machine_center_is_zero", "value"):
+            origin = (Vector(min_w, min_h, min_d) + Vector(max_w, min_h, max_d)) / 2
         else:
-            self._error_mesh = None
+            origin = Vector(min_w, min_h, max_d)
+
+        self._origin_mesh = self._buildOriginMesh(origin)
+
+        disallowed_area_height = 0.1
+        self._disallowed_area_size = 0.
+        self._disallowed_area_mesh = self._buildDisallowedAreaMesh(min_w, max_w, min_h, max_h, min_d, max_d, disallowed_area_height)
+
+        self._error_mesh = self._buildErrorMesh(min_w, max_w, min_h, max_h, min_d, max_d, disallowed_area_height)
 
         self._volume_aabb = AxisAlignedBox(
             minimum = Vector(min_w, min_h - 1.0, min_d),
@@ -484,21 +529,24 @@ class BuildVolume(SceneNode):
         # This is probably wrong in all other cases. TODO!
         # The +1 and -1 is added as there is always a bit of extra room required to work properly.
         scale_to_max_bounds = AxisAlignedBox(
-            minimum = Vector(min_w + bed_adhesion_size + 1, min_h, min_d + disallowed_area_size - bed_adhesion_size + 1),
-            maximum = Vector(max_w - bed_adhesion_size - 1, max_h - self._raft_thickness - self._extra_z_clearance, max_d - disallowed_area_size + bed_adhesion_size - 1)
+            minimum = Vector(min_w + bed_adhesion_size + 1, min_h, min_d + self._disallowed_area_size - bed_adhesion_size + 1),
+            maximum = Vector(max_w - bed_adhesion_size - 1, max_h - self._raft_thickness - self._extra_z_clearance, max_d - self._disallowed_area_size + bed_adhesion_size - 1)
         )
 
-        self._application.getController().getScene()._maximum_bounds = scale_to_max_bounds
+        self._application.getController().getScene()._maximum_bounds = scale_to_max_bounds  # type: ignore
 
         self.updateNodeBoundaryCheck()
 
-    def getBoundingBox(self) -> AxisAlignedBox:
+    def getBoundingBox(self):
         return self._volume_aabb
 
     def getRaftThickness(self) -> float:
         return self._raft_thickness
 
-    def _updateRaftThickness(self):
+    def _updateRaftThickness(self) -> None:
+        if not self._global_container_stack:
+            return
+
         old_raft_thickness = self._raft_thickness
         if self._global_container_stack.extruders:
             # This might be called before the extruder stacks have initialised, in which case getting the adhesion_type fails
@@ -509,7 +557,7 @@ class BuildVolume(SceneNode):
                 self._global_container_stack.getProperty("raft_base_thickness", "value") +
                 self._global_container_stack.getProperty("raft_interface_thickness", "value") +
                 self._global_container_stack.getProperty("raft_surface_layers", "value") *
-                    self._global_container_stack.getProperty("raft_surface_thickness", "value") +
+                self._global_container_stack.getProperty("raft_surface_thickness", "value") +
                 self._global_container_stack.getProperty("raft_airgap", "value") -
                 self._global_container_stack.getProperty("layer_0_z_overlap", "value"))
 
@@ -518,28 +566,23 @@ class BuildVolume(SceneNode):
             self.setPosition(Vector(0, -self._raft_thickness, 0), SceneNode.TransformSpace.World)
             self.raftThicknessChanged.emit()
 
-    def _updateExtraZClearance(self) -> None:
+    def _calculateExtraZClearance(self, extruders: List["ContainerStack"]) -> float:
+        if not self._global_container_stack:
+            return 0
+        
         extra_z = 0.0
-        extruders = ExtruderManager.getInstance().getUsedExtruderStacks()
-        use_extruders = False
         for extruder in extruders:
             if extruder.getProperty("retraction_hop_enabled", "value"):
                 retraction_hop = extruder.getProperty("retraction_hop", "value")
                 if extra_z is None or retraction_hop > extra_z:
                     extra_z = retraction_hop
-            use_extruders = True
-        if not use_extruders:
-            # If no extruders, take global value.
-            if self._global_container_stack.getProperty("retraction_hop_enabled", "value"):
-                extra_z = self._global_container_stack.getProperty("retraction_hop", "value")
-        if extra_z != self._extra_z_clearance:
-            self._extra_z_clearance = extra_z
+        return extra_z
 
     def _onStackChanged(self):
         self._stack_change_timer.start()
 
     ##  Update the build volume visualization
-    def _onStackChangeTimerFinished(self):
+    def _onStackChangeTimerFinished(self) -> None:
         if self._global_container_stack:
             self._global_container_stack.propertyChanged.disconnect(self._onSettingPropertyChanged)
             extruders = ExtruderManager.getInstance().getActiveExtruderStacks()
@@ -570,7 +613,7 @@ class BuildVolume(SceneNode):
 
             self._updateDisallowedAreas()
             self._updateRaftThickness()
-            self._updateExtraZClearance()
+            self._extra_z_clearance = self._calculateExtraZClearance(ExtruderManager.getInstance().getUsedExtruderStacks())
 
             if self._engine_ready:
                 self.rebuild()
@@ -579,20 +622,23 @@ class BuildVolume(SceneNode):
             if camera:
                 diagonal = self.getDiagonalSize()
                 if diagonal > 1:
-                    camera.setZoomRange(min = 0.1, max = diagonal * 5) #You can zoom out up to 5 times the diagonal. This gives some space around the volume.
+                    # You can zoom out up to 5 times the diagonal. This gives some space around the volume.
+                    camera.setZoomRange(min = 0.1, max = diagonal * 5)  # type: ignore
 
-    def _onEngineCreated(self):
+    def _onEngineCreated(self) -> None:
         self._engine_ready = True
         self.rebuild()
 
-    def _onSettingChangeTimerFinished(self):
+    def _onSettingChangeTimerFinished(self) -> None:
+        if not self._global_container_stack:
+            return
+
         rebuild_me = False
         update_disallowed_areas = False
         update_raft_thickness = False
         update_extra_z_clearance = True
 
         for setting_key in self._changed_settings_since_last_rebuild:
-
             if setting_key == "print_sequence":
                 machine_height = self._global_container_stack.getProperty("machine_height", "value")
                 if self._application.getGlobalContainerStack().getProperty("print_sequence", "value") == "one_at_a_time" and len(self._scene_objects) > 1:
@@ -605,33 +651,26 @@ class BuildVolume(SceneNode):
                     self._height = self._global_container_stack.getProperty("machine_height", "value")
                     self._build_volume_message.hide()
                 update_disallowed_areas = True
-                rebuild_me = True
 
             # sometimes the machine size or shape settings are adjusted on the active machine, we should reflect this
             if setting_key in self._machine_settings:
-                self._height = self._global_container_stack.getProperty("machine_height", "value")
-                self._width = self._global_container_stack.getProperty("machine_width", "value")
-                self._depth = self._global_container_stack.getProperty("machine_depth", "value")
-                self._shape = self._global_container_stack.getProperty("machine_shape", "value")
+                self._updateMachineSizeProperties()
                 update_extra_z_clearance = True
                 update_disallowed_areas = True
-                rebuild_me = True
 
-            if setting_key in self._skirt_settings + self._prime_settings + self._tower_settings + self._ooze_shield_settings + self._distance_settings + self._extruder_settings:
+            if setting_key in self._disallowed_area_settings:
                 update_disallowed_areas = True
-                rebuild_me = True
 
             if setting_key in self._raft_settings:
                 update_raft_thickness = True
-                rebuild_me = True
 
             if setting_key in self._extra_z_settings:
                 update_extra_z_clearance = True
-                rebuild_me = True
 
             if setting_key in self._limit_to_extruder_settings:
                 update_disallowed_areas = True
-                rebuild_me = True
+
+            rebuild_me = update_extra_z_clearance or update_disallowed_areas or update_raft_thickness
 
         # We only want to update all of them once.
         if update_disallowed_areas:
@@ -641,7 +680,7 @@ class BuildVolume(SceneNode):
             self._updateRaftThickness()
 
         if update_extra_z_clearance:
-            self._updateExtraZClearance()
+            self._extra_z_clearance = self._calculateExtraZClearance(ExtruderManager.getInstance().getUsedExtruderStacks())
 
         if rebuild_me:
             self.rebuild()
@@ -649,7 +688,7 @@ class BuildVolume(SceneNode):
         # We just did a rebuild, reset the list.
         self._changed_settings_since_last_rebuild = []
 
-    def _onSettingPropertyChanged(self, setting_key: str, property_name: str):
+    def _onSettingPropertyChanged(self, setting_key: str, property_name: str) -> None:
         if property_name != "value":
             return
 
@@ -660,6 +699,14 @@ class BuildVolume(SceneNode):
     def hasErrors(self) -> bool:
         return self._has_errors
 
+    def _updateMachineSizeProperties(self) -> None:
+        if not self._global_container_stack:
+            return
+        self._height = self._global_container_stack.getProperty("machine_height", "value")
+        self._width = self._global_container_stack.getProperty("machine_width", "value")
+        self._depth = self._global_container_stack.getProperty("machine_depth", "value")
+        self._shape = self._global_container_stack.getProperty("machine_shape", "value")
+
     ##  Calls _updateDisallowedAreas and makes sure the changes appear in the
     #   scene.
     #
@@ -671,10 +718,10 @@ class BuildVolume(SceneNode):
     def _updateDisallowedAreasAndRebuild(self):
         self._updateDisallowedAreas()
         self._updateRaftThickness()
-        self._updateExtraZClearance()
+        self._extra_z_clearance = self._calculateExtraZClearance(ExtruderManager.getInstance().getUsedExtruderStacks())
         self.rebuild()
 
-    def _updateDisallowedAreas(self):
+    def _updateDisallowedAreas(self) -> None:
         if not self._global_container_stack:
             return
 
@@ -736,7 +783,7 @@ class BuildVolume(SceneNode):
         # Add prime tower location as disallowed area.
         if len(used_extruders) > 1: #No prime tower in single-extrusion.
 
-            if len([x for x in used_extruders if x.isEnabled == True]) > 1: #No prime tower if only one extruder is enabled
+            if len([x for x in used_extruders if x.isEnabled]) > 1: #No prime tower if only one extruder is enabled
                 prime_tower_collision = False
                 prime_tower_areas = self._computeDisallowedAreasPrinted(used_extruders)
                 for extruder_id in prime_tower_areas:
@@ -800,17 +847,10 @@ class BuildVolume(SceneNode):
                 prime_tower_x -= brim_size
                 prime_tower_y += brim_size
 
-            if self._global_container_stack.getProperty("prime_tower_circular", "value"):
-                radius = prime_tower_size / 2
-                prime_tower_area = Polygon.approximatedCircle(radius)
-                prime_tower_area = prime_tower_area.translate(prime_tower_x - radius, prime_tower_y - radius)
-            else:
-                prime_tower_area = Polygon([
-                    [prime_tower_x - prime_tower_size, prime_tower_y - prime_tower_size],
-                    [prime_tower_x, prime_tower_y - prime_tower_size],
-                    [prime_tower_x, prime_tower_y],
-                    [prime_tower_x - prime_tower_size, prime_tower_y],
-                ])
+            radius = prime_tower_size / 2
+            prime_tower_area = Polygon.approximatedCircle(radius)
+            prime_tower_area = prime_tower_area.translate(prime_tower_x - radius, prime_tower_y - radius)
+
             prime_tower_area = prime_tower_area.getMinkowskiHull(Polygon.approximatedCircle(0))
             for extruder in used_extruders:
                 result[extruder.getId()].append(prime_tower_area) #The prime tower location is the same for each extruder, regardless of offset.
@@ -828,9 +868,10 @@ class BuildVolume(SceneNode):
     #   \param used_extruders The extruder stacks to generate disallowed areas
     #   for.
     #   \return A dictionary with for each used extruder ID the prime areas.
-    def _computeDisallowedAreasPrimeBlob(self, border_size, used_extruders):
-        result = {}
-
+    def _computeDisallowedAreasPrimeBlob(self, border_size: float, used_extruders: List["ExtruderStack"]) -> Dict[str, List[Polygon]]:
+        result = {}  # type: Dict[str, List[Polygon]]
+        if not self._global_container_stack:
+            return result
         machine_width = self._global_container_stack.getProperty("machine_width", "value")
         machine_depth = self._global_container_stack.getProperty("machine_depth", "value")
         for extruder in used_extruders:
@@ -838,13 +879,13 @@ class BuildVolume(SceneNode):
             prime_x = extruder.getProperty("extruder_prime_pos_x", "value")
             prime_y = -extruder.getProperty("extruder_prime_pos_y", "value")
 
-            #Ignore extruder prime position if it is not set or if blob is disabled
+            # Ignore extruder prime position if it is not set or if blob is disabled
             if (prime_x == 0 and prime_y == 0) or not prime_blob_enabled:
                 result[extruder.getId()] = []
                 continue
 
             if not self._global_container_stack.getProperty("machine_center_is_zero", "value"):
-                prime_x = prime_x - machine_width / 2 #Offset by half machine_width and _depth to put the origin in the front-left.
+                prime_x = prime_x - machine_width / 2  # Offset by half machine_width and _depth to put the origin in the front-left.
                 prime_y = prime_y + machine_depth / 2
 
             prime_polygon = Polygon.approximatedCircle(PRIME_CLEARANCE)
@@ -1000,63 +1041,48 @@ class BuildVolume(SceneNode):
     #   stack.
     #
     #   \return A sequence of setting values, one for each extruder.
-    def _getSettingFromAllExtruders(self, setting_key):
+    def _getSettingFromAllExtruders(self, setting_key: str) -> List[Any]:
         all_values = ExtruderManager.getInstance().getAllExtruderSettings(setting_key, "value")
         all_types = ExtruderManager.getInstance().getAllExtruderSettings(setting_key, "type")
-        for i in range(len(all_values)):
-            if not all_values[i] and (all_types[i] == "int" or all_types[i] == "float"):
+        for i, (setting_value, setting_type) in enumerate(zip(all_values, all_types)):
+            if not setting_value and (setting_type == "int" or setting_type == "float"):
                 all_values[i] = 0
         return all_values
 
-    ##  Calculate the disallowed radius around the edge.
-    #
-    #   This disallowed radius is to allow for space around the models that is
-    #   not part of the collision radius, such as bed adhesion (skirt/brim/raft)
-    #   and travel avoid distance.
-    def getEdgeDisallowedSize(self):
-        if not self._global_container_stack or not self._global_container_stack.extruders:
-            return 0
+    def _calculateBedAdhesionSize(self, used_extruders):
+        if self._global_container_stack is None:
+            return
 
         container_stack = self._global_container_stack
-        used_extruders = ExtruderManager.getInstance().getUsedExtruderStacks()
-
-        # If we are printing one at a time, we need to add the bed adhesion size to the disallowed areas of the objects
-        if container_stack.getProperty("print_sequence", "value") == "one_at_a_time":
-            return 0.1  # Return a very small value, so we do draw disallowed area's near the edges.
-
         adhesion_type = container_stack.getProperty("adhesion_type", "value")
         skirt_brim_line_width = self._global_container_stack.getProperty("skirt_brim_line_width", "value")
         initial_layer_line_width_factor = self._global_container_stack.getProperty("initial_layer_line_width_factor", "value")
-        if adhesion_type == "skirt":
-            skirt_distance = self._global_container_stack.getProperty("skirt_gap", "value")
-            skirt_line_count = self._global_container_stack.getProperty("skirt_line_count", "value")
-
-            bed_adhesion_size = skirt_distance + (skirt_brim_line_width * skirt_line_count) * initial_layer_line_width_factor / 100.0
+        # Use brim width if brim is enabled OR the prime tower has a brim.
+        if adhesion_type == "brim" or (self._global_container_stack.getProperty("prime_tower_brim_enable", "value") and adhesion_type != "raft"):
+            brim_line_count = self._global_container_stack.getProperty("brim_line_count", "value")
+            bed_adhesion_size = skirt_brim_line_width * brim_line_count * initial_layer_line_width_factor / 100.0
 
             for extruder_stack in used_extruders:
                 bed_adhesion_size += extruder_stack.getProperty("skirt_brim_line_width", "value") * extruder_stack.getProperty("initial_layer_line_width_factor", "value") / 100.0
 
-            # We don't create an additional line for the extruder we're printing the skirt with.
+            # We don't create an additional line for the extruder we're printing the brim with.
             bed_adhesion_size -= skirt_brim_line_width * initial_layer_line_width_factor / 100.0
+        elif adhesion_type == "skirt":  # No brim? Also not on prime tower? Then use whatever the adhesion type is saying: Skirt, raft or none.
+            skirt_distance = self._global_container_stack.getProperty("skirt_gap", "value")
+            skirt_line_count = self._global_container_stack.getProperty("skirt_line_count", "value")
 
-        elif (adhesion_type == "brim" or
-                (self._global_container_stack.getProperty("prime_tower_brim_enable", "value") and
-                    self._global_container_stack.getProperty("adhesion_type", "value") != "raft")):
-            brim_line_count = self._global_container_stack.getProperty("brim_line_count", "value")
-            bed_adhesion_size = skirt_brim_line_width * brim_line_count * initial_layer_line_width_factor / 100.0
+            bed_adhesion_size = skirt_distance + (
+                        skirt_brim_line_width * skirt_line_count) * initial_layer_line_width_factor / 100.0
 
             for extruder_stack in used_extruders:
                 bed_adhesion_size += extruder_stack.getProperty("skirt_brim_line_width", "value") * extruder_stack.getProperty("initial_layer_line_width_factor", "value") / 100.0
 
-            # We don't create an additional line for the extruder we're printing the brim with.
+            # We don't create an additional line for the extruder we're printing the skirt with.
             bed_adhesion_size -= skirt_brim_line_width * initial_layer_line_width_factor / 100.0
-
         elif adhesion_type == "raft":
             bed_adhesion_size = self._global_container_stack.getProperty("raft_margin", "value")
-
         elif adhesion_type == "none":
             bed_adhesion_size = 0
-
         else:
             raise Exception("Unknown bed adhesion type. Did you forget to update the build volume calculations for your new bed adhesion type?")
 
@@ -1065,26 +1091,56 @@ class BuildVolume(SceneNode):
             self._global_container_stack.getProperty("machine_depth", "value")
         )
         bed_adhesion_size = min(bed_adhesion_size, max_length_available)
+        return bed_adhesion_size
 
+    def _calculateFarthestShieldDistance(self, container_stack):
+        farthest_shield_distance = 0
+        if container_stack.getProperty("draft_shield_enabled", "value"):
+            farthest_shield_distance = max(farthest_shield_distance, container_stack.getProperty("draft_shield_dist", "value"))
+        if container_stack.getProperty("ooze_shield_enabled", "value"):
+            farthest_shield_distance = max(farthest_shield_distance,container_stack.getProperty("ooze_shield_dist", "value"))
+        return farthest_shield_distance
+
+    def _calculateSupportExpansion(self, container_stack):
         support_expansion = 0
         support_enabled = self._global_container_stack.getProperty("support_enable", "value")
         support_offset = self._global_container_stack.getProperty("support_offset", "value")
         if support_enabled and support_offset:
             support_expansion += support_offset
+        return support_expansion
 
-        farthest_shield_distance = 0
-        if container_stack.getProperty("draft_shield_enabled", "value"):
-            farthest_shield_distance = max(farthest_shield_distance, container_stack.getProperty("draft_shield_dist", "value"))
-        if container_stack.getProperty("ooze_shield_enabled", "value"):
-            farthest_shield_distance = max(farthest_shield_distance, container_stack.getProperty("ooze_shield_dist", "value"))
-
+    def _calculateMoveFromWallRadius(self, used_extruders):
         move_from_wall_radius = 0  # Moves that start from outer wall.
-        move_from_wall_radius = max(move_from_wall_radius, max(self._getSettingFromAllExtruders("infill_wipe_dist")))
-        avoid_enabled_per_extruder = [stack.getProperty("travel_avoid_other_parts","value") for stack in used_extruders]
+        all_values = [move_from_wall_radius]
+        all_values.extend(self._getSettingFromAllExtruders("infill_wipe_dist"))
+        move_from_wall_radius = max(all_values)
+        avoid_enabled_per_extruder = [stack.getProperty("travel_avoid_other_parts", "value") for stack in used_extruders]
         travel_avoid_distance_per_extruder = [stack.getProperty("travel_avoid_distance", "value") for stack in used_extruders]
-        for avoid_other_parts_enabled, avoid_distance in zip(avoid_enabled_per_extruder, travel_avoid_distance_per_extruder): #For each extruder (or just global).
+        for avoid_other_parts_enabled, avoid_distance in zip(avoid_enabled_per_extruder, travel_avoid_distance_per_extruder):  # For each extruder (or just global).
             if avoid_other_parts_enabled:
                 move_from_wall_radius = max(move_from_wall_radius, avoid_distance)
+        return move_from_wall_radius
+
+    ##  Calculate the disallowed radius around the edge.
+    #
+    #   This disallowed radius is to allow for space around the models that is
+    #   not part of the collision radius, such as bed adhesion (skirt/brim/raft)
+    #   and travel avoid distance.
+    def getEdgeDisallowedSize(self):
+        if not self._global_container_stack or not self._global_container_stack.extruders:
+            return 0
+
+        container_stack = self._global_container_stack
+        used_extruders = ExtruderManager.getInstance().getUsedExtruderStacks()
+
+        # If we are printing one at a time, we need to add the bed adhesion size to the disallowed areas of the objects
+        if container_stack.getProperty("print_sequence", "value") == "one_at_a_time":
+            return 0.1  # Return a very small value, so we do draw disallowed area's near the edges.
+
+        bed_adhesion_size = self._calculateBedAdhesionSize(used_extruders)
+        support_expansion = self._calculateSupportExpansion(self._global_container_stack)
+        farthest_shield_distance = self._calculateFarthestShieldDistance(self._global_container_stack)
+        move_from_wall_radius = self._calculateMoveFromWallRadius(used_extruders)
 
         # Now combine our different pieces of data to get the final border size.
         # Support expansion is added to the bed adhesion, since the bed adhesion goes around support.
@@ -1100,8 +1156,9 @@ class BuildVolume(SceneNode):
     _raft_settings = ["adhesion_type", "raft_base_thickness", "raft_interface_thickness", "raft_surface_layers", "raft_surface_thickness", "raft_airgap", "layer_0_z_overlap"]
     _extra_z_settings = ["retraction_hop_enabled", "retraction_hop"]
     _prime_settings = ["extruder_prime_pos_x", "extruder_prime_pos_y", "extruder_prime_pos_z", "prime_blob_enable"]
-    _tower_settings = ["prime_tower_enable", "prime_tower_circular", "prime_tower_size", "prime_tower_position_x", "prime_tower_position_y", "prime_tower_brim_enable"]
+    _tower_settings = ["prime_tower_enable", "prime_tower_size", "prime_tower_position_x", "prime_tower_position_y", "prime_tower_brim_enable"]
     _ooze_shield_settings = ["ooze_shield_enabled", "ooze_shield_dist"]
     _distance_settings = ["infill_wipe_dist", "travel_avoid_distance", "support_offset", "support_enable", "travel_avoid_other_parts", "travel_avoid_supports"]
     _extruder_settings = ["support_enable", "support_bottom_enable", "support_roof_enable", "support_infill_extruder_nr", "support_extruder_nr_layer_0", "support_bottom_extruder_nr", "support_roof_extruder_nr", "brim_line_count", "adhesion_extruder_nr", "adhesion_type"] #Settings that can affect which extruders are used.
     _limit_to_extruder_settings = ["wall_extruder_nr", "wall_0_extruder_nr", "wall_x_extruder_nr", "top_bottom_extruder_nr", "infill_extruder_nr", "support_infill_extruder_nr", "support_extruder_nr_layer_0", "support_bottom_extruder_nr", "support_roof_extruder_nr", "adhesion_extruder_nr"]
+    _disallowed_area_settings = _skirt_settings + _prime_settings + _tower_settings + _ooze_shield_settings + _distance_settings + _extruder_settings

+ 1 - 2
cura/CuraApplication.py

@@ -144,7 +144,7 @@ class CuraApplication(QtApplication):
     # SettingVersion represents the set of settings available in the machine/extruder definitions.
     # You need to make sure that this version number needs to be increased if there is any non-backwards-compatible
     # changes of the settings.
-    SettingVersion = 7
+    SettingVersion = 8
 
     Created = False
 
@@ -839,7 +839,6 @@ class CuraApplication(QtApplication):
         if diagonal < 1: #No printer added yet. Set a default camera distance for normal-sized printers.
             diagonal = 375
         camera.setPosition(Vector(-80, 250, 700) * diagonal / 375)
-        camera.setPerspective(True)
         camera.lookAt(Vector(0, 0, 0))
         controller.getScene().setActiveCamera("3d")
 

+ 2 - 2
cura/CuraView.py

@@ -18,8 +18,8 @@ class CuraView(View):
     def __init__(self, parent = None, use_empty_menu_placeholder: bool = False) -> None:
         super().__init__(parent)
 
-        self._empty_menu_placeholder_url = QUrl(Resources.getPath(CuraApplication.ResourceTypes.QmlFiles,
-                                                                  "EmptyViewMenuComponent.qml"))
+        self._empty_menu_placeholder_url = QUrl.fromLocalFile(Resources.getPath(CuraApplication.ResourceTypes.QmlFiles,
+                                                                                "EmptyViewMenuComponent.qml"))
         self._use_empty_menu_placeholder = use_empty_menu_placeholder
 
     @pyqtProperty(QUrl, constant = True)

+ 20 - 6
cura/Machines/Models/DiscoveredPrintersModel.py

@@ -62,6 +62,14 @@ class DiscoveredPrinter(QObject):
             self._machine_type = machine_type
             self.machineTypeChanged.emit()
 
+    # Checks if the given machine type name in the available machine list.
+    # The machine type is a code name such as "ultimaker_3", while the machine type name is the human-readable name of
+    # the machine type, which is "Ultimaker 3" for "ultimaker_3".
+    def _hasHumanReadableMachineTypeName(self, machine_type_name: str) -> bool:
+        from cura.CuraApplication import CuraApplication
+        results = CuraApplication.getInstance().getContainerRegistry().findDefinitionContainersMetadata(name = machine_type_name)
+        return len(results) > 0
+
     # Human readable machine type string
     @pyqtProperty(str, notify = machineTypeChanged)
     def readableMachineType(self) -> str:
@@ -70,24 +78,30 @@ class DiscoveredPrinter(QObject):
         # In ClusterUM3OutputDevice, when it updates a printer information, it updates the machine type using the field
         # "machine_variant", and for some reason, it's not the machine type ID/codename/... but a human-readable string
         # like "Ultimaker 3". The code below handles this case.
-        if machine_manager.hasHumanReadableMachineTypeName(self._machine_type):
+        if self._hasHumanReadableMachineTypeName(self._machine_type):
             readable_type = self._machine_type
         else:
-            readable_type = machine_manager.getMachineTypeNameFromId(self._machine_type)
+            readable_type = self._getMachineTypeNameFromId(self._machine_type)
             if not readable_type:
                 readable_type = catalog.i18nc("@label", "Unknown")
         return readable_type
 
     @pyqtProperty(bool, notify = machineTypeChanged)
     def isUnknownMachineType(self) -> bool:
-        from cura.CuraApplication import CuraApplication
-        machine_manager = CuraApplication.getInstance().getMachineManager()
-        if machine_manager.hasHumanReadableMachineTypeName(self._machine_type):
+        if self._hasHumanReadableMachineTypeName(self._machine_type):
             readable_type = self._machine_type
         else:
-            readable_type = machine_manager.getMachineTypeNameFromId(self._machine_type)
+            readable_type = self._getMachineTypeNameFromId(self._machine_type)
         return not readable_type
 
+    def _getMachineTypeNameFromId(self, machine_type_id: str) -> str:
+        machine_type_name = ""
+        from cura.CuraApplication import CuraApplication
+        results = CuraApplication.getInstance().getContainerRegistry().findDefinitionContainersMetadata(id = machine_type_id)
+        if results:
+            machine_type_name = results[0]["name"]
+        return machine_type_name
+
     @pyqtProperty(QObject, constant = True)
     def device(self) -> "NetworkedPrinterOutputDevice":
         return self._device

+ 0 - 3
cura/Machines/QualityManager.py

@@ -202,9 +202,6 @@ class QualityManager(QObject):
     def getQualityGroups(self, machine: "GlobalStack") -> Dict[str, QualityGroup]:
         machine_definition_id = getMachineDefinitionIDForQualitySearch(machine.definition)
 
-        # This determines if we should only get the global qualities for the global stack and skip the global qualities for the extruder stacks
-        has_machine_specific_qualities = machine.getHasMachineQuality()
-
         # To find the quality container for the GlobalStack, check in the following fall-back manner:
         #   (1) the machine-specific node
         #   (2) the generic node

+ 24 - 23
cura/PreviewPass.py

@@ -84,29 +84,30 @@ class PreviewPass(RenderPass):
 
         # Fill up the batch with objects that can be sliced.
         for node in DepthFirstIterator(self._scene.getRoot()): #type: ignore #Ignore type error because iter() should get called automatically by Python syntax.
-            if node.callDecoration("isSliceable") and node.getMeshData() and node.isVisible():
-                per_mesh_stack = node.callDecoration("getStack")
-                if node.callDecoration("isNonThumbnailVisibleMesh"):
-                    # Non printing mesh
-                    continue
-                elif per_mesh_stack is not None and per_mesh_stack.getProperty("support_mesh", "value"):
-                    # Support mesh
-                    uniforms = {}
-                    shade_factor = 0.6
-                    diffuse_color = node.getDiffuseColor()
-                    diffuse_color2 = [
-                        diffuse_color[0] * shade_factor,
-                        diffuse_color[1] * shade_factor,
-                        diffuse_color[2] * shade_factor,
-                        1.0]
-                    uniforms["diffuse_color"] = prettier_color(diffuse_color)
-                    uniforms["diffuse_color_2"] = diffuse_color2
-                    batch_support_mesh.addItem(node.getWorldTransformation(), node.getMeshData(), uniforms = uniforms)
-                else:
-                    # Normal scene node
-                    uniforms = {}
-                    uniforms["diffuse_color"] = prettier_color(node.getDiffuseColor())
-                    batch.addItem(node.getWorldTransformation(), node.getMeshData(), uniforms = uniforms)
+            if hasattr(node, "_outside_buildarea") and not node._outside_buildarea:
+                if node.callDecoration("isSliceable") and node.getMeshData() and node.isVisible():
+                    per_mesh_stack = node.callDecoration("getStack")
+                    if node.callDecoration("isNonThumbnailVisibleMesh"):
+                        # Non printing mesh
+                        continue
+                    elif per_mesh_stack is not None and per_mesh_stack.getProperty("support_mesh", "value"):
+                        # Support mesh
+                        uniforms = {}
+                        shade_factor = 0.6
+                        diffuse_color = node.getDiffuseColor()
+                        diffuse_color2 = [
+                            diffuse_color[0] * shade_factor,
+                            diffuse_color[1] * shade_factor,
+                            diffuse_color[2] * shade_factor,
+                            1.0]
+                        uniforms["diffuse_color"] = prettier_color(diffuse_color)
+                        uniforms["diffuse_color_2"] = diffuse_color2
+                        batch_support_mesh.addItem(node.getWorldTransformation(), node.getMeshData(), uniforms = uniforms)
+                    else:
+                        # Normal scene node
+                        uniforms = {}
+                        uniforms["diffuse_color"] = prettier_color(node.getDiffuseColor())
+                        batch.addItem(node.getWorldTransformation(), node.getMeshData(), uniforms = uniforms)
 
         self.bind()
 

+ 5 - 11
cura/Scene/CuraSceneNode.py

@@ -1,4 +1,4 @@
-# Copyright (c) 2018 Ultimaker B.V.
+# Copyright (c) 2019 Ultimaker B.V.
 # Cura is released under the terms of the LGPLv3 or higher.
 
 from copy import deepcopy
@@ -14,6 +14,7 @@ import cura.CuraApplication #To get the build plate.
 from cura.Settings.ExtruderStack import ExtruderStack #For typing.
 from cura.Settings.SettingOverrideDecorator import SettingOverrideDecorator #For per-object settings.
 
+
 ##  Scene nodes that are models are only seen when selecting the corresponding build plate
 #   Note that many other nodes can just be UM SceneNode objects.
 class CuraSceneNode(SceneNode):
@@ -85,16 +86,6 @@ class CuraSceneNode(SceneNode):
             1.0
         ]
 
-    ##  Return if the provided bbox collides with the bbox of this scene node
-    def collidesWithBbox(self, check_bbox: AxisAlignedBox) -> bool:
-        bbox = self.getBoundingBox()
-        if bbox is not None:
-            # Mark the node as outside the build volume if the bounding box test fails.
-            if check_bbox.intersectsBox(bbox) != AxisAlignedBox.IntersectionResult.FullIntersection:
-                return True
-
-        return False
-
     ##  Return if any area collides with the convex hull of this scene node
     def collidesWithArea(self, areas: List[Polygon]) -> bool:
         convex_hull = self.callDecoration("getConvexHull")
@@ -115,6 +106,9 @@ class CuraSceneNode(SceneNode):
         self._aabb = None
         if self._mesh_data:
             self._aabb = self._mesh_data.getExtents(self.getWorldTransformation())
+        else:  # If there is no mesh_data, use a boundingbox that encompasses the local (0,0,0)
+            position = self.getWorldPosition()
+            self._aabb = AxisAlignedBox(minimum=position, maximum=position)
 
         for child in self.getAllChildren():
             if child.callDecoration("isNonPrintingMesh"):

+ 11 - 9
cura/Settings/CuraContainerRegistry.py

@@ -103,13 +103,14 @@ class CuraContainerRegistry(ContainerRegistry):
     #   \param instance_ids \type{list} the IDs of the profiles to export.
     #   \param file_name \type{str} the full path and filename to export to.
     #   \param file_type \type{str} the file type with the format "<description> (*.<extension>)"
-    def exportQualityProfile(self, container_list, file_name, file_type):
+    #   \return True if the export succeeded, false otherwise.
+    def exportQualityProfile(self, container_list, file_name, file_type) -> bool:
         # Parse the fileType to deduce what plugin can save the file format.
         # fileType has the format "<description> (*.<extension>)"
         split = file_type.rfind(" (*.")  # Find where the description ends and the extension starts.
         if split < 0:  # Not found. Invalid format.
             Logger.log("e", "Invalid file format identifier %s", file_type)
-            return
+            return False
         description = file_type[:split]
         extension = file_type[split + 4:-1]  # Leave out the " (*." and ")".
         if not file_name.endswith("." + extension):  # Auto-fill the extension if the user did not provide any.
@@ -121,7 +122,7 @@ class CuraContainerRegistry(ContainerRegistry):
                 result = QMessageBox.question(None, catalog.i18nc("@title:window", "File Already Exists"),
                                               catalog.i18nc("@label Don't translate the XML tag <filename>!", "The file <filename>{0}</filename> already exists. Are you sure you want to overwrite it?").format(file_name))
                 if result == QMessageBox.No:
-                    return
+                    return False
 
         profile_writer = self._findProfileWriter(extension, description)
         try:
@@ -132,17 +133,18 @@ class CuraContainerRegistry(ContainerRegistry):
                         lifetime = 0,
                         title = catalog.i18nc("@info:title", "Error"))
             m.show()
-            return
+            return False
         if not success:
             Logger.log("w", "Failed to export profile to %s: Writer plugin reported failure.", file_name)
             m = Message(catalog.i18nc("@info:status Don't translate the XML tag <filename>!", "Failed to export profile to <filename>{0}</filename>: Writer plugin reported failure.", file_name),
                         lifetime = 0,
                         title = catalog.i18nc("@info:title", "Error"))
             m.show()
-            return
+            return False
         m = Message(catalog.i18nc("@info:status Don't translate the XML tag <filename>!", "Exported profile to <filename>{0}</filename>", file_name),
                     title = catalog.i18nc("@info:title", "Export succeeded"))
         m.show()
+        return True
 
     ##  Gets the plugin object matching the criteria
     #   \param extension
@@ -169,9 +171,6 @@ class CuraContainerRegistry(ContainerRegistry):
         if not file_name:
             return { "status": "error", "message": catalog.i18nc("@info:status Don't translate the XML tags <filename>!", "Failed to import profile from <filename>{0}</filename>: {1}", file_name, "Invalid path")}
 
-        plugin_registry = PluginRegistry.getInstance()
-        extension = file_name.split(".")[-1]
-
         global_stack = Application.getInstance().getGlobalContainerStack()
         if not global_stack:
             return {"status": "error", "message": catalog.i18nc("@info:status Don't translate the XML tags <filename>!", "Can't import profile from <filename>{0}</filename> before a printer is added.", file_name)}
@@ -180,6 +179,9 @@ class CuraContainerRegistry(ContainerRegistry):
         for position in sorted(global_stack.extruders):
             machine_extruders.append(global_stack.extruders[position])
 
+        plugin_registry = PluginRegistry.getInstance()
+        extension = file_name.split(".")[-1]
+
         for plugin_id, meta_data in self._getIOPlugins("profile_reader"):
             if meta_data["profile_reader"][0]["extension"] != extension:
                 continue
@@ -281,7 +283,7 @@ class CuraContainerRegistry(ContainerRegistry):
                                         profile.addInstance(new_instance)
                                         profile.setDirty(True)
 
-                                    global_profile.removeInstance(qc_setting_key, postpone_emit=True)
+                                    global_profile.removeInstance(qc_setting_key, postpone_emit = True)
                         extruder_profiles.append(profile)
 
                     for profile in extruder_profiles:

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