1# Copyright (c) 2020 Ultimaker B.V.
2# Cura is released under the terms of the LGPLv3 or higher.
3
4import numpy
5import math
6
7from typing import List, Optional, TYPE_CHECKING, Any, Set, cast, Iterable, Dict
8
9from UM.Mesh.MeshData import MeshData
10from UM.Mesh.MeshBuilder import MeshBuilder
11
12from UM.Application import Application #To modify the maximum zoom level.
13from UM.i18n import i18nCatalog
14from UM.Scene.Platform import Platform
15from UM.Scene.Iterator.BreadthFirstIterator import BreadthFirstIterator
16from UM.Scene.SceneNode import SceneNode
17from UM.Resources import Resources
18
19from UM.Math.Vector import Vector
20from UM.Math.Matrix import Matrix
21from UM.Math.Color import Color
22from UM.Math.AxisAlignedBox import AxisAlignedBox
23from UM.Math.Polygon import Polygon
24from UM.Message import Message
25from UM.Signal import Signal
26from UM.View.RenderBatch import RenderBatch
27from UM.View.GL.OpenGL import OpenGL
28
29from cura.Settings.GlobalStack import GlobalStack
30from cura.Scene.CuraSceneNode import CuraSceneNode
31from cura.Settings.ExtruderManager import ExtruderManager
32
33from PyQt5.QtCore import QTimer
34
35
36if TYPE_CHECKING:
37    from cura.CuraApplication import CuraApplication
38    from cura.Settings.ExtruderStack import ExtruderStack
39    from UM.Settings.ContainerStack import ContainerStack
40
41catalog = i18nCatalog("cura")
42
43# Radius of disallowed area in mm around prime. I.e. how much distance to keep from prime position.
44PRIME_CLEARANCE = 6.5
45
46
47class BuildVolume(SceneNode):
48    """Build volume is a special kind of node that is responsible for rendering the printable area & disallowed areas."""
49
50    raftThicknessChanged = Signal()
51
52    def __init__(self, application: "CuraApplication", parent: Optional[SceneNode] = None) -> None:
53        super().__init__(parent)
54        self._application = application
55        self._machine_manager = self._application.getMachineManager()
56
57        self._volume_outline_color = None  # type: Optional[Color]
58        self._x_axis_color = None  # type: Optional[Color]
59        self._y_axis_color = None  # type: Optional[Color]
60        self._z_axis_color = None  # type: Optional[Color]
61        self._disallowed_area_color = None  # type: Optional[Color]
62        self._error_area_color = None  # type: Optional[Color]
63
64        self._width = 0  # type: float
65        self._height = 0  # type: float
66        self._depth = 0  # type: float
67        self._shape = ""  # type: str
68
69        self._shader = None
70
71        self._origin_mesh = None  # type: Optional[MeshData]
72        self._origin_line_length = 20
73        self._origin_line_width = 1.5
74        self._enabled = False
75
76        self._grid_mesh = None   # type: Optional[MeshData]
77        self._grid_shader = None
78
79        self._disallowed_areas = []  # type: List[Polygon]
80        self._disallowed_areas_no_brim = []  # type: List[Polygon]
81        self._disallowed_area_mesh = None  # type: Optional[MeshData]
82        self._disallowed_area_size = 0.
83
84        self._error_areas = []  # type: List[Polygon]
85        self._error_mesh = None  # type: Optional[MeshData]
86
87        self.setCalculateBoundingBox(False)
88        self._volume_aabb = None  # type: Optional[AxisAlignedBox]
89
90        self._raft_thickness = 0.0
91        self._extra_z_clearance = 0.0
92        self._adhesion_type = None  # type: Any
93        self._platform = Platform(self)
94
95        self._edge_disallowed_size = None
96
97        self._build_volume_message = Message(catalog.i18nc("@info:status",
98            "The build volume height has been reduced due to the value of the"
99            " \"Print Sequence\" setting to prevent the gantry from colliding"
100            " with printed models."), title = catalog.i18nc("@info:title", "Build Volume"))
101
102        self._global_container_stack = None  # type: Optional[GlobalStack]
103
104        self._stack_change_timer = QTimer()
105        self._stack_change_timer.setInterval(100)
106        self._stack_change_timer.setSingleShot(True)
107        self._stack_change_timer.timeout.connect(self._onStackChangeTimerFinished)
108
109        self._application.globalContainerStackChanged.connect(self._onStackChanged)
110
111        self._engine_ready = False
112        self._application.engineCreatedSignal.connect(self._onEngineCreated)
113
114        self._has_errors = False
115        self._application.getController().getScene().sceneChanged.connect(self._onSceneChanged)
116
117        # Objects loaded at the moment. We are connected to the property changed events of these objects.
118        self._scene_objects = set()  # type: Set[SceneNode]
119
120        self._scene_change_timer = QTimer()
121        self._scene_change_timer.setInterval(200)
122        self._scene_change_timer.setSingleShot(True)
123        self._scene_change_timer.timeout.connect(self._onSceneChangeTimerFinished)
124
125        self._setting_change_timer = QTimer()
126        self._setting_change_timer.setInterval(150)
127        self._setting_change_timer.setSingleShot(True)
128        self._setting_change_timer.timeout.connect(self._onSettingChangeTimerFinished)
129
130        # Must be after setting _build_volume_message, apparently that is used in getMachineManager.
131        # activeQualityChanged is always emitted after setActiveVariant, setActiveMaterial and setActiveQuality.
132        # Therefore this works.
133        self._machine_manager.activeQualityChanged.connect(self._onStackChanged)
134
135        # Enable and disable extruder
136        self._machine_manager.extruderChanged.connect(self.updateNodeBoundaryCheck)
137
138        # List of settings which were updated
139        self._changed_settings_since_last_rebuild = []  # type: List[str]
140
141    def _onSceneChanged(self, source):
142        if self._global_container_stack:
143            # Ignore anything that is not something we can slice in the first place!
144            if source.callDecoration("isSliceable"):
145                self._scene_change_timer.start()
146
147    def _onSceneChangeTimerFinished(self):
148        root = self._application.getController().getScene().getRoot()
149        new_scene_objects = set(node for node in BreadthFirstIterator(root) if node.callDecoration("isSliceable"))
150        if new_scene_objects != self._scene_objects:
151            for node in new_scene_objects - self._scene_objects: #Nodes that were added to the scene.
152                self._updateNodeListeners(node)
153                node.decoratorsChanged.connect(self._updateNodeListeners)  # Make sure that decoration changes afterwards also receive the same treatment
154            for node in self._scene_objects - new_scene_objects: #Nodes that were removed from the scene.
155                per_mesh_stack = node.callDecoration("getStack")
156                if per_mesh_stack:
157                    per_mesh_stack.propertyChanged.disconnect(self._onSettingPropertyChanged)
158                active_extruder_changed = node.callDecoration("getActiveExtruderChangedSignal")
159                if active_extruder_changed is not None:
160                    node.callDecoration("getActiveExtruderChangedSignal").disconnect(self._updateDisallowedAreasAndRebuild)
161                node.decoratorsChanged.disconnect(self._updateNodeListeners)
162            self.rebuild()
163
164            self._scene_objects = new_scene_objects
165            self._onSettingPropertyChanged("print_sequence", "value")  # Create fake event, so right settings are triggered.
166
167    def _updateNodeListeners(self, node: SceneNode):
168        """Updates the listeners that listen for changes in per-mesh stacks.
169
170        :param node: The node for which the decorators changed.
171        """
172
173        per_mesh_stack = node.callDecoration("getStack")
174        if per_mesh_stack:
175            per_mesh_stack.propertyChanged.connect(self._onSettingPropertyChanged)
176        active_extruder_changed = node.callDecoration("getActiveExtruderChangedSignal")
177        if active_extruder_changed is not None:
178            active_extruder_changed.connect(self._updateDisallowedAreasAndRebuild)
179
180    def setWidth(self, width: float) -> None:
181        self._width = width
182
183    def getWidth(self) -> float:
184        return self._width
185
186    def setHeight(self, height: float) -> None:
187        self._height = height
188
189    def getHeight(self) -> float:
190        return self._height
191
192    def setDepth(self, depth: float) -> None:
193        self._depth = depth
194
195    def getDepth(self) -> float:
196        return self._depth
197
198    def setShape(self, shape: str) -> None:
199        if shape:
200            self._shape = shape
201
202    def getDiagonalSize(self) -> float:
203        """Get the length of the 3D diagonal through the build volume.
204
205        This gives a sense of the scale of the build volume in general.
206
207        :return: length of the 3D diagonal through the build volume
208        """
209
210        return math.sqrt(self._width * self._width + self._height * self._height + self._depth * self._depth)
211
212    def getDisallowedAreas(self) -> List[Polygon]:
213        return self._disallowed_areas
214
215    def getDisallowedAreasNoBrim(self) -> List[Polygon]:
216        return self._disallowed_areas_no_brim
217
218    def setDisallowedAreas(self, areas: List[Polygon]):
219        self._disallowed_areas = areas
220
221    def render(self, renderer):
222        if not self.getMeshData() or not self.isVisible():
223            return True
224
225        if not self._shader:
226            self._shader = OpenGL.getInstance().createShaderProgram(Resources.getPath(Resources.Shaders, "default.shader"))
227            self._grid_shader = OpenGL.getInstance().createShaderProgram(Resources.getPath(Resources.Shaders, "grid.shader"))
228            theme = self._application.getTheme()
229            self._grid_shader.setUniformValue("u_plateColor", Color(*theme.getColor("buildplate").getRgb()))
230            self._grid_shader.setUniformValue("u_gridColor0", Color(*theme.getColor("buildplate_grid").getRgb()))
231            self._grid_shader.setUniformValue("u_gridColor1", Color(*theme.getColor("buildplate_grid_minor").getRgb()))
232
233        renderer.queueNode(self, mode = RenderBatch.RenderMode.Lines)
234        renderer.queueNode(self, mesh = self._origin_mesh, backface_cull = True)
235        renderer.queueNode(self, mesh = self._grid_mesh, shader = self._grid_shader, backface_cull = True)
236        if self._disallowed_area_mesh:
237            renderer.queueNode(self, mesh = self._disallowed_area_mesh, shader = self._shader, transparent = True, backface_cull = True, sort = -9)
238
239        if self._error_mesh:
240            renderer.queueNode(self, mesh=self._error_mesh, shader=self._shader, transparent=True,
241                               backface_cull=True, sort=-8)
242
243        return True
244
245    def updateNodeBoundaryCheck(self):
246        """For every sliceable node, update node._outside_buildarea"""
247
248        if not self._global_container_stack:
249            return
250
251        root = self._application.getController().getScene().getRoot()
252        nodes = cast(List[SceneNode], list(cast(Iterable, BreadthFirstIterator(root))))
253        group_nodes = []  # type: List[SceneNode]
254
255        build_volume_bounding_box = self.getBoundingBox()
256        if build_volume_bounding_box:
257            # It's over 9000!
258            # We set this to a very low number, as we do allow models to intersect the build plate.
259            # This means the model gets cut off at the build plate.
260            build_volume_bounding_box = build_volume_bounding_box.set(bottom=-9001)
261        else:
262            # No bounding box. This is triggered when running Cura from command line with a model for the first time
263            # In that situation there is a model, but no machine (and therefore no build volume.
264            return
265
266        for node in nodes:
267            # Need to check group nodes later
268            if node.callDecoration("isGroup"):
269                group_nodes.append(node)  # Keep list of affected group_nodes
270
271            if node.callDecoration("isSliceable") or node.callDecoration("isGroup"):
272                if not isinstance(node, CuraSceneNode):
273                    continue
274
275                if node.collidesWithBbox(build_volume_bounding_box):
276                    node.setOutsideBuildArea(True)
277                    continue
278
279                if node.collidesWithAreas(self.getDisallowedAreas()):
280                    node.setOutsideBuildArea(True)
281                    continue
282                # If the entire node is below the build plate, still mark it as outside.
283                node_bounding_box = node.getBoundingBox()
284                if node_bounding_box and node_bounding_box.top < 0:
285                    node.setOutsideBuildArea(True)
286                    continue
287                # Mark the node as outside build volume if the set extruder is disabled
288                extruder_position = node.callDecoration("getActiveExtruderPosition")
289                try:
290                    if not self._global_container_stack.extruderList[int(extruder_position)].isEnabled:
291                        node.setOutsideBuildArea(True)
292                        continue
293                except IndexError:  # Happens when the extruder list is too short. We're not done building the printer in memory yet.
294                    continue
295                except TypeError:  # Happens when extruder_position is None. This object has no extruder decoration.
296                    continue
297
298                node.setOutsideBuildArea(False)
299
300        # Group nodes should override the _outside_buildarea property of their children.
301        for group_node in group_nodes:
302            children = group_node.getAllChildren()
303
304            # Check if one or more children are non-printable and if so, set the parent as non-printable:
305            for child_node in children:
306                if child_node.isOutsideBuildArea():
307                    group_node.setOutsideBuildArea(True)
308                    break
309
310            # Apply results of the check to all children of the group:
311            for child_node in children:
312                child_node.setOutsideBuildArea(group_node.isOutsideBuildArea())
313
314    def checkBoundsAndUpdate(self, node: CuraSceneNode, bounds: Optional[AxisAlignedBox] = None) -> None:
315        """Update the outsideBuildArea of a single node, given bounds or current build volume
316
317        :param node: single node
318        :param bounds: bounds or current build volume
319        """
320
321        if not isinstance(node, CuraSceneNode) or self._global_container_stack is None:
322            return
323
324        if bounds is None:
325            build_volume_bounding_box = self.getBoundingBox()
326            if build_volume_bounding_box:
327                # It's over 9000!
328                build_volume_bounding_box = build_volume_bounding_box.set(bottom=-9001)
329            else:
330                # No bounding box. This is triggered when running Cura from command line with a model for the first time
331                # In that situation there is a model, but no machine (and therefore no build volume.
332                return
333        else:
334            build_volume_bounding_box = bounds
335
336        if node.callDecoration("isSliceable") or node.callDecoration("isGroup"):
337            if node.collidesWithBbox(build_volume_bounding_box):
338                node.setOutsideBuildArea(True)
339                return
340
341            if node.collidesWithAreas(self.getDisallowedAreas()):
342                node.setOutsideBuildArea(True)
343                return
344
345            # Mark the node as outside build volume if the set extruder is disabled
346            extruder_position = node.callDecoration("getActiveExtruderPosition")
347            if not self._global_container_stack.extruderList[int(extruder_position)].isEnabled:
348                node.setOutsideBuildArea(True)
349                return
350
351            node.setOutsideBuildArea(False)
352
353    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:
354        mb = MeshBuilder()
355        if self._shape != "elliptic":
356            # Build plate grid mesh
357            mb.addQuad(
358                Vector(min_w, min_h - z_fight_distance, min_d),
359                Vector(max_w, min_h - z_fight_distance, min_d),
360                Vector(max_w, min_h - z_fight_distance, max_d),
361                Vector(min_w, min_h - z_fight_distance, max_d)
362            )
363
364            for n in range(0, 6):
365                v = mb.getVertex(n)
366                mb.setVertexUVCoordinates(n, v[0], v[2])
367            return mb.build()
368        else:
369            aspect = 1.0
370            scale_matrix = Matrix()
371            if self._width != 0:
372                # Scale circular meshes by aspect ratio if width != height
373                aspect = self._depth / self._width
374                scale_matrix.compose(scale=Vector(1, 1, aspect))
375            mb.addVertex(0, min_h - z_fight_distance, 0)
376            mb.addArc(max_w, Vector.Unit_Y, center=Vector(0, min_h - z_fight_distance, 0))
377            sections = mb.getVertexCount() - 1  # Center point is not an arc section
378            indices = []
379            for n in range(0, sections - 1):
380                indices.append([0, n + 2, n + 1])
381            mb.addIndices(numpy.asarray(indices, dtype=numpy.int32))
382            mb.calculateNormals()
383
384            for n in range(0, mb.getVertexCount()):
385                v = mb.getVertex(n)
386                mb.setVertexUVCoordinates(n, v[0], v[2] * aspect)
387            return mb.build().getTransformed(scale_matrix)
388
389    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:
390        if self._shape != "elliptic":
391            # Outline 'cube' of the build volume
392            mb = MeshBuilder()
393            mb.addLine(Vector(min_w, min_h, min_d), Vector(max_w, min_h, min_d), color = self._volume_outline_color)
394            mb.addLine(Vector(min_w, min_h, min_d), Vector(min_w, max_h, min_d), color = self._volume_outline_color)
395            mb.addLine(Vector(min_w, max_h, min_d), Vector(max_w, max_h, min_d), color = self._volume_outline_color)
396            mb.addLine(Vector(max_w, min_h, min_d), Vector(max_w, max_h, min_d), color = self._volume_outline_color)
397
398            mb.addLine(Vector(min_w, min_h, max_d), Vector(max_w, min_h, max_d), color = self._volume_outline_color)
399            mb.addLine(Vector(min_w, min_h, max_d), Vector(min_w, max_h, max_d), color = self._volume_outline_color)
400            mb.addLine(Vector(min_w, max_h, max_d), Vector(max_w, max_h, max_d), color = self._volume_outline_color)
401            mb.addLine(Vector(max_w, min_h, max_d), Vector(max_w, max_h, max_d), color = self._volume_outline_color)
402
403            mb.addLine(Vector(min_w, min_h, min_d), Vector(min_w, min_h, max_d), color = self._volume_outline_color)
404            mb.addLine(Vector(max_w, min_h, min_d), Vector(max_w, min_h, max_d), color = self._volume_outline_color)
405            mb.addLine(Vector(min_w, max_h, min_d), Vector(min_w, max_h, max_d), color = self._volume_outline_color)
406            mb.addLine(Vector(max_w, max_h, min_d), Vector(max_w, max_h, max_d), color = self._volume_outline_color)
407
408            return mb.build()
409
410        else:
411            # Bottom and top 'ellipse' of the build volume
412            scale_matrix = Matrix()
413            if self._width != 0:
414                # Scale circular meshes by aspect ratio if width != height
415                aspect = self._depth / self._width
416                scale_matrix.compose(scale = Vector(1, 1, aspect))
417            mb = MeshBuilder()
418            mb.addArc(max_w, Vector.Unit_Y, center = (0, min_h - z_fight_distance, 0), color = self._volume_outline_color)
419            mb.addArc(max_w, Vector.Unit_Y, center = (0, max_h, 0),  color = self._volume_outline_color)
420            return mb.build().getTransformed(scale_matrix)
421
422    def _buildOriginMesh(self, origin: Vector) -> MeshData:
423        mb = MeshBuilder()
424        mb.addCube(
425            width=self._origin_line_length,
426            height=self._origin_line_width,
427            depth=self._origin_line_width,
428            center=origin + Vector(self._origin_line_length / 2, 0, 0),
429            color=self._x_axis_color
430        )
431        mb.addCube(
432            width=self._origin_line_width,
433            height=self._origin_line_length,
434            depth=self._origin_line_width,
435            center=origin + Vector(0, self._origin_line_length / 2, 0),
436            color=self._y_axis_color
437        )
438        mb.addCube(
439            width=self._origin_line_width,
440            height=self._origin_line_width,
441            depth=self._origin_line_length,
442            center=origin - Vector(0, 0, self._origin_line_length / 2),
443            color=self._z_axis_color
444        )
445        return mb.build()
446
447    def _updateColors(self):
448        theme = self._application.getTheme()
449        if theme is None:
450            return
451        self._volume_outline_color = Color(*theme.getColor("volume_outline").getRgb())
452        self._x_axis_color = Color(*theme.getColor("x_axis").getRgb())
453        self._y_axis_color = Color(*theme.getColor("y_axis").getRgb())
454        self._z_axis_color = Color(*theme.getColor("z_axis").getRgb())
455        self._disallowed_area_color = Color(*theme.getColor("disallowed_area").getRgb())
456        self._error_area_color = Color(*theme.getColor("error_area").getRgb())
457
458    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]:
459        if not self._error_areas:
460            return None
461        mb = MeshBuilder()
462        for error_area in self._error_areas:
463            color = self._error_area_color
464            points = error_area.getPoints()
465            first = Vector(self._clamp(points[0][0], min_w, max_w), disallowed_area_height,
466                           self._clamp(points[0][1], min_d, max_d))
467            previous_point = Vector(self._clamp(points[0][0], min_w, max_w), disallowed_area_height,
468                                    self._clamp(points[0][1], min_d, max_d))
469            for point in points:
470                new_point = Vector(self._clamp(point[0], min_w, max_w), disallowed_area_height,
471                                   self._clamp(point[1], min_d, max_d))
472                mb.addFace(first, previous_point, new_point, color=color)
473                previous_point = new_point
474        return mb.build()
475
476    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]:
477        if not self._disallowed_areas:
478            return None
479
480        mb = MeshBuilder()
481        color = self._disallowed_area_color
482        for polygon in self._disallowed_areas:
483            points = polygon.getPoints()
484            if len(points) == 0:
485                continue
486
487            first = Vector(self._clamp(points[0][0], min_w, max_w), disallowed_area_height,
488                           self._clamp(points[0][1], min_d, max_d))
489            previous_point = Vector(self._clamp(points[0][0], min_w, max_w), disallowed_area_height,
490                                    self._clamp(points[0][1], min_d, max_d))
491            for point in points:
492                new_point = Vector(self._clamp(point[0], min_w, max_w), disallowed_area_height,
493                                   self._clamp(point[1], min_d, max_d))
494                mb.addFace(first, previous_point, new_point, color=color)
495                previous_point = new_point
496
497            # Find the largest disallowed area to exclude it from the maximum scale bounds.
498            # This is a very nasty hack. This pretty much only works for UM machines.
499            # This disallowed area_size needs a -lot- of rework at some point in the future: TODO
500            if numpy.min(points[:,
501                         1]) >= 0:  # This filters out all areas that have points to the left of the centre. This is done to filter the skirt area.
502                size = abs(numpy.max(points[:, 1]) - numpy.min(points[:, 1]))
503            else:
504                size = 0
505            self._disallowed_area_size = max(size, self._disallowed_area_size)
506        return mb.build()
507
508    def rebuild(self) -> None:
509        """Recalculates the build volume & disallowed areas."""
510
511        if not self._width or not self._height or not self._depth:
512            return
513
514        if not self._engine_ready:
515            return
516
517        if not self._global_container_stack:
518            return
519
520        if not self._volume_outline_color:
521            self._updateColors()
522
523        min_w = -self._width / 2
524        max_w = self._width / 2
525        min_h = 0.0
526        max_h = self._height
527        min_d = -self._depth / 2
528        max_d = self._depth / 2
529
530        z_fight_distance = 0.2  # Distance between buildplate and disallowed area meshes to prevent z-fighting
531
532        self._grid_mesh = self._buildGridMesh(min_w, max_w, min_h, max_h, min_d, max_d, z_fight_distance)
533        self.setMeshData(self._buildMesh(min_w, max_w, min_h, max_h, min_d, max_d, z_fight_distance))
534
535        # Indication of the machine origin
536        if self._global_container_stack.getProperty("machine_center_is_zero", "value"):
537            origin = (Vector(min_w, min_h, min_d) + Vector(max_w, min_h, max_d)) / 2
538        else:
539            origin = Vector(min_w, min_h, max_d)
540
541        self._origin_mesh = self._buildOriginMesh(origin)
542
543        disallowed_area_height = 0.1
544        self._disallowed_area_size = 0.
545        self._disallowed_area_mesh = self._buildDisallowedAreaMesh(min_w, max_w, min_h, max_h, min_d, max_d, disallowed_area_height)
546
547        self._error_mesh = self._buildErrorMesh(min_w, max_w, min_h, max_h, min_d, max_d, disallowed_area_height)
548
549        self._volume_aabb = AxisAlignedBox(
550            minimum = Vector(min_w, min_h - 1.0, min_d),
551            maximum = Vector(max_w, max_h - self._raft_thickness - self._extra_z_clearance, max_d))
552
553        bed_adhesion_size = self.getEdgeDisallowedSize()
554
555        # As this works better for UM machines, we only add the disallowed_area_size for the z direction.
556        # This is probably wrong in all other cases. TODO!
557        # The +1 and -1 is added as there is always a bit of extra room required to work properly.
558        scale_to_max_bounds = AxisAlignedBox(
559            minimum = Vector(min_w + bed_adhesion_size + 1, min_h, min_d + self._disallowed_area_size - bed_adhesion_size + 1),
560            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)
561        )
562
563        self._application.getController().getScene()._maximum_bounds = scale_to_max_bounds  # type: ignore
564
565        self.updateNodeBoundaryCheck()
566
567    def getBoundingBox(self):
568        return self._volume_aabb
569
570    def getRaftThickness(self) -> float:
571        return self._raft_thickness
572
573    def _updateRaftThickness(self) -> None:
574        if not self._global_container_stack:
575            return
576
577        old_raft_thickness = self._raft_thickness
578        if self._global_container_stack.extruderList:
579            # This might be called before the extruder stacks have initialised, in which case getting the adhesion_type fails
580            self._adhesion_type = self._global_container_stack.getProperty("adhesion_type", "value")
581        self._raft_thickness = 0.0
582        if self._adhesion_type == "raft":
583            self._raft_thickness = (
584                self._global_container_stack.getProperty("raft_base_thickness", "value") +
585                self._global_container_stack.getProperty("raft_interface_thickness", "value") +
586                self._global_container_stack.getProperty("raft_surface_layers", "value") *
587                self._global_container_stack.getProperty("raft_surface_thickness", "value") +
588                self._global_container_stack.getProperty("raft_airgap", "value") -
589                self._global_container_stack.getProperty("layer_0_z_overlap", "value"))
590
591        # Rounding errors do not matter, we check if raft_thickness has changed at all
592        if old_raft_thickness != self._raft_thickness:
593            self.setPosition(Vector(0, -self._raft_thickness, 0), SceneNode.TransformSpace.World)
594            self.raftThicknessChanged.emit()
595
596    def _calculateExtraZClearance(self, extruders: List["ContainerStack"]) -> float:
597        if not self._global_container_stack:
598            return 0
599
600        extra_z = 0.0
601        for extruder in extruders:
602            if extruder.getProperty("retraction_hop_enabled", "value"):
603                retraction_hop = extruder.getProperty("retraction_hop", "value")
604                if extra_z is None or retraction_hop > extra_z:
605                    extra_z = retraction_hop
606        return extra_z
607
608    def _onStackChanged(self):
609        self._stack_change_timer.start()
610
611    def _onStackChangeTimerFinished(self) -> None:
612        """Update the build volume visualization"""
613
614        if self._global_container_stack:
615            self._global_container_stack.propertyChanged.disconnect(self._onSettingPropertyChanged)
616            extruders = ExtruderManager.getInstance().getActiveExtruderStacks()
617            for extruder in extruders:
618                extruder.propertyChanged.disconnect(self._onSettingPropertyChanged)
619
620        self._global_container_stack = self._application.getGlobalContainerStack()
621
622        if self._global_container_stack:
623            self._global_container_stack.propertyChanged.connect(self._onSettingPropertyChanged)
624            extruders = ExtruderManager.getInstance().getActiveExtruderStacks()
625            for extruder in extruders:
626                extruder.propertyChanged.connect(self._onSettingPropertyChanged)
627
628            self._width = self._global_container_stack.getProperty("machine_width", "value")
629            machine_height = self._global_container_stack.getProperty("machine_height", "value")
630            if self._global_container_stack.getProperty("print_sequence", "value") == "one_at_a_time" and len(self._scene_objects) > 1:
631                self._height = min(self._global_container_stack.getProperty("gantry_height", "value"), machine_height)
632                if self._height < machine_height:
633                    self._build_volume_message.show()
634                else:
635                    self._build_volume_message.hide()
636            else:
637                self._height = self._global_container_stack.getProperty("machine_height", "value")
638                self._build_volume_message.hide()
639            self._depth = self._global_container_stack.getProperty("machine_depth", "value")
640            self._shape = self._global_container_stack.getProperty("machine_shape", "value")
641
642            self._updateDisallowedAreas()
643            self._updateRaftThickness()
644            self._extra_z_clearance = self._calculateExtraZClearance(ExtruderManager.getInstance().getUsedExtruderStacks())
645
646            if self._engine_ready:
647                self.rebuild()
648
649            camera = Application.getInstance().getController().getCameraTool()
650            if camera:
651                diagonal = self.getDiagonalSize()
652                if diagonal > 1:
653                    # You can zoom out up to 5 times the diagonal. This gives some space around the volume.
654                    camera.setZoomRange(min = 0.1, max = diagonal * 5)  # type: ignore
655
656    def _onEngineCreated(self) -> None:
657        self._engine_ready = True
658        self.rebuild()
659
660    def _onSettingChangeTimerFinished(self) -> None:
661        if not self._global_container_stack:
662            return
663
664        rebuild_me = False
665        update_disallowed_areas = False
666        update_raft_thickness = False
667        update_extra_z_clearance = True
668
669        for setting_key in self._changed_settings_since_last_rebuild:
670            if setting_key == "print_sequence":
671                machine_height = self._global_container_stack.getProperty("machine_height", "value")
672                if self._application.getGlobalContainerStack().getProperty("print_sequence", "value") == "one_at_a_time" and len(self._scene_objects) > 1:
673                    self._height = min(self._global_container_stack.getProperty("gantry_height", "value"), machine_height)
674                    if self._height < machine_height:
675                        self._build_volume_message.show()
676                    else:
677                        self._build_volume_message.hide()
678                else:
679                    self._height = self._global_container_stack.getProperty("machine_height", "value")
680                    self._build_volume_message.hide()
681                update_disallowed_areas = True
682
683            # sometimes the machine size or shape settings are adjusted on the active machine, we should reflect this
684            if setting_key in self._machine_settings:
685                self._updateMachineSizeProperties()
686                update_extra_z_clearance = True
687                update_disallowed_areas = True
688
689            if setting_key in self._disallowed_area_settings:
690                update_disallowed_areas = True
691
692            if setting_key in self._raft_settings:
693                update_raft_thickness = True
694
695            if setting_key in self._extra_z_settings:
696                update_extra_z_clearance = True
697
698            if setting_key in self._limit_to_extruder_settings:
699                update_disallowed_areas = True
700
701            rebuild_me = update_extra_z_clearance or update_disallowed_areas or update_raft_thickness
702
703        # We only want to update all of them once.
704        if update_disallowed_areas:
705            self._updateDisallowedAreas()
706
707        if update_raft_thickness:
708            self._updateRaftThickness()
709
710        if update_extra_z_clearance:
711            self._extra_z_clearance = self._calculateExtraZClearance(ExtruderManager.getInstance().getUsedExtruderStacks())
712
713        if rebuild_me:
714            self.rebuild()
715
716        # We just did a rebuild, reset the list.
717        self._changed_settings_since_last_rebuild = []
718
719    def _onSettingPropertyChanged(self, setting_key: str, property_name: str) -> None:
720        if property_name != "value":
721            return
722
723        if setting_key not in self._changed_settings_since_last_rebuild:
724            self._changed_settings_since_last_rebuild.append(setting_key)
725            self._setting_change_timer.start()
726
727    def hasErrors(self) -> bool:
728        return self._has_errors
729
730    def _updateMachineSizeProperties(self) -> None:
731        if not self._global_container_stack:
732            return
733        self._height = self._global_container_stack.getProperty("machine_height", "value")
734        self._width = self._global_container_stack.getProperty("machine_width", "value")
735        self._depth = self._global_container_stack.getProperty("machine_depth", "value")
736        self._shape = self._global_container_stack.getProperty("machine_shape", "value")
737
738    def _updateDisallowedAreasAndRebuild(self):
739        """Calls :py:meth:`cura.BuildVolume._updateDisallowedAreas` and makes sure the changes appear in the scene.
740
741        This is required for a signal to trigger the update in one go. The
742        :py:meth:`cura.BuildVolume._updateDisallowedAreas` method itself shouldn't call
743        :py:meth:`cura.BuildVolume.rebuild`, since there may be other changes before it needs to be rebuilt,
744        which would hit performance.
745        """
746
747        self._updateDisallowedAreas()
748        self._updateRaftThickness()
749        self._extra_z_clearance = self._calculateExtraZClearance(ExtruderManager.getInstance().getUsedExtruderStacks())
750        self.rebuild()
751
752    def _updateDisallowedAreas(self) -> None:
753        if not self._global_container_stack:
754            return
755
756        self._error_areas = []
757
758        used_extruders = ExtruderManager.getInstance().getUsedExtruderStacks()
759        self._edge_disallowed_size = None  # Force a recalculation
760        disallowed_border_size = self.getEdgeDisallowedSize()
761
762        result_areas = self._computeDisallowedAreasStatic(disallowed_border_size, used_extruders)  # Normal machine disallowed areas can always be added.
763        prime_areas = self._computeDisallowedAreasPrimeBlob(disallowed_border_size, used_extruders)
764        result_areas_no_brim = self._computeDisallowedAreasStatic(0, used_extruders)  # Where the priming is not allowed to happen. This is not added to the result, just for collision checking.
765
766        # Check if prime positions intersect with disallowed areas.
767        for extruder in used_extruders:
768            extruder_id = extruder.getId()
769
770            result_areas[extruder_id].extend(prime_areas[extruder_id])
771            result_areas_no_brim[extruder_id].extend(prime_areas[extruder_id])
772
773            nozzle_disallowed_areas = extruder.getProperty("nozzle_disallowed_areas", "value")
774            for area in nozzle_disallowed_areas:
775                polygon = Polygon(numpy.array(area, numpy.float32))
776                polygon_disallowed_border = polygon.getMinkowskiHull(Polygon.approximatedCircle(disallowed_border_size))
777                result_areas[extruder_id].append(polygon_disallowed_border)  # Don't perform the offset on these.
778                result_areas_no_brim[extruder_id].append(polygon)  # No brim
779
780        # Add prime tower location as disallowed area.
781        if len([x for x in used_extruders if x.isEnabled]) > 1:  # No prime tower if only one extruder is enabled
782            prime_tower_collision = False
783            prime_tower_areas = self._computeDisallowedAreasPrinted(used_extruders)
784            for extruder_id in prime_tower_areas:
785                for area_index, prime_tower_area in enumerate(prime_tower_areas[extruder_id]):
786                    for area in result_areas[extruder_id]:
787                        if prime_tower_area.intersectsPolygon(area) is not None:
788                            prime_tower_collision = True
789                            break
790                    if prime_tower_collision:  # Already found a collision.
791                        break
792                    if self._global_container_stack.getProperty("prime_tower_brim_enable", "value") and self._global_container_stack.getProperty("adhesion_type", "value") != "raft":
793                        brim_size = self._calculateBedAdhesionSize(used_extruders, "brim")
794                        # Use 2x the brim size, since we need 1x brim size distance due to the object brim and another
795                        # times the brim due to the brim of the prime tower
796                        prime_tower_areas[extruder_id][area_index] = prime_tower_area.getMinkowskiHull(Polygon.approximatedCircle(2 * brim_size, num_segments = 24))
797                if not prime_tower_collision:
798                    result_areas[extruder_id].extend(prime_tower_areas[extruder_id])
799                    result_areas_no_brim[extruder_id].extend(prime_tower_areas[extruder_id])
800                else:
801                    self._error_areas.extend(prime_tower_areas[extruder_id])
802
803        self._has_errors = len(self._error_areas) > 0
804
805        self._disallowed_areas = []
806        for extruder_id in result_areas:
807            self._disallowed_areas.extend(result_areas[extruder_id])
808        self._disallowed_areas_no_brim = []
809        for extruder_id in result_areas_no_brim:
810            self._disallowed_areas_no_brim.extend(result_areas_no_brim[extruder_id])
811
812    def _computeDisallowedAreasPrinted(self, used_extruders):
813        """Computes the disallowed areas for objects that are printed with print features.
814
815        This means that the brim, travel avoidance and such will be applied to these features.
816
817        :return: A dictionary with for each used extruder ID the disallowed areas where that extruder may not print.
818        """
819
820        result = {}
821        adhesion_extruder = None #type: ExtruderStack
822        for extruder in used_extruders:
823            if int(extruder.getProperty("extruder_nr", "value")) == int(self._global_container_stack.getProperty("adhesion_extruder_nr", "value")):
824                adhesion_extruder = extruder
825            result[extruder.getId()] = []
826
827        # Currently, the only normally printed object is the prime tower.
828        if self._global_container_stack.getProperty("prime_tower_enable", "value"):
829            prime_tower_size = self._global_container_stack.getProperty("prime_tower_size", "value")
830            machine_width = self._global_container_stack.getProperty("machine_width", "value")
831            machine_depth = self._global_container_stack.getProperty("machine_depth", "value")
832            prime_tower_x = self._global_container_stack.getProperty("prime_tower_position_x", "value")
833            prime_tower_y = - self._global_container_stack.getProperty("prime_tower_position_y", "value")
834            if not self._global_container_stack.getProperty("machine_center_is_zero", "value"):
835                prime_tower_x = prime_tower_x - machine_width / 2 #Offset by half machine_width and _depth to put the origin in the front-left.
836                prime_tower_y = prime_tower_y + machine_depth / 2
837
838            if adhesion_extruder is not None and self._global_container_stack.getProperty("prime_tower_brim_enable", "value") and self._global_container_stack.getProperty("adhesion_type", "value") != "raft":
839                brim_size = (
840                    adhesion_extruder.getProperty("brim_line_count", "value") *
841                    adhesion_extruder.getProperty("skirt_brim_line_width", "value") / 100.0 *
842                    adhesion_extruder.getProperty("initial_layer_line_width_factor", "value")
843                )
844                prime_tower_x -= brim_size
845                prime_tower_y += brim_size
846
847            radius = prime_tower_size / 2
848            prime_tower_area = Polygon.approximatedCircle(radius, num_segments = 24)
849            prime_tower_area = prime_tower_area.translate(prime_tower_x - radius, prime_tower_y - radius)
850
851            prime_tower_area = prime_tower_area.getMinkowskiHull(Polygon.approximatedCircle(0))
852            for extruder in used_extruders:
853                result[extruder.getId()].append(prime_tower_area) #The prime tower location is the same for each extruder, regardless of offset.
854
855        return result
856
857    def _computeDisallowedAreasPrimeBlob(self, border_size: float, used_extruders: List["ExtruderStack"]) -> Dict[str, List[Polygon]]:
858        """Computes the disallowed areas for the prime blobs.
859
860        These are special because they are not subject to things like brim or travel avoidance. They do get a dilute
861        with the border size though because they may not intersect with brims and such of other objects.
862
863        :param border_size: The size with which to offset the disallowed areas due to skirt, brim, travel avoid distance
864         , etc.
865        :param used_extruders: The extruder stacks to generate disallowed areas for.
866        :return: A dictionary with for each used extruder ID the prime areas.
867        """
868
869        result = {}  # type: Dict[str, List[Polygon]]
870        if not self._global_container_stack:
871            return result
872        machine_width = self._global_container_stack.getProperty("machine_width", "value")
873        machine_depth = self._global_container_stack.getProperty("machine_depth", "value")
874        for extruder in used_extruders:
875            prime_blob_enabled = extruder.getProperty("prime_blob_enable", "value")
876            prime_x = extruder.getProperty("extruder_prime_pos_x", "value")
877            prime_y = -extruder.getProperty("extruder_prime_pos_y", "value")
878
879            # Ignore extruder prime position if it is not set or if blob is disabled
880            if (prime_x == 0 and prime_y == 0) or not prime_blob_enabled:
881                result[extruder.getId()] = []
882                continue
883
884            if not self._global_container_stack.getProperty("machine_center_is_zero", "value"):
885                prime_x = prime_x - machine_width / 2  # Offset by half machine_width and _depth to put the origin in the front-left.
886                prime_y = prime_y + machine_depth / 2
887
888            prime_polygon = Polygon.approximatedCircle(PRIME_CLEARANCE)
889            prime_polygon = prime_polygon.getMinkowskiHull(Polygon.approximatedCircle(border_size))
890
891            prime_polygon = prime_polygon.translate(prime_x, prime_y)
892            result[extruder.getId()] = [prime_polygon]
893
894        return result
895
896    def _computeDisallowedAreasStatic(self, border_size:float, used_extruders: List["ExtruderStack"]) -> Dict[str, List[Polygon]]:
897        """Computes the disallowed areas that are statically placed in the machine.
898
899        It computes different disallowed areas depending on the offset of the extruder. The resulting dictionary will
900         therefore have an entry for each extruder that is used.
901
902        :param border_size: The size with which to offset the disallowed areas due to skirt, brim, travel avoid distance
903         , etc.
904        :param used_extruders: The extruder stacks to generate disallowed areas for.
905        :return: A dictionary with for each used extruder ID the disallowed areas where that extruder may not print.
906        """
907
908        # Convert disallowed areas to polygons and dilate them.
909        machine_disallowed_polygons = []
910        if self._global_container_stack is None:
911            return {}
912
913        for area in self._global_container_stack.getProperty("machine_disallowed_areas", "value"):
914            polygon = Polygon(numpy.array(area, numpy.float32))
915            polygon = polygon.getMinkowskiHull(Polygon.approximatedCircle(border_size))
916            machine_disallowed_polygons.append(polygon)
917
918        # For certain machines we don't need to compute disallowed areas for each nozzle.
919        # So we check here and only do the nozzle offsetting if needed.
920        nozzle_offsetting_for_disallowed_areas = self._global_container_stack.getMetaDataEntry(
921            "nozzle_offsetting_for_disallowed_areas", True)
922
923        result = {}  # type: Dict[str, List[Polygon]]
924        for extruder in used_extruders:
925            extruder_id = extruder.getId()
926            offset_x = extruder.getProperty("machine_nozzle_offset_x", "value")
927            if offset_x is None:
928                offset_x = 0
929            offset_y = extruder.getProperty("machine_nozzle_offset_y", "value")
930            if offset_y is None:
931                offset_y = 0
932            offset_y = -offset_y  # Y direction of g-code is the inverse of Y direction of Cura's scene space.
933            result[extruder_id] = []
934
935            for polygon in machine_disallowed_polygons:
936                result[extruder_id].append(polygon.translate(offset_x, offset_y))  # Compensate for the nozzle offset of this extruder.
937
938            # Add the border around the edge of the build volume.
939            left_unreachable_border = 0
940            right_unreachable_border = 0
941            top_unreachable_border = 0
942            bottom_unreachable_border = 0
943
944            # Only do nozzle offsetting if needed
945            if nozzle_offsetting_for_disallowed_areas:
946                # The build volume is defined as the union of the area that all extruders can reach, so we need to know
947                # the relative offset to all extruders.
948                for other_extruder in ExtruderManager.getInstance().getActiveExtruderStacks():
949                    other_offset_x = other_extruder.getProperty("machine_nozzle_offset_x", "value")
950                    if other_offset_x is None:
951                        other_offset_x = 0
952                    other_offset_y = other_extruder.getProperty("machine_nozzle_offset_y", "value")
953                    if other_offset_y is None:
954                        other_offset_y = 0
955                    other_offset_y = -other_offset_y
956                    left_unreachable_border = min(left_unreachable_border, other_offset_x - offset_x)
957                    right_unreachable_border = max(right_unreachable_border, other_offset_x - offset_x)
958                    top_unreachable_border = min(top_unreachable_border, other_offset_y - offset_y)
959                    bottom_unreachable_border = max(bottom_unreachable_border, other_offset_y - offset_y)
960            half_machine_width = self._global_container_stack.getProperty("machine_width", "value") / 2
961            half_machine_depth = self._global_container_stack.getProperty("machine_depth", "value") / 2
962
963            if self._shape != "elliptic":
964                if border_size - left_unreachable_border > 0:
965                    result[extruder_id].append(Polygon(numpy.array([
966                        [-half_machine_width, -half_machine_depth],
967                        [-half_machine_width, half_machine_depth],
968                        [-half_machine_width + border_size - left_unreachable_border, half_machine_depth - border_size - bottom_unreachable_border],
969                        [-half_machine_width + border_size - left_unreachable_border, -half_machine_depth + border_size - top_unreachable_border]
970                    ], numpy.float32)))
971                if border_size + right_unreachable_border > 0:
972                    result[extruder_id].append(Polygon(numpy.array([
973                        [half_machine_width, half_machine_depth],
974                        [half_machine_width, -half_machine_depth],
975                        [half_machine_width - border_size - right_unreachable_border, -half_machine_depth + border_size - top_unreachable_border],
976                        [half_machine_width - border_size - right_unreachable_border, half_machine_depth - border_size - bottom_unreachable_border]
977                    ], numpy.float32)))
978                if border_size + bottom_unreachable_border > 0:
979                    result[extruder_id].append(Polygon(numpy.array([
980                        [-half_machine_width, half_machine_depth],
981                        [half_machine_width, half_machine_depth],
982                        [half_machine_width - border_size - right_unreachable_border, half_machine_depth - border_size - bottom_unreachable_border],
983                        [-half_machine_width + border_size - left_unreachable_border, half_machine_depth - border_size - bottom_unreachable_border]
984                    ], numpy.float32)))
985                if border_size - top_unreachable_border > 0:
986                    result[extruder_id].append(Polygon(numpy.array([
987                        [half_machine_width, -half_machine_depth],
988                        [-half_machine_width, -half_machine_depth],
989                        [-half_machine_width + border_size - left_unreachable_border, -half_machine_depth + border_size - top_unreachable_border],
990                        [half_machine_width - border_size - right_unreachable_border, -half_machine_depth + border_size - top_unreachable_border]
991                    ], numpy.float32)))
992            else:
993                sections = 32
994                arc_vertex = [0, half_machine_depth - border_size]
995                for i in range(0, sections):
996                    quadrant = math.floor(4 * i / sections)
997                    vertices = []
998                    if quadrant == 0:
999                        vertices.append([-half_machine_width, half_machine_depth])
1000                    elif quadrant == 1:
1001                        vertices.append([-half_machine_width, -half_machine_depth])
1002                    elif quadrant == 2:
1003                        vertices.append([half_machine_width, -half_machine_depth])
1004                    elif quadrant == 3:
1005                        vertices.append([half_machine_width, half_machine_depth])
1006                    vertices.append(arc_vertex)
1007
1008                    angle = 2 * math.pi * (i + 1) / sections
1009                    arc_vertex = [-(half_machine_width - border_size) * math.sin(angle), (half_machine_depth - border_size) * math.cos(angle)]
1010                    vertices.append(arc_vertex)
1011
1012                    result[extruder_id].append(Polygon(numpy.array(vertices, numpy.float32)))
1013
1014                if border_size > 0:
1015                    result[extruder_id].append(Polygon(numpy.array([
1016                        [-half_machine_width, -half_machine_depth],
1017                        [-half_machine_width, half_machine_depth],
1018                        [-half_machine_width + border_size, 0]
1019                    ], numpy.float32)))
1020                    result[extruder_id].append(Polygon(numpy.array([
1021                        [-half_machine_width, half_machine_depth],
1022                        [ half_machine_width, half_machine_depth],
1023                        [ 0, half_machine_depth - border_size]
1024                    ], numpy.float32)))
1025                    result[extruder_id].append(Polygon(numpy.array([
1026                        [ half_machine_width, half_machine_depth],
1027                        [ half_machine_width, -half_machine_depth],
1028                        [ half_machine_width - border_size, 0]
1029                    ], numpy.float32)))
1030                    result[extruder_id].append(Polygon(numpy.array([
1031                        [ half_machine_width, -half_machine_depth],
1032                        [-half_machine_width, -half_machine_depth],
1033                        [ 0, -half_machine_depth + border_size]
1034                    ], numpy.float32)))
1035
1036        return result
1037
1038    def _getSettingFromAllExtruders(self, setting_key: str) -> List[Any]:
1039        """Private convenience function to get a setting from every extruder.
1040
1041        For single extrusion machines, this gets the setting from the global stack.
1042
1043        :return: A sequence of setting values, one for each extruder.
1044        """
1045
1046        all_values = ExtruderManager.getInstance().getAllExtruderSettings(setting_key, "value")
1047        all_types = ExtruderManager.getInstance().getAllExtruderSettings(setting_key, "type")
1048        for i, (setting_value, setting_type) in enumerate(zip(all_values, all_types)):
1049            if not setting_value and setting_type in ["int", "float"]:
1050                all_values[i] = 0
1051        return all_values
1052
1053    def _calculateBedAdhesionSize(self, used_extruders, adhesion_override = None):
1054        """Get the bed adhesion size for the global container stack and used extruders
1055
1056        :param adhesion_override: override adhesion type.
1057          Use None to use the global stack default, "none" for no adhesion, "brim" for brim etc.
1058        """
1059        if self._global_container_stack is None:
1060            return None
1061
1062        container_stack = self._global_container_stack
1063        adhesion_type = adhesion_override
1064        if adhesion_type is None:
1065            adhesion_type = container_stack.getProperty("adhesion_type", "value")
1066        skirt_brim_line_width = self._global_container_stack.getProperty("skirt_brim_line_width", "value")
1067        initial_layer_line_width_factor = self._global_container_stack.getProperty("initial_layer_line_width_factor", "value")
1068        # Use brim width if brim is enabled OR the prime tower has a brim.
1069        if adhesion_type == "brim":
1070            brim_line_count = self._global_container_stack.getProperty("brim_line_count", "value")
1071            bed_adhesion_size = skirt_brim_line_width * brim_line_count * initial_layer_line_width_factor / 100.0
1072
1073            for extruder_stack in used_extruders:
1074                bed_adhesion_size += extruder_stack.getProperty("skirt_brim_line_width", "value") * extruder_stack.getProperty("initial_layer_line_width_factor", "value") / 100.0
1075
1076            # We don't create an additional line for the extruder we're printing the brim with.
1077            bed_adhesion_size -= skirt_brim_line_width * initial_layer_line_width_factor / 100.0
1078        elif adhesion_type == "skirt":
1079            skirt_distance = self._global_container_stack.getProperty("skirt_gap", "value")
1080            skirt_line_count = self._global_container_stack.getProperty("skirt_line_count", "value")
1081
1082            bed_adhesion_size = skirt_distance + (
1083                        skirt_brim_line_width * skirt_line_count) * initial_layer_line_width_factor / 100.0
1084
1085            for extruder_stack in used_extruders:
1086                bed_adhesion_size += extruder_stack.getProperty("skirt_brim_line_width", "value") * extruder_stack.getProperty("initial_layer_line_width_factor", "value") / 100.0
1087
1088            # We don't create an additional line for the extruder we're printing the skirt with.
1089            bed_adhesion_size -= skirt_brim_line_width * initial_layer_line_width_factor / 100.0
1090        elif adhesion_type == "raft":
1091            bed_adhesion_size = self._global_container_stack.getProperty("raft_margin", "value")
1092        elif adhesion_type == "none":
1093            bed_adhesion_size = 0
1094        else:
1095            raise Exception("Unknown bed adhesion type. Did you forget to update the build volume calculations for your new bed adhesion type?")
1096
1097        max_length_available = 0.5 * min(
1098            self._global_container_stack.getProperty("machine_width", "value"),
1099            self._global_container_stack.getProperty("machine_depth", "value")
1100        )
1101        bed_adhesion_size = min(bed_adhesion_size, max_length_available)
1102        return bed_adhesion_size
1103
1104    def _calculateFarthestShieldDistance(self, container_stack):
1105        farthest_shield_distance = 0
1106        if container_stack.getProperty("draft_shield_enabled", "value"):
1107            farthest_shield_distance = max(farthest_shield_distance, container_stack.getProperty("draft_shield_dist", "value"))
1108        if container_stack.getProperty("ooze_shield_enabled", "value"):
1109            farthest_shield_distance = max(farthest_shield_distance,container_stack.getProperty("ooze_shield_dist", "value"))
1110        return farthest_shield_distance
1111
1112    def _calculateSupportExpansion(self, container_stack):
1113        support_expansion = 0
1114        support_enabled = self._global_container_stack.getProperty("support_enable", "value")
1115        support_offset = self._global_container_stack.getProperty("support_offset", "value")
1116        if support_enabled and support_offset:
1117            support_expansion += support_offset
1118        return support_expansion
1119
1120    def _calculateMoveFromWallRadius(self, used_extruders):
1121        move_from_wall_radius = 0  # Moves that start from outer wall.
1122
1123        for stack in used_extruders:
1124            if stack.getProperty("travel_avoid_other_parts", "value"):
1125                move_from_wall_radius = max(move_from_wall_radius, stack.getProperty("travel_avoid_distance", "value"))
1126
1127            infill_wipe_distance = stack.getProperty("infill_wipe_dist", "value")
1128            num_walls = stack.getProperty("wall_line_count", "value")
1129            if num_walls >= 1:  # Infill wipes start from the infill, so subtract the total wall thickness from this.
1130                infill_wipe_distance -= stack.getProperty("wall_line_width_0", "value")
1131                if num_walls >= 2:
1132                    infill_wipe_distance -= stack.getProperty("wall_line_width_x", "value") * (num_walls - 1)
1133            move_from_wall_radius = max(move_from_wall_radius, infill_wipe_distance)
1134
1135        return move_from_wall_radius
1136
1137    def getEdgeDisallowedSize(self):
1138        """Calculate the disallowed radius around the edge.
1139
1140        This disallowed radius is to allow for space around the models that is not part of the collision radius,
1141        such as bed adhesion (skirt/brim/raft) and travel avoid distance.
1142        """
1143
1144        if not self._global_container_stack or not self._global_container_stack.extruderList:
1145            return 0
1146
1147        if self._edge_disallowed_size is not None:
1148            return self._edge_disallowed_size
1149
1150        container_stack = self._global_container_stack
1151        used_extruders = ExtruderManager.getInstance().getUsedExtruderStacks()
1152
1153        # If we are printing one at a time, we need to add the bed adhesion size to the disallowed areas of the objects
1154        if container_stack.getProperty("print_sequence", "value") == "one_at_a_time":
1155            return 0.1
1156
1157        bed_adhesion_size = self._calculateBedAdhesionSize(used_extruders)
1158        support_expansion = self._calculateSupportExpansion(self._global_container_stack)
1159        farthest_shield_distance = self._calculateFarthestShieldDistance(self._global_container_stack)
1160        move_from_wall_radius = self._calculateMoveFromWallRadius(used_extruders)
1161
1162        # Now combine our different pieces of data to get the final border size.
1163        # Support expansion is added to the bed adhesion, since the bed adhesion goes around support.
1164        # Support expansion is added to farthest shield distance, since the shields go around support.
1165        self._edge_disallowed_size = max(move_from_wall_radius, support_expansion + farthest_shield_distance, support_expansion + bed_adhesion_size)
1166        return self._edge_disallowed_size
1167
1168    def _clamp(self, value, min_value, max_value):
1169        return max(min(value, max_value), min_value)
1170
1171    _machine_settings = ["machine_width", "machine_depth", "machine_height", "machine_shape", "machine_center_is_zero"]
1172    _skirt_settings = ["adhesion_type", "skirt_gap", "skirt_line_count", "skirt_brim_line_width", "brim_width", "brim_line_count", "raft_margin", "draft_shield_enabled", "draft_shield_dist", "initial_layer_line_width_factor"]
1173    _raft_settings = ["adhesion_type", "raft_base_thickness", "raft_interface_thickness", "raft_surface_layers", "raft_surface_thickness", "raft_airgap", "layer_0_z_overlap"]
1174    _extra_z_settings = ["retraction_hop_enabled", "retraction_hop"]
1175    _prime_settings = ["extruder_prime_pos_x", "extruder_prime_pos_y", "prime_blob_enable"]
1176    _tower_settings = ["prime_tower_enable", "prime_tower_size", "prime_tower_position_x", "prime_tower_position_y", "prime_tower_brim_enable"]
1177    _ooze_shield_settings = ["ooze_shield_enabled", "ooze_shield_dist"]
1178    _distance_settings = ["infill_wipe_dist", "travel_avoid_distance", "support_offset", "support_enable", "travel_avoid_other_parts", "travel_avoid_supports", "wall_line_count", "wall_line_width_0", "wall_line_width_x"]
1179    _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.
1180    _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"]
1181    _disallowed_area_settings = _skirt_settings + _prime_settings + _tower_settings + _ooze_shield_settings + _distance_settings + _extruder_settings
1182