123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242 |
- # Copyright (c) 2021 Ultimaker B.V.
- # Cura is released under the terms of the LGPLv3 or higher.
- import numpy
- import math
- from typing import List, Optional, TYPE_CHECKING, Any, Set, cast, Iterable, Dict
- from UM.Logger import Logger
- from UM.Mesh.MeshData import MeshData
- from UM.Mesh.MeshBuilder import MeshBuilder
- from UM.Application import Application #To modify the maximum zoom level.
- from UM.i18n import i18nCatalog
- from UM.Scene.Platform import Platform
- from UM.Scene.Iterator.BreadthFirstIterator import BreadthFirstIterator
- from UM.Scene.SceneNode import SceneNode
- from UM.Resources import Resources
- from UM.Math.Vector import Vector
- from UM.Math.Matrix import Matrix
- from UM.Math.Color import Color
- from UM.Math.AxisAlignedBox import AxisAlignedBox
- from UM.Math.Polygon import Polygon
- from UM.Message import Message
- from UM.Signal import Signal
- from UM.View.RenderBatch import RenderBatch
- from UM.View.GL.OpenGL import OpenGL
- from cura.Settings.GlobalStack import GlobalStack
- from cura.Scene.CuraSceneNode import CuraSceneNode
- from cura.Settings.ExtruderManager import ExtruderManager
- from PyQt6.QtCore import QTimer
- if TYPE_CHECKING:
- from cura.CuraApplication import CuraApplication
- from cura.Settings.ExtruderStack import ExtruderStack
- from UM.Settings.ContainerStack import ContainerStack
- catalog = i18nCatalog("cura")
- # Radius of disallowed area in mm around prime. I.e. how much distance to keep from prime position.
- PRIME_CLEARANCE = 6.5
- class BuildVolume(SceneNode):
- """Build volume is a special kind of node that is responsible for rendering the printable area & disallowed areas."""
- raftThicknessChanged = Signal()
- def __init__(self, application: "CuraApplication", parent: Optional[SceneNode] = None) -> None:
- super().__init__(parent)
- self._application = application
- self._machine_manager = self._application.getMachineManager()
- self._volume_outline_color = None # type: Optional[Color]
- self._x_axis_color = None # type: Optional[Color]
- self._y_axis_color = None # type: Optional[Color]
- self._z_axis_color = None # type: Optional[Color]
- self._disallowed_area_color = None # type: Optional[Color]
- self._error_area_color = None # type: Optional[Color]
- self._width = 0 # type: float
- self._height = 0 # type: float
- self._depth = 0 # type: float
- self._shape = "" # type: str
- self._scale_vector = Vector(1.0, 1.0, 1.0)
- self._shader = None
- self._origin_mesh = None # type: Optional[MeshData]
- self._origin_line_length = 20
- self._origin_line_width = 1
- self._enabled = False
- self._grid_mesh = None # type: Optional[MeshData]
- self._grid_shader = None
- self._disallowed_areas = [] # type: List[Polygon]
- self._disallowed_areas_no_brim = [] # type: List[Polygon]
- self._disallowed_area_mesh = None # type: Optional[MeshData]
- self._disallowed_area_size = 0.
- self._error_areas = [] # type: List[Polygon]
- self._error_mesh = None # type: Optional[MeshData]
- self.setCalculateBoundingBox(False)
- self._volume_aabb = None # type: Optional[AxisAlignedBox]
- self._raft_thickness = 0.0
- self._extra_z_clearance = 0.0
- self._adhesion_type = None # type: Any
- self._platform = Platform(self)
- self._edge_disallowed_size = None
- self._build_volume_message = Message(catalog.i18nc("@info:status",
- "The build volume height has been reduced due to the value of the"
- " \"Print Sequence\" setting to prevent the gantry from colliding"
- " with printed models."),
- title = catalog.i18nc("@info:title", "Build Volume"),
- message_type = Message.MessageType.WARNING)
- self._global_container_stack = None # type: Optional[GlobalStack]
- self._stack_change_timer = QTimer()
- self._stack_change_timer.setInterval(100)
- self._stack_change_timer.setSingleShot(True)
- self._stack_change_timer.timeout.connect(self._onStackChangeTimerFinished)
- self._application.globalContainerStackChanged.connect(self._onStackChanged)
- self._engine_ready = False
- self._application.engineCreatedSignal.connect(self._onEngineCreated)
- self._has_errors = False
- self._application.getController().getScene().sceneChanged.connect(self._onSceneChanged)
- # Objects loaded at the moment. We are connected to the property changed events of these objects.
- self._scene_objects = set() # type: Set[SceneNode]
- # Number of toplevel printable meshes. If there is more than one, the build volume needs to take account of the gantry height in One at a Time printing.
- self._root_printable_object_count = 0
- self._scene_change_timer = QTimer()
- self._scene_change_timer.setInterval(200)
- self._scene_change_timer.setSingleShot(True)
- self._scene_change_timer.timeout.connect(self._onSceneChangeTimerFinished)
- self._setting_change_timer = QTimer()
- self._setting_change_timer.setInterval(150)
- self._setting_change_timer.setSingleShot(True)
- self._setting_change_timer.timeout.connect(self._onSettingChangeTimerFinished)
- # Must be after setting _build_volume_message, apparently that is used in getMachineManager.
- # activeQualityChanged is always emitted after setActiveVariant, setActiveMaterial and setActiveQuality.
- # Therefore this works.
- self._machine_manager.activeQualityChanged.connect(self._onStackChanged)
- # Enable and disable extruder
- self._machine_manager.extruderChanged.connect(self.updateNodeBoundaryCheck)
- # List of settings which were updated
- self._changed_settings_since_last_rebuild = [] # type: List[str]
- def _onSceneChanged(self, source):
- if self._global_container_stack:
- # Ignore anything that is not something we can slice in the first place!
- if source.callDecoration("isSliceable"):
- self._scene_change_timer.start()
- def _onSceneChangeTimerFinished(self):
- root = self._application.getController().getScene().getRoot()
- new_scene_objects = set(node for node in BreadthFirstIterator(root) if node.callDecoration("isSliceable"))
- if new_scene_objects != self._scene_objects:
- for node in new_scene_objects - self._scene_objects: #Nodes that were added to the scene.
- self._updateNodeListeners(node)
- node.decoratorsChanged.connect(self._updateNodeListeners) # Make sure that decoration changes afterwards also receive the same treatment
- for node in self._scene_objects - new_scene_objects: #Nodes that were removed from the scene.
- per_mesh_stack = node.callDecoration("getStack")
- if per_mesh_stack:
- per_mesh_stack.propertyChanged.disconnect(self._onSettingPropertyChanged)
- active_extruder_changed = node.callDecoration("getActiveExtruderChangedSignal")
- if active_extruder_changed is not None:
- node.callDecoration("getActiveExtruderChangedSignal").disconnect(self._updateDisallowedAreasAndRebuild)
- node.decoratorsChanged.disconnect(self._updateNodeListeners)
- self._updateUsedExtruders()
- self.rebuild()
- self._scene_objects = new_scene_objects
- # This also needs to be called when objects are grouped/ungrouped,
- # which is not reflected in a change in self._scene_objects
- self._updateRootPrintableObjectCount()
- def _updateRootPrintableObjectCount(self):
- # Get the number of models in the scene root, excluding modifier meshes and counting grouped models as 1
- root = self._application.getController().getScene().getRoot()
- scene_objects = set(node for node in BreadthFirstIterator(root) if node.callDecoration("isSliceable") or node.callDecoration("isGroup"))
- new_root_printable_object_count = len(list(node for node in scene_objects if node.getParent() == root and not (
- node_stack := node.callDecoration("getStack") and (
- node.callDecoration("getStack").getProperty("anti_overhang_mesh", "value") or
- node.callDecoration("getStack").getProperty("support_mesh", "value") or
- node.callDecoration("getStack").getProperty("cutting_mesh", "value") or
- node.callDecoration("getStack").getProperty("infill_mesh", "value")
- ))
- ))
- if new_root_printable_object_count != self._root_printable_object_count:
- self._root_printable_object_count = new_root_printable_object_count
- self._onSettingPropertyChanged("print_sequence", "value") # Create fake event, so right settings are triggered.
- def _updateNodeListeners(self, node: SceneNode):
- """Updates the listeners that listen for changes in per-mesh stacks.
- :param node: The node for which the decorators changed.
- """
- per_mesh_stack = node.callDecoration("getStack")
- if per_mesh_stack:
- per_mesh_stack.propertyChanged.connect(self._onSettingPropertyChanged)
- active_extruder_changed = node.callDecoration("getActiveExtruderChangedSignal")
- if active_extruder_changed is not None:
- active_extruder_changed.connect(self._nodeActiveExtruderChanged)
- def setWidth(self, width: float) -> None:
- self._width = width
- def getWidth(self) -> float:
- return self._width
- def setHeight(self, height: float) -> None:
- self._height = height
- def getHeight(self) -> float:
- return self._height
- def setDepth(self, depth: float) -> None:
- self._depth = depth
- def getDepth(self) -> float:
- return self._depth
- def setShape(self, shape: str) -> None:
- if shape:
- self._shape = shape
- def getShape(self) -> str:
- return self._shape
- def getDiagonalSize(self) -> float:
- """Get the length of the 3D diagonal through the build volume.
- This gives a sense of the scale of the build volume in general.
- :return: length of the 3D diagonal through the build volume
- """
- return math.sqrt(self._width * self._width + self._height * self._height + self._depth * self._depth)
- def getDisallowedAreas(self) -> List[Polygon]:
- return self._disallowed_areas
- def getDisallowedAreasNoBrim(self) -> List[Polygon]:
- return self._disallowed_areas_no_brim
- def setDisallowedAreas(self, areas: List[Polygon]):
- self._disallowed_areas = areas
- def render(self, renderer):
- if not self.getMeshData() or not self.isVisible():
- return True
- if not self._shader:
- self._shader = OpenGL.getInstance().createShaderProgram(Resources.getPath(Resources.Shaders, "default.shader"))
- self._grid_shader = OpenGL.getInstance().createShaderProgram(Resources.getPath(Resources.Shaders, "grid.shader"))
- theme = self._application.getTheme()
- self._grid_shader.setUniformValue("u_plateColor", Color(*theme.getColor("buildplate").getRgb()))
- self._grid_shader.setUniformValue("u_gridColor0", Color(*theme.getColor("buildplate_grid").getRgb()))
- self._grid_shader.setUniformValue("u_gridColor1", Color(*theme.getColor("buildplate_grid_minor").getRgb()))
- renderer.queueNode(self, mode = RenderBatch.RenderMode.Lines)
- renderer.queueNode(self, mesh = self._origin_mesh, backface_cull = True)
- renderer.queueNode(self, mesh = self._grid_mesh, shader = self._grid_shader, backface_cull = True)
- if self._disallowed_area_mesh:
- renderer.queueNode(self, mesh = self._disallowed_area_mesh, shader = self._shader, transparent = True, backface_cull = True, sort = -9)
- if self._error_mesh:
- renderer.queueNode(self, mesh=self._error_mesh, shader=self._shader, transparent=True,
- backface_cull=True, sort=-8)
- return True
- def updateNodeBoundaryCheck(self):
- """For every sliceable node, update node._outside_buildarea"""
- if not self._global_container_stack:
- return
- root = self._application.getController().getScene().getRoot()
- nodes = cast(List[SceneNode], list(cast(Iterable, BreadthFirstIterator(root))))
- group_nodes = [] # type: List[SceneNode]
- build_volume_bounding_box = self.getBoundingBox()
- if build_volume_bounding_box:
- # It's over 9000!
- # We set this to a very low number, as we do allow models to intersect the build plate.
- # This means the model gets cut off at the build plate.
- build_volume_bounding_box = build_volume_bounding_box.set(bottom=-9001)
- else:
- # No bounding box. This is triggered when running Cura from command line with a model for the first time
- # In that situation there is a model, but no machine (and therefore no build volume.
- return
- for node in nodes:
- # Need to check group nodes later
- if node.callDecoration("isGroup"):
- group_nodes.append(node) # Keep list of affected group_nodes
- if node.callDecoration("isSliceable") or node.callDecoration("isGroup"):
- if not isinstance(node, CuraSceneNode):
- continue
- if node.collidesWithBbox(build_volume_bounding_box):
- node.setOutsideBuildArea(True)
- continue
- if node.collidesWithAreas(self.getDisallowedAreas()):
- node.setOutsideBuildArea(True)
- continue
- # If the entire node is below the build plate, still mark it as outside.
- node_bounding_box = node.getBoundingBox()
- if node_bounding_box and node_bounding_box.top < 0 and not node.getParent().callDecoration("isGroup"):
- node.setOutsideBuildArea(True)
- continue
- # Mark the node as outside build volume if the set extruder is disabled
- extruder_position = node.callDecoration("getActiveExtruderPosition")
- try:
- if not self._global_container_stack.extruderList[int(extruder_position)].isEnabled and not node.callDecoration("isGroup"):
- node.setOutsideBuildArea(True)
- continue
- except IndexError: # Happens when the extruder list is too short. We're not done building the printer in memory yet.
- continue
- except TypeError: # Happens when extruder_position is None. This object has no extruder decoration.
- continue
- node.setOutsideBuildArea(False)
- # Group nodes should override the _outside_buildarea property of their children.
- for group_node in group_nodes:
- children = group_node.getAllChildren()
- # Check if one or more children are non-printable and if so, set the parent as non-printable:
- for child_node in children:
- if child_node.isOutsideBuildArea():
- group_node.setOutsideBuildArea(True)
- break
- # Apply results of the check to all children of the group:
- for child_node in children:
- child_node.setOutsideBuildArea(group_node.isOutsideBuildArea())
- def checkBoundsAndUpdate(self, node: CuraSceneNode, bounds: Optional[AxisAlignedBox] = None) -> None:
- """Update the outsideBuildArea of a single node, given bounds or current build volume
- :param node: single node
- :param bounds: bounds or current build volume
- """
- if not isinstance(node, CuraSceneNode) or self._global_container_stack is None:
- return
- if bounds is None:
- build_volume_bounding_box = self.getBoundingBox()
- if build_volume_bounding_box:
- # It's over 9000!
- build_volume_bounding_box = build_volume_bounding_box.set(bottom=-9001)
- else:
- # No bounding box. This is triggered when running Cura from command line with a model for the first time
- # In that situation there is a model, but no machine (and therefore no build volume.
- return
- else:
- build_volume_bounding_box = bounds
- if node.callDecoration("isSliceable") or node.callDecoration("isGroup"):
- if node.collidesWithBbox(build_volume_bounding_box):
- node.setOutsideBuildArea(True)
- return
- if node.collidesWithAreas(self.getDisallowedAreas()):
- node.setOutsideBuildArea(True)
- return
- # Mark the node as outside build volume if the set extruder is disabled
- extruder_position = node.callDecoration("getActiveExtruderPosition")
- try:
- if not self._global_container_stack.extruderList[int(extruder_position)].isEnabled:
- node.setOutsideBuildArea(True)
- return
- except IndexError:
- # If the extruder doesn't exist, also mark it as unprintable.
- node.setOutsideBuildArea(True)
- return
- node.setOutsideBuildArea(False)
- def _buildGridMesh(self, min_w: float, max_w: float, min_h: float, max_h: float, min_d: float, max_d:float, z_fight_distance: float) -> MeshData:
- mb = MeshBuilder()
- if self._shape != "elliptic":
- # Build plate grid mesh
- mb.addQuad(
- Vector(min_w, min_h - z_fight_distance, min_d),
- Vector(max_w, min_h - z_fight_distance, min_d),
- Vector(max_w, min_h - z_fight_distance, max_d),
- Vector(min_w, min_h - z_fight_distance, max_d)
- )
- for n in range(0, 6):
- v = mb.getVertex(n)
- mb.setVertexUVCoordinates(n, v[0], v[2])
- return mb.build()
- else:
- aspect = 1.0
- scale_matrix = Matrix()
- if self._width != 0:
- # Scale circular meshes by aspect ratio if width != height
- aspect = self._depth / self._width
- scale_matrix.compose(scale=Vector(1, 1, aspect))
- mb.addVertex(0, min_h - z_fight_distance, 0)
- mb.addArc(max_w, Vector.Unit_Y, center=Vector(0, min_h - z_fight_distance, 0))
- sections = mb.getVertexCount() - 1 # Center point is not an arc section
- indices = []
- for n in range(0, sections - 1):
- indices.append([0, n + 2, n + 1])
- mb.addIndices(numpy.asarray(indices, dtype=numpy.int32))
- mb.calculateNormals()
- for n in range(0, mb.getVertexCount()):
- v = mb.getVertex(n)
- mb.setVertexUVCoordinates(n, v[0], v[2] * aspect)
- return mb.build().getTransformed(scale_matrix)
- def _buildMesh(self, min_w: float, max_w: float, min_h: float, max_h: float, min_d: float, max_d:float, z_fight_distance: float) -> MeshData:
- if self._shape != "elliptic":
- # Outline 'cube' of the build volume
- mb = MeshBuilder()
- mb.addLine(Vector(min_w, min_h, min_d), Vector(max_w, min_h, min_d), color = self._volume_outline_color)
- mb.addLine(Vector(min_w, min_h, min_d), Vector(min_w, max_h, min_d), color = self._volume_outline_color)
- mb.addLine(Vector(min_w, max_h, min_d), Vector(max_w, max_h, min_d), color = self._volume_outline_color)
- mb.addLine(Vector(max_w, min_h, min_d), Vector(max_w, max_h, min_d), color = self._volume_outline_color)
- mb.addLine(Vector(min_w, min_h, max_d), Vector(max_w, min_h, max_d), color = self._volume_outline_color)
- mb.addLine(Vector(min_w, min_h, max_d), Vector(min_w, max_h, max_d), color = self._volume_outline_color)
- mb.addLine(Vector(min_w, max_h, max_d), Vector(max_w, max_h, max_d), color = self._volume_outline_color)
- mb.addLine(Vector(max_w, min_h, max_d), Vector(max_w, max_h, max_d), color = self._volume_outline_color)
- mb.addLine(Vector(min_w, min_h, min_d), Vector(min_w, min_h, max_d), color = self._volume_outline_color)
- mb.addLine(Vector(max_w, min_h, min_d), Vector(max_w, min_h, max_d), color = self._volume_outline_color)
- mb.addLine(Vector(min_w, max_h, min_d), Vector(min_w, max_h, max_d), color = self._volume_outline_color)
- mb.addLine(Vector(max_w, max_h, min_d), Vector(max_w, max_h, max_d), color = self._volume_outline_color)
- return mb.build()
- else:
- # Bottom and top 'ellipse' of the build volume
- scale_matrix = Matrix()
- if self._width != 0:
- # Scale circular meshes by aspect ratio if width != height
- aspect = self._depth / self._width
- scale_matrix.compose(scale = Vector(1, 1, aspect))
- mb = MeshBuilder()
- mb.addArc(max_w, Vector.Unit_Y, center = (0, min_h - z_fight_distance, 0), color = self._volume_outline_color)
- mb.addArc(max_w, Vector.Unit_Y, center = (0, max_h, 0), color = self._volume_outline_color)
- return mb.build().getTransformed(scale_matrix)
- def _buildOriginMesh(self, origin: Vector) -> MeshData:
- mb = MeshBuilder()
- mb.addCube(
- width=self._origin_line_length,
- height=self._origin_line_width,
- depth=self._origin_line_width,
- center=origin + Vector(self._origin_line_length / 2, 0, 0),
- color=self._x_axis_color
- )
- mb.addCube(
- width=self._origin_line_width,
- height=self._origin_line_length,
- depth=self._origin_line_width,
- center=origin + Vector(0, self._origin_line_length / 2, 0),
- color=self._y_axis_color
- )
- mb.addCube(
- width=self._origin_line_width,
- height=self._origin_line_width,
- depth=self._origin_line_length,
- center=origin - Vector(0, 0, self._origin_line_length / 2),
- color=self._z_axis_color
- )
- return mb.build()
- def _updateColors(self):
- theme = self._application.getTheme()
- if theme is None:
- return
- self._volume_outline_color = Color(*theme.getColor("volume_outline").getRgb())
- self._x_axis_color = Color(*theme.getColor("x_axis").getRgb())
- self._y_axis_color = Color(*theme.getColor("y_axis").getRgb())
- self._z_axis_color = Color(*theme.getColor("z_axis").getRgb())
- self._disallowed_area_color = Color(*theme.getColor("disallowed_area").getRgb())
- self._error_area_color = Color(*theme.getColor("error_area").getRgb())
- def _buildErrorMesh(self, min_w: float, max_w: float, min_h: float, max_h: float, min_d: float, max_d: float, disallowed_area_height: float) -> Optional[MeshData]:
- if not self._error_areas:
- return None
- mb = MeshBuilder()
- for error_area in self._error_areas:
- color = self._error_area_color
- points = error_area.getPoints()
- first = Vector(self._clamp(points[0][0], min_w, max_w), disallowed_area_height,
- self._clamp(points[0][1], min_d, max_d))
- previous_point = Vector(self._clamp(points[0][0], min_w, max_w), disallowed_area_height,
- self._clamp(points[0][1], min_d, max_d))
- for point in points:
- new_point = Vector(self._clamp(point[0], min_w, max_w), disallowed_area_height,
- self._clamp(point[1], min_d, max_d))
- mb.addFace(first, previous_point, new_point, color=color)
- previous_point = new_point
- return mb.build()
- def _buildDisallowedAreaMesh(self, min_w: float, max_w: float, min_h: float, max_h: float, min_d: float, max_d: float, disallowed_area_height: float) -> Optional[MeshData]:
- if not self._disallowed_areas:
- return None
- bounding_box = Polygon(numpy.array([[min_w, min_d], [min_w, max_d], [max_w, max_d], [max_w, min_d]], numpy.float32))
- mb = MeshBuilder()
- color = self._disallowed_area_color
- for polygon in self._disallowed_areas:
- intersection = polygon.intersectionConvexHulls(bounding_box)
- points = numpy.flipud(intersection.getPoints())
- if len(points) < 3:
- continue
- first = Vector(points[0][0], disallowed_area_height, points[0][1])
- previous_point = Vector(points[1][0], disallowed_area_height, points[1][1])
- for point in points[2:]:
- new_point = Vector(point[0], disallowed_area_height, point[1])
- mb.addFace(first, previous_point, new_point, color=color)
- previous_point = new_point
- # Find the largest disallowed area to exclude it from the maximum scale bounds.
- # This is a very nasty hack. This pretty much only works for UM machines.
- # This disallowed area_size needs a -lot- of rework at some point in the future: TODO
- if numpy.min(points[:,
- 1]) >= 0: # This filters out all areas that have points to the left of the centre. This is done to filter the skirt area.
- size = abs(numpy.max(points[:, 1]) - numpy.min(points[:, 1]))
- else:
- size = 0
- self._disallowed_area_size = max(size, self._disallowed_area_size)
- return mb.build()
- def _updateScaleFactor(self) -> None:
- if not self._global_container_stack:
- return
- scale_xy = 100.0 / max(100.0, self._global_container_stack.getProperty("material_shrinkage_percentage_xy", "value"))
- scale_z = 100.0 / max(100.0, self._global_container_stack.getProperty("material_shrinkage_percentage_z" , "value"))
- self._scale_vector = Vector(scale_xy, scale_xy, scale_z)
- def rebuild(self) -> None:
- """Recalculates the build volume & disallowed areas."""
- if not self._width or not self._height or not self._depth:
- return
- if not self._engine_ready:
- return
- if not self._global_container_stack:
- return
- if not self._volume_outline_color:
- self._updateColors()
- min_w = -self._width / 2
- max_w = self._width / 2
- min_h = 0.0
- max_h = self._height
- min_d = -self._depth / 2
- max_d = self._depth / 2
- z_fight_distance = 0.2 # Distance between buildplate and disallowed area meshes to prevent z-fighting
- self._grid_mesh = self._buildGridMesh(min_w, max_w, min_h, max_h, min_d, max_d, z_fight_distance)
- self.setMeshData(self._buildMesh(min_w, max_w, min_h, max_h, min_d, max_d, z_fight_distance))
- # Indication of the machine origin
- if self._global_container_stack.getProperty("machine_center_is_zero", "value"):
- origin = (Vector(min_w, min_h, min_d) + Vector(max_w, min_h, max_d)) / 2
- else:
- origin = Vector(min_w, min_h, max_d)
- self._origin_mesh = self._buildOriginMesh(origin)
- disallowed_area_height = 0.1
- self._disallowed_area_size = 0.
- self._disallowed_area_mesh = self._buildDisallowedAreaMesh(min_w, max_w, min_h, max_h, min_d, max_d, disallowed_area_height)
- self._error_mesh = self._buildErrorMesh(min_w, max_w, min_h, max_h, min_d, max_d, disallowed_area_height)
- self._updateScaleFactor()
- self._volume_aabb = AxisAlignedBox(
- minimum = Vector(min_w, min_h - 1.0, min_d),
- maximum = Vector(max_w, max_h - self._raft_thickness - self._extra_z_clearance, max_d)
- )
- bed_adhesion_size = self.getEdgeDisallowedSize()
- # As this works better for UM machines, we only add the disallowed_area_size for the z direction.
- # This is probably wrong in all other cases. TODO!
- # The +1 and -1 is added as there is always a bit of extra room required to work properly.
- scale_to_max_bounds = AxisAlignedBox(
- minimum = Vector(min_w + bed_adhesion_size + 1, min_h, min_d + self._disallowed_area_size - bed_adhesion_size + 1),
- maximum = Vector(max_w - bed_adhesion_size - 1, max_h - self._raft_thickness - self._extra_z_clearance, max_d - self._disallowed_area_size + bed_adhesion_size - 1)
- )
- self._application.getController().getScene()._maximum_bounds = scale_to_max_bounds # type: ignore
- self.updateNodeBoundaryCheck()
- def getBoundingBox(self) -> Optional[AxisAlignedBox]:
- return self._volume_aabb
- def getRaftThickness(self) -> float:
- return self._raft_thickness
- def _updateRaftThickness(self) -> None:
- if not self._global_container_stack:
- return
- old_raft_thickness = self._raft_thickness
- if self._global_container_stack.extruderList:
- # This might be called before the extruder stacks have initialised, in which case getting the adhesion_type fails
- self._adhesion_type = self._global_container_stack.getProperty("adhesion_type", "value")
- self._raft_thickness = 0.0
- if self._adhesion_type == "raft":
- self._raft_thickness = (
- self._global_container_stack.getProperty("raft_base_thickness", "value") +
- self._global_container_stack.getProperty("raft_interface_layers", "value") *
- self._global_container_stack.getProperty("raft_interface_thickness", "value") +
- self._global_container_stack.getProperty("raft_surface_layers", "value") *
- self._global_container_stack.getProperty("raft_surface_thickness", "value") +
- self._global_container_stack.getProperty("raft_airgap", "value") -
- self._global_container_stack.getProperty("layer_0_z_overlap", "value"))
- # Rounding errors do not matter, we check if raft_thickness has changed at all
- if old_raft_thickness != self._raft_thickness:
- self.setPosition(Vector(0, -self._raft_thickness, 0), SceneNode.TransformSpace.World)
- self.raftThicknessChanged.emit()
- def _calculateExtraZClearance(self, extruders: List["ContainerStack"]) -> float:
- if not self._global_container_stack:
- return 0
- extra_z = 0.0
- for extruder in extruders:
- if extruder.getProperty("retraction_hop_enabled", "value"):
- retraction_hop = extruder.getProperty("retraction_hop", "value")
- if extra_z is None or retraction_hop > extra_z:
- extra_z = retraction_hop
- return extra_z
- def _onStackChanged(self):
- self._stack_change_timer.start()
- def _onStackChangeTimerFinished(self) -> None:
- """Update the build volume visualization"""
- if self._global_container_stack:
- self._global_container_stack.propertyChanged.disconnect(self._onSettingPropertyChanged)
- extruders = ExtruderManager.getInstance().getActiveExtruderStacks()
- for extruder in extruders:
- extruder.propertyChanged.disconnect(self._onSettingPropertyChanged)
- self._global_container_stack = self._application.getGlobalContainerStack()
- if self._global_container_stack:
- self._global_container_stack.propertyChanged.connect(self._onSettingPropertyChanged)
- extruders = ExtruderManager.getInstance().getActiveExtruderStacks()
- for extruder in extruders:
- extruder.propertyChanged.connect(self._onSettingPropertyChanged)
- self._width = self._global_container_stack.getProperty("machine_width", "value")
- machine_height = self._global_container_stack.getProperty("machine_height", "value")
- if self._global_container_stack.getProperty("print_sequence", "value") == "one_at_a_time" and self._root_printable_object_count > 1:
- new_height = min(self._global_container_stack.getProperty("gantry_height", "value") * self._scale_vector.z, machine_height)
- if self._height > new_height:
- self._build_volume_message.show()
- elif self._height < new_height:
- self._build_volume_message.hide()
- self._height = new_height
- else:
- self._height = self._global_container_stack.getProperty("machine_height", "value")
- self._build_volume_message.hide()
- self._depth = self._global_container_stack.getProperty("machine_depth", "value")
- self._shape = self._global_container_stack.getProperty("machine_shape", "value")
- self._updateUsedExtruders()
- self._updateDisallowedAreas()
- self._updateRaftThickness()
- self._extra_z_clearance = self._calculateExtraZClearance(ExtruderManager.getInstance().getUsedExtruderStacks())
- if self._engine_ready:
- self.rebuild()
- camera = Application.getInstance().getController().getCameraTool()
- if camera:
- diagonal = self.getDiagonalSize()
- if diagonal > 1:
- # You can zoom out up to 5 times the diagonal. This gives some space around the volume.
- camera.setZoomRange(min = 0.1, max = diagonal * 5) # type: ignore
- def _onEngineCreated(self) -> None:
- self._engine_ready = True
- self.rebuild()
- def _onSettingChangeTimerFinished(self) -> None:
- if not self._global_container_stack:
- return
- rebuild_me = False
- update_disallowed_areas = False
- update_raft_thickness = False
- update_extra_z_clearance = True
- update_used_extruders = False
- for setting_key in self._changed_settings_since_last_rebuild:
- if setting_key in ["print_sequence", "support_mesh", "infill_mesh", "cutting_mesh", "anti_overhang_mesh"]:
- self._updateRootPrintableObjectCount()
- if setting_key == "print_sequence":
- machine_height = self._global_container_stack.getProperty("machine_height", "value")
- if self._application.getGlobalContainerStack().getProperty("print_sequence", "value") == "one_at_a_time" and self._root_printable_object_count > 1:
- new_height = min(
- self._global_container_stack.getProperty("gantry_height", "value") * self._scale_vector.z,
- machine_height)
- if self._height > new_height:
- self._build_volume_message.show()
- elif self._height < new_height:
- self._build_volume_message.hide()
- self._height = new_height
- else:
- self._height = self._global_container_stack.getProperty("machine_height", "value") * self._scale_vector.z
- self._build_volume_message.hide()
- update_disallowed_areas = True
- # sometimes the machine size or shape settings are adjusted on the active machine, we should reflect this
- if setting_key in self._machine_settings or setting_key in self._material_size_settings:
- self._updateMachineSizeProperties()
- update_extra_z_clearance = True
- update_disallowed_areas = True
- if setting_key in self._disallowed_area_settings:
- update_disallowed_areas = True
- if setting_key in self._raft_settings:
- update_raft_thickness = True
- update_used_extruders = True
- if setting_key in self._extra_z_settings:
- update_extra_z_clearance = True
- if setting_key in self._limit_to_extruder_settings:
- update_disallowed_areas = True
- update_used_extruders = True
- if setting_key in self._extruder_settings:
- update_used_extruders = True
- rebuild_me = update_extra_z_clearance or update_disallowed_areas or update_raft_thickness
- # We only want to update all of them once.
- if update_disallowed_areas:
- self._updateDisallowedAreas()
- if update_raft_thickness:
- self._updateRaftThickness()
- if update_extra_z_clearance:
- self._extra_z_clearance = self._calculateExtraZClearance(ExtruderManager.getInstance().getUsedExtruderStacks())
- if update_used_extruders:
- self._updateUsedExtruders()
- if rebuild_me:
- self.rebuild()
- # We just did a rebuild, reset the list.
- self._changed_settings_since_last_rebuild = []
- def _onSettingPropertyChanged(self, setting_key: str, property_name: str) -> None:
- if property_name != "value":
- return
- if setting_key not in self._changed_settings_since_last_rebuild:
- self._changed_settings_since_last_rebuild.append(setting_key)
- self._setting_change_timer.start()
- def hasErrors(self) -> bool:
- return self._has_errors
- def _updateMachineSizeProperties(self) -> None:
- if not self._global_container_stack:
- return
- self._updateScaleFactor()
- self._height = self._global_container_stack.getProperty("machine_height", "value") * self._scale_vector.z
- self._width = self._global_container_stack.getProperty("machine_width", "value")
- self._depth = self._global_container_stack.getProperty("machine_depth", "value")
- self._shape = self._global_container_stack.getProperty("machine_shape", "value")
- def _updateUsedExtruders(self):
- global_container_stack = self._application.getGlobalContainerStack()
- if not global_container_stack:
- return
- used_extruders = ExtruderManager.getInstance().getUsedExtruderStacks()
- for extruder in global_container_stack.extruderList:
- used = extruder in used_extruders
- extruder.definitionChanges.setProperty("extruder_used", "value", used)
- global_container_stack.definitionChanges.setProperty("extruders_used", "value", [extruder.position for extruder in used_extruders])
- def _nodeActiveExtruderChanged(self):
- self._updateDisallowedAreasAndRebuild()
- self._updateUsedExtruders()
- def _updateDisallowedAreasAndRebuild(self):
- """Calls :py:meth:`cura.BuildVolume._updateDisallowedAreas` and makes sure the changes appear in the scene.
- This is required for a signal to trigger the update in one go. The
- :py:meth:`cura.BuildVolume._updateDisallowedAreas` method itself shouldn't call
- :py:meth:`cura.BuildVolume.rebuild`, since there may be other changes before it needs to be rebuilt,
- which would hit performance.
- """
- self._updateDisallowedAreas()
- self._updateRaftThickness()
- self._extra_z_clearance = self._calculateExtraZClearance(ExtruderManager.getInstance().getUsedExtruderStacks())
- self.rebuild()
- def _updateDisallowedAreas(self) -> None:
- if not self._global_container_stack:
- return
- self._error_areas = []
- used_extruders = ExtruderManager.getInstance().getUsedExtruderStacks()
- self._edge_disallowed_size = None # Force a recalculation
- disallowed_border_size = self.getEdgeDisallowedSize()
- result_areas = self._computeDisallowedAreasStatic(disallowed_border_size, used_extruders) # Normal machine disallowed areas can always be added.
- prime_areas = self._computeDisallowedAreasPrimeBlob(disallowed_border_size, used_extruders)
- 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.
- # Check if prime positions intersect with disallowed areas.
- for extruder in used_extruders:
- extruder_id = extruder.getId()
- result_areas[extruder_id].extend(prime_areas[extruder_id])
- result_areas_no_brim[extruder_id].extend(prime_areas[extruder_id])
- nozzle_disallowed_areas = extruder.getProperty("nozzle_disallowed_areas", "value")
- for area in nozzle_disallowed_areas:
- polygon = Polygon(numpy.array(area, numpy.float32))
- polygon_disallowed_border = polygon.getMinkowskiHull(Polygon.approximatedCircle(disallowed_border_size))
- result_areas[extruder_id].append(polygon_disallowed_border) # Don't perform the offset on these.
- result_areas_no_brim[extruder_id].append(polygon) # No brim
- # Add prime tower location as disallowed area.
- if len([x for x in used_extruders if x.isEnabled]) > 1: # No prime tower if only one extruder is enabled
- prime_tower_collision = False
- prime_tower_areas = self._computeDisallowedAreasPrinted(used_extruders)
- for extruder_id in prime_tower_areas:
- for area_index, prime_tower_area in enumerate(prime_tower_areas[extruder_id]):
- for area in result_areas_no_brim[extruder_id]:
- if prime_tower_area.intersectsPolygon(area) is not None:
- prime_tower_collision = True
- break
- if prime_tower_collision: # Already found a collision.
- break
- if not prime_tower_collision:
- result_areas[extruder_id].extend(prime_tower_areas[extruder_id])
- result_areas_no_brim[extruder_id].extend(prime_tower_areas[extruder_id])
- else:
- self._error_areas.extend(prime_tower_areas[extruder_id])
- self._has_errors = len(self._error_areas) > 0
- self._disallowed_areas = []
- for extruder_id in result_areas:
- self._disallowed_areas.extend(result_areas[extruder_id])
- self._disallowed_areas_no_brim = []
- for extruder_id in result_areas_no_brim:
- self._disallowed_areas_no_brim.extend(result_areas_no_brim[extruder_id])
- def _computeDisallowedAreasPrinted(self, used_extruders):
- """Computes the disallowed areas for objects that are printed with print features.
- This means that the brim, travel avoidance and such will be applied to these features.
- :return: A dictionary with for each used extruder ID the disallowed areas where that extruder may not print.
- """
- result = {}
- skirt_brim_extruder: ExtruderStack = None
- skirt_brim_extruder_nr = self._global_container_stack.getProperty("skirt_brim_extruder_nr", "value")
- for extruder in used_extruders:
- if skirt_brim_extruder_nr == -1:
- skirt_brim_extruder = used_extruders[0] # The prime tower brim is always printed with the first extruder
- elif int(extruder.getProperty("extruder_nr", "value")) == int(skirt_brim_extruder_nr):
- skirt_brim_extruder = extruder
- result[extruder.getId()] = []
- # Currently, the only normally printed object is the prime tower.
- if self._global_container_stack.getProperty("prime_tower_enable", "value"):
- prime_tower_size = self._global_container_stack.getProperty("prime_tower_size", "value")
- machine_width = self._global_container_stack.getProperty("machine_width", "value")
- machine_depth = self._global_container_stack.getProperty("machine_depth", "value")
- prime_tower_x = self._global_container_stack.getProperty("prime_tower_position_x", "value")
- prime_tower_y = - self._global_container_stack.getProperty("prime_tower_position_y", "value")
- prime_tower_brim_enable = self._global_container_stack.getProperty("prime_tower_brim_enable", "value")
- prime_tower_base_size = self._global_container_stack.getProperty("prime_tower_base_size", "value")
- prime_tower_base_height = self._global_container_stack.getProperty("prime_tower_base_height", "value")
- adhesion_type = self._global_container_stack.getProperty("adhesion_type", "value")
- if not self._global_container_stack.getProperty("machine_center_is_zero", "value"):
- prime_tower_x = prime_tower_x - machine_width / 2 #Offset by half machine_width and _depth to put the origin in the front-left.
- prime_tower_y = prime_tower_y + machine_depth / 2
- radius = prime_tower_size / 2
- delta_x = -radius
- delta_y = -radius
- if prime_tower_base_size > 0 and ((prime_tower_brim_enable and prime_tower_base_height > 0) or adhesion_type == "raft"):
- radius += prime_tower_base_size
- prime_tower_area = Polygon.approximatedCircle(radius, num_segments = 32)
- prime_tower_area = prime_tower_area.translate(prime_tower_x + delta_x, prime_tower_y + delta_y)
- prime_tower_area = prime_tower_area.getMinkowskiHull(Polygon.approximatedCircle(0))
- for extruder in used_extruders:
- result[extruder.getId()].append(prime_tower_area) #The prime tower location is the same for each extruder, regardless of offset.
- return result
- def _computeDisallowedAreasPrimeBlob(self, border_size: float, used_extruders: List["ExtruderStack"]) -> Dict[str, List[Polygon]]:
- """Computes the disallowed areas for the prime blobs.
- These are special because they are not subject to things like brim or travel avoidance. They do get a dilute
- with the border size though because they may not intersect with brims and such of other objects.
- :param border_size: The size with which to offset the disallowed areas due to skirt, brim, travel avoid distance
- , etc.
- :param used_extruders: The extruder stacks to generate disallowed areas for.
- :return: A dictionary with for each used extruder ID the prime areas.
- """
- result = {} # type: Dict[str, List[Polygon]]
- if not self._global_container_stack:
- return result
- machine_width = self._global_container_stack.getProperty("machine_width", "value")
- machine_depth = self._global_container_stack.getProperty("machine_depth", "value")
- for extruder in used_extruders:
- prime_blob_enabled = extruder.getProperty("prime_blob_enable", "value")
- prime_x = extruder.getProperty("extruder_prime_pos_x", "value")
- prime_y = -extruder.getProperty("extruder_prime_pos_y", "value")
- # Ignore extruder prime position if it is not set or if blob is disabled
- if (prime_x == 0 and prime_y == 0) or not prime_blob_enabled:
- result[extruder.getId()] = []
- continue
- if not self._global_container_stack.getProperty("machine_center_is_zero", "value"):
- prime_x = prime_x - machine_width / 2 # Offset by half machine_width and _depth to put the origin in the front-left.
- prime_y = prime_y + machine_depth / 2
- prime_polygon = Polygon.approximatedCircle(PRIME_CLEARANCE)
- prime_polygon = prime_polygon.getMinkowskiHull(Polygon.approximatedCircle(border_size))
- prime_polygon = prime_polygon.translate(prime_x, prime_y)
- result[extruder.getId()] = [prime_polygon]
- return result
- def _computeDisallowedAreasStatic(self, border_size:float, used_extruders: List["ExtruderStack"]) -> Dict[str, List[Polygon]]:
- """Computes the disallowed areas that are statically placed in the machine.
- It computes different disallowed areas depending on the offset of the extruder. The resulting dictionary will
- therefore have an entry for each extruder that is used.
- :param border_size: The size with which to offset the disallowed areas due to skirt, brim, travel avoid distance
- , etc.
- :param used_extruders: The extruder stacks to generate disallowed areas for.
- :return: A dictionary with for each used extruder ID the disallowed areas where that extruder may not print.
- """
- # Convert disallowed areas to polygons and dilate them.
- machine_disallowed_polygons = []
- if self._global_container_stack is None:
- return {}
- for area in self._global_container_stack.getProperty("machine_disallowed_areas", "value"):
- if len(area) == 0:
- continue # Numpy doesn't deal well with 0-length arrays, since it can't determine the dimensionality of them.
- polygon = Polygon(numpy.array(area, numpy.float32))
- polygon = polygon.getMinkowskiHull(Polygon.approximatedCircle(border_size))
- machine_disallowed_polygons.append(polygon)
- # For certain machines we don't need to compute disallowed areas for each nozzle.
- # So we check here and only do the nozzle offsetting if needed.
- nozzle_offsetting_for_disallowed_areas = self._global_container_stack.getMetaDataEntry(
- "nozzle_offsetting_for_disallowed_areas", True)
- result = {} # type: Dict[str, List[Polygon]]
- for extruder in used_extruders:
- extruder_id = extruder.getId()
- offset_x = extruder.getProperty("machine_nozzle_offset_x", "value")
- if offset_x is None:
- offset_x = 0
- offset_y = extruder.getProperty("machine_nozzle_offset_y", "value")
- if offset_y is None:
- offset_y = 0
- offset_y = -offset_y # Y direction of g-code is the inverse of Y direction of Cura's scene space.
- result[extruder_id] = []
- for polygon in machine_disallowed_polygons:
- result[extruder_id].append(polygon.translate(offset_x, offset_y)) # Compensate for the nozzle offset of this extruder.
- # Add the border around the edge of the build volume.
- left_unreachable_border = 0
- right_unreachable_border = 0
- top_unreachable_border = 0
- bottom_unreachable_border = 0
- # Only do nozzle offsetting if needed
- if nozzle_offsetting_for_disallowed_areas:
- # The build volume is defined as the union of the area that all extruders can reach, so we need to know
- # the relative offset to all extruders.
- for other_extruder in ExtruderManager.getInstance().getActiveExtruderStacks():
- other_offset_x = other_extruder.getProperty("machine_nozzle_offset_x", "value")
- if other_offset_x is None:
- other_offset_x = 0
- other_offset_y = other_extruder.getProperty("machine_nozzle_offset_y", "value")
- if other_offset_y is None:
- other_offset_y = 0
- other_offset_y = -other_offset_y
- left_unreachable_border = min(left_unreachable_border, other_offset_x - offset_x)
- right_unreachable_border = max(right_unreachable_border, other_offset_x - offset_x)
- top_unreachable_border = min(top_unreachable_border, other_offset_y - offset_y)
- bottom_unreachable_border = max(bottom_unreachable_border, other_offset_y - offset_y)
- half_machine_width = self._global_container_stack.getProperty("machine_width", "value") / 2
- half_machine_depth = self._global_container_stack.getProperty("machine_depth", "value") / 2
- # We need at a minimum a very small border around the edge so that models can't go off the build plate
- border_size = max(border_size, 0.1)
- if self._shape != "elliptic":
- if border_size - left_unreachable_border > 0:
- result[extruder_id].append(Polygon(numpy.array([
- [-half_machine_width, -half_machine_depth],
- [-half_machine_width, half_machine_depth],
- [-half_machine_width + border_size - left_unreachable_border, half_machine_depth - border_size - bottom_unreachable_border],
- [-half_machine_width + border_size - left_unreachable_border, -half_machine_depth + border_size - top_unreachable_border]
- ], numpy.float32)))
- if border_size + right_unreachable_border > 0:
- result[extruder_id].append(Polygon(numpy.array([
- [half_machine_width, half_machine_depth],
- [half_machine_width, -half_machine_depth],
- [half_machine_width - border_size - right_unreachable_border, -half_machine_depth + border_size - top_unreachable_border],
- [half_machine_width - border_size - right_unreachable_border, half_machine_depth - border_size - bottom_unreachable_border]
- ], numpy.float32)))
- if border_size + bottom_unreachable_border > 0:
- result[extruder_id].append(Polygon(numpy.array([
- [-half_machine_width, half_machine_depth],
- [half_machine_width, half_machine_depth],
- [half_machine_width - border_size - right_unreachable_border, half_machine_depth - border_size - bottom_unreachable_border],
- [-half_machine_width + border_size - left_unreachable_border, half_machine_depth - border_size - bottom_unreachable_border]
- ], numpy.float32)))
- if border_size - top_unreachable_border > 0:
- result[extruder_id].append(Polygon(numpy.array([
- [half_machine_width, -half_machine_depth],
- [-half_machine_width, -half_machine_depth],
- [-half_machine_width + border_size - left_unreachable_border, -half_machine_depth + border_size - top_unreachable_border],
- [half_machine_width - border_size - right_unreachable_border, -half_machine_depth + border_size - top_unreachable_border]
- ], numpy.float32)))
- else:
- sections = 32
- arc_vertex = [0, half_machine_depth - border_size]
- for i in range(0, sections):
- quadrant = math.floor(4 * i / sections)
- vertices = []
- if quadrant == 0:
- vertices.append([-half_machine_width, half_machine_depth])
- elif quadrant == 1:
- vertices.append([-half_machine_width, -half_machine_depth])
- elif quadrant == 2:
- vertices.append([half_machine_width, -half_machine_depth])
- elif quadrant == 3:
- vertices.append([half_machine_width, half_machine_depth])
- vertices.append(arc_vertex)
- angle = 2 * math.pi * (i + 1) / sections
- arc_vertex = [-(half_machine_width - border_size) * math.sin(angle), (half_machine_depth - border_size) * math.cos(angle)]
- vertices.append(arc_vertex)
- result[extruder_id].append(Polygon(numpy.array(vertices, numpy.float32)))
- if border_size > 0:
- result[extruder_id].append(Polygon(numpy.array([
- [-half_machine_width, -half_machine_depth],
- [-half_machine_width, half_machine_depth],
- [-half_machine_width + border_size, 0]
- ], numpy.float32)))
- result[extruder_id].append(Polygon(numpy.array([
- [-half_machine_width, half_machine_depth],
- [ half_machine_width, half_machine_depth],
- [ 0, half_machine_depth - border_size]
- ], numpy.float32)))
- result[extruder_id].append(Polygon(numpy.array([
- [ half_machine_width, half_machine_depth],
- [ half_machine_width, -half_machine_depth],
- [ half_machine_width - border_size, 0]
- ], numpy.float32)))
- result[extruder_id].append(Polygon(numpy.array([
- [ half_machine_width, -half_machine_depth],
- [-half_machine_width, -half_machine_depth],
- [ 0, -half_machine_depth + border_size]
- ], numpy.float32)))
- return result
- def _getSettingFromAllExtruders(self, setting_key: str) -> List[Any]:
- """Private convenience function to get a setting from every extruder.
- For single extrusion machines, this gets the setting from the global stack.
- :return: A sequence of setting values, one for each extruder.
- """
- all_values = ExtruderManager.getInstance().getAllExtruderSettings(setting_key, "value")
- all_types = ExtruderManager.getInstance().getAllExtruderSettings(setting_key, "type")
- for i, (setting_value, setting_type) in enumerate(zip(all_values, all_types)):
- if not setting_value and setting_type in ["int", "float"]:
- all_values[i] = 0
- return all_values
- def _calculateBedAdhesionSize(self, used_extruders):
- """Get the bed adhesion size for the global container stack and used extruders
- :param adhesion_override: override adhesion type.
- Use None to use the global stack default, "none" for no adhesion, "brim" for brim etc.
- """
- if self._global_container_stack is None:
- return None
- container_stack = self._global_container_stack
- adhesion_type = container_stack.getProperty("adhesion_type", "value")
- if adhesion_type == "raft":
- bed_adhesion_size = self._global_container_stack.getProperty("raft_margin", "value") # Should refer to the raft extruder if set.
- else: # raft, brim or skirt. Those last two are handled by CuraEngine.
- bed_adhesion_size = 0
- max_length_available = 0.5 * min(
- self._global_container_stack.getProperty("machine_width", "value"),
- self._global_container_stack.getProperty("machine_depth", "value")
- )
- bed_adhesion_size = min(bed_adhesion_size, max_length_available)
- return bed_adhesion_size
- def _calculateFarthestShieldDistance(self, container_stack):
- farthest_shield_distance = 0
- if container_stack.getProperty("draft_shield_enabled", "value"):
- farthest_shield_distance = max(farthest_shield_distance, container_stack.getProperty("draft_shield_dist", "value"))
- if container_stack.getProperty("ooze_shield_enabled", "value"):
- farthest_shield_distance = max(farthest_shield_distance,container_stack.getProperty("ooze_shield_dist", "value"))
- return farthest_shield_distance
- def _calculateSupportExpansion(self, container_stack):
- support_expansion = 0
- support_enabled = self._global_container_stack.getProperty("support_enable", "value")
- support_offset = self._global_container_stack.getProperty("support_offset", "value")
- if support_enabled and support_offset:
- support_expansion += support_offset
- return support_expansion
- def _calculateMoveFromWallRadius(self, used_extruders):
- move_from_wall_radius = 0 # Moves that start from outer wall.
- for stack in used_extruders:
- if stack.getProperty("travel_avoid_other_parts", "value"):
- move_from_wall_radius = max(move_from_wall_radius, stack.getProperty("travel_avoid_distance", "value"))
- infill_wipe_distance = stack.getProperty("infill_wipe_dist", "value")
- num_walls = stack.getProperty("wall_line_count", "value")
- if num_walls >= 1: # Infill wipes start from the infill, so subtract the total wall thickness from this.
- infill_wipe_distance -= stack.getProperty("wall_line_width_0", "value")
- if num_walls >= 2:
- infill_wipe_distance -= stack.getProperty("wall_line_width_x", "value") * (num_walls - 1)
- move_from_wall_radius = max(move_from_wall_radius, infill_wipe_distance)
- return move_from_wall_radius
- def getEdgeDisallowedSize(self):
- """Calculate the disallowed radius around the edge.
- This disallowed radius is to allow for space around the models that is not part of the collision radius,
- such as bed adhesion (skirt/brim/raft) and travel avoid distance.
- """
- if not self._global_container_stack or not self._global_container_stack.extruderList:
- return 0
- if self._edge_disallowed_size is not None:
- return self._edge_disallowed_size
- container_stack = self._global_container_stack
- used_extruders = ExtruderManager.getInstance().getUsedExtruderStacks()
- # If we are printing one at a time, we need to add the bed adhesion size to the disallowed areas of the objects
- if container_stack.getProperty("print_sequence", "value") == "one_at_a_time":
- return 0.1
- bed_adhesion_size = self._calculateBedAdhesionSize(used_extruders)
- support_expansion = self._calculateSupportExpansion(self._global_container_stack)
- farthest_shield_distance = self._calculateFarthestShieldDistance(self._global_container_stack)
- move_from_wall_radius = self._calculateMoveFromWallRadius(used_extruders)
- # Now combine our different pieces of data to get the final border size.
- # Support expansion is added to the bed adhesion, since the bed adhesion goes around support.
- # Support expansion is added to farthest shield distance, since the shields go around support.
- self._edge_disallowed_size = max(move_from_wall_radius, support_expansion + farthest_shield_distance, support_expansion + bed_adhesion_size)
- return self._edge_disallowed_size
- def _clamp(self, value, min_value, max_value):
- return max(min(value, max_value), min_value)
- _machine_settings = ["machine_width", "machine_depth", "machine_height", "machine_shape", "machine_center_is_zero"]
- _skirt_settings = ["adhesion_type", "skirt_gap", "skirt_line_count", "skirt_brim_line_width", "brim_gap", "brim_width", "brim_line_count", "raft_margin", "draft_shield_enabled", "draft_shield_dist", "initial_layer_line_width_factor"]
- _raft_settings = ["adhesion_type", "raft_base_thickness", "raft_interface_layers", "raft_interface_thickness", "raft_surface_layers", "raft_surface_thickness", "raft_airgap", "layer_0_z_overlap"]
- _extra_z_settings = ["retraction_hop_enabled", "retraction_hop"]
- _prime_settings = ["extruder_prime_pos_x", "extruder_prime_pos_y", "prime_blob_enable"]
- _tower_settings = ["prime_tower_enable", "prime_tower_size", "prime_tower_position_x", "prime_tower_position_y", "prime_tower_brim_enable", "prime_tower_base_size", "prime_tower_base_height"]
- _ooze_shield_settings = ["ooze_shield_enabled", "ooze_shield_dist"]
- _distance_settings = ["infill_wipe_dist", "travel_avoid_distance", "support_offset", "support_enable", "travel_avoid_other_parts", "travel_avoid_supports", "wall_line_count", "wall_line_width_0", "wall_line_width_x"]
- _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", "skirt_brim_extruder_nr", "raft_base_extruder_nr", "raft_interface_extruder_nr", "raft_surface_extruder_nr", "adhesion_type"] #Settings that can affect which extruders are used.
- _limit_to_extruder_settings = ["wall_extruder_nr", "wall_0_extruder_nr", "wall_x_extruder_nr", "top_bottom_extruder_nr", "infill_extruder_nr", "support_infill_extruder_nr", "support_extruder_nr_layer_0", "support_bottom_extruder_nr", "support_roof_extruder_nr", "skirt_brim_extruder_nr", "raft_base_extruder_nr", "raft_interface_extruder_nr", "raft_surface_extruder_nr"]
- _material_size_settings = ["material_shrinkage_percentage", "material_shrinkage_percentage_xy", "material_shrinkage_percentage_z"]
- _disallowed_area_settings = _skirt_settings + _prime_settings + _tower_settings + _ooze_shield_settings + _distance_settings + _extruder_settings + _material_size_settings
|