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