diff --git a/cura/BuildVolume.py b/cura/BuildVolume.py index 901a8182ef..1709a67dd5 100644 --- a/cura/BuildVolume.py +++ b/cura/BuildVolume.py @@ -3,6 +3,7 @@ from cura.Settings.ExtruderManager import ExtruderManager from UM.i18n import i18nCatalog +from UM.Scene.Platform import Platform from UM.Scene.SceneNode import SceneNode from UM.Application import Application from UM.Resources import Resources @@ -18,12 +19,32 @@ from UM.View.GL.OpenGL import OpenGL catalog = i18nCatalog("cura") import numpy +import copy # Setting for clearance around the prime PRIME_CLEARANCE = 10 +def approximatedCircleVertices(r): + """ + Return vertices from an approximated circle. + :param r: radius + :return: numpy 2-array with the vertices + """ + + return numpy.array([ + [-r, 0], + [-r * 0.707, r * 0.707], + [0, r], + [r * 0.707, r * 0.707], + [r, 0], + [r * 0.707, -r * 0.707], + [0, -r], + [-r * 0.707, -r * 0.707] + ], numpy.float32) + + ## Build volume is a special kind of node that is responsible for rendering the printable area & disallowed areas. class BuildVolume(SceneNode): VolumeOutlineColor = Color(12, 169, 227, 255) @@ -46,6 +67,11 @@ class BuildVolume(SceneNode): self.setCalculateBoundingBox(False) self._volume_aabb = None + self._raft_thickness = 0.0 + self._adhesion_type = None + self._raft_mesh = None + self._platform = Platform(self) + self._active_container_stack = None Application.getInstance().globalContainerStackChanged.connect(self._onGlobalContainerStackChanged) self._onGlobalContainerStackChanged() @@ -77,6 +103,9 @@ class BuildVolume(SceneNode): 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._raft_mesh and self._adhesion_type == "raft": + renderer.queueNode(self, mesh=self._raft_mesh, transparent=True, backface_cull=True, sort=-9) + return True ## Recalculates the build volume & disallowed areas. @@ -93,6 +122,7 @@ class BuildVolume(SceneNode): mb = MeshBuilder() + # Outline 'cube' of the build volume mb.addLine(Vector(min_w, min_h, min_d), Vector(max_w, min_h, min_d), color = self.VolumeOutlineColor) mb.addLine(Vector(min_w, min_h, min_d), Vector(min_w, max_h, min_d), color = self.VolumeOutlineColor) mb.addLine(Vector(min_w, max_h, min_d), Vector(max_w, max_h, min_d), color = self.VolumeOutlineColor) @@ -117,11 +147,23 @@ class BuildVolume(SceneNode): Vector(max_w, min_h - 0.2, max_d), Vector(min_w, min_h - 0.2, max_d) ) + for n in range(0, 6): v = mb.getVertex(n) mb.setVertexUVCoordinates(n, v[0], v[2]) self._grid_mesh = mb.build() + # Build raft mesh: a plane on the height of the raft. + mb = MeshBuilder() + mb.addQuad( + Vector(min_w, self._raft_thickness, min_d), + Vector(max_w, self._raft_thickness, min_d), + Vector(max_w, self._raft_thickness, max_d), + Vector(min_w, self._raft_thickness, max_d), + color=Color(128, 128, 128, 64) + ) + self._raft_mesh = mb.build() + disallowed_area_height = 0.1 disallowed_area_size = 0 if self._disallowed_areas: @@ -177,6 +219,21 @@ class BuildVolume(SceneNode): " \"Print Sequence\" setting to prevent the gantry from colliding" " with printed objects."), lifetime=10).show() + def _updateRaftThickness(self): + old_raft_thickness = self._raft_thickness + self._adhesion_type = self._active_container_stack.getProperty("adhesion_type", "value") + self._raft_thickness = 0.0 + if self._adhesion_type == "raft": + self._raft_thickness = ( + self._active_container_stack.getProperty("raft_base_thickness", "value") + + self._active_container_stack.getProperty("raft_interface_thickness", "value") + + self._active_container_stack.getProperty("raft_surface_layers", "value") * + self._active_container_stack.getProperty("raft_surface_thickness", "value") + + self._active_container_stack.getProperty("raft_airgap", "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) + def _onGlobalContainerStackChanged(self): if self._active_container_stack: self._active_container_stack.propertyChanged.disconnect(self._onSettingPropertyChanged) @@ -195,6 +252,7 @@ class BuildVolume(SceneNode): self._depth = self._active_container_stack.getProperty("machine_depth", "value") self._updateDisallowedAreas() + self._updateRaftThickness() self.rebuild() @@ -202,32 +260,40 @@ class BuildVolume(SceneNode): if property_name != "value": return + rebuild_me = False if setting_key == "print_sequence": if Application.getInstance().getGlobalContainerStack().getProperty("print_sequence", "value") == "one_at_a_time": self._height = self._active_container_stack.getProperty("gantry_height", "value") self._buildVolumeMessage() else: self._height = self._active_container_stack.getProperty("machine_height", "value") - self.rebuild() + rebuild_me = True + if setting_key in self._skirt_settings: self._updateDisallowedAreas() + rebuild_me = True + + if setting_key in self._raft_settings: + self._updateRaftThickness() + rebuild_me = True + + if rebuild_me: self.rebuild() def _updateDisallowedAreas(self): if not self._active_container_stack: return - disallowed_areas = self._active_container_stack.getProperty("machine_disallowed_areas", "value") + disallowed_areas = copy.deepcopy( + self._active_container_stack.getProperty("machine_disallowed_areas", "value")) areas = [] # Add extruder prime locations as disallowed areas. # Probably needs some rework after coordinate system change. - machine_definition = self._active_container_stack.getBottom() - current_machine_id = machine_definition.getId() extruder_manager = ExtruderManager.getInstance() - extruders = extruder_manager.getMachineExtruders(current_machine_id) - machine_width = machine_definition.getProperty("machine_width", "value") - machine_depth = machine_definition.getProperty("machine_depth", "value") + extruders = extruder_manager.getMachineExtruders(self._active_container_stack.getId()) + machine_width = self._active_container_stack.getProperty("machine_width", "value") + machine_depth = self._active_container_stack.getProperty("machine_depth", "value") for single_extruder in extruders: extruder_prime_pos_x = single_extruder.getProperty("extruder_prime_pos_x", "value") extruder_prime_pos_y = single_extruder.getProperty("extruder_prime_pos_y", "value") @@ -249,16 +315,7 @@ class BuildVolume(SceneNode): # Extend every area already in the disallowed_areas with the skirt size. for area in disallowed_areas: poly = Polygon(numpy.array(area, numpy.float32)) - poly = poly.getMinkowskiHull(Polygon(numpy.array([ - [-skirt_size, 0], - [-skirt_size * 0.707, skirt_size * 0.707], - [0, skirt_size], - [skirt_size * 0.707, skirt_size * 0.707], - [skirt_size, 0], - [skirt_size * 0.707, -skirt_size * 0.707], - [0, -skirt_size], - [-skirt_size * 0.707, -skirt_size * 0.707] - ], numpy.float32))) + poly = poly.getMinkowskiHull(Polygon(approximatedCircleVertices(skirt_size))) areas.append(poly) @@ -297,7 +354,7 @@ class BuildVolume(SceneNode): self._disallowed_areas = areas - ## Convenience function to calculate the size of the bed adhesion. + ## Convenience function to calculate the size of the bed adhesion in directions x, y. def _getSkirtSize(self, container_stack): skirt_size = 0.0 @@ -323,3 +380,4 @@ class BuildVolume(SceneNode): return max(min(value, max_value), min_value) _skirt_settings = ["adhesion_type", "skirt_gap", "skirt_line_count", "skirt_line_width", "brim_width", "brim_line_count", "raft_margin", "draft_shield_enabled", "draft_shield_dist", "xy_offset"] + _raft_settings = ["adhesion_type", "raft_base_thickness", "raft_interface_thickness", "raft_surface_layers", "raft_surface_thickness", "raft_airgap"] diff --git a/cura/CuraApplication.py b/cura/CuraApplication.py index 08d46af4df..29c1f086b4 100644 --- a/cura/CuraApplication.py +++ b/cura/CuraApplication.py @@ -4,7 +4,6 @@ from UM.Qt.QtApplication import QtApplication from UM.Scene.SceneNode import SceneNode from UM.Scene.Camera import Camera -from UM.Scene.Platform import Platform as Scene_Platform from UM.Math.Vector import Vector from UM.Math.Quaternion import Quaternion from UM.Math.AxisAlignedBox import AxisAlignedBox @@ -144,7 +143,6 @@ class CuraApplication(QtApplication): ]) self._physics = None self._volume = None - self._platform = None self._output_devices = {} self._print_information = None self._previous_active_tool = None @@ -196,6 +194,14 @@ class CuraApplication(QtApplication): Preferences.getInstance().addPreference("view/center_on_select", True) Preferences.getInstance().addPreference("mesh/scale_to_fit", True) Preferences.getInstance().addPreference("mesh/scale_tiny_meshes", True) + + for key in [ + "dialog_load_path", # dialog_save_path is in LocalFileOutputDevicePlugin + "dialog_profile_path", + "dialog_material_path"]: + + Preferences.getInstance().addPreference("local_file/%s" % key, "~/") + Preferences.getInstance().setDefault("local_file/last_used_type", "text/x-gcode") Preferences.getInstance().setDefault("general/visible_settings", """ @@ -334,10 +340,16 @@ class CuraApplication(QtApplication): f.write(data) - @pyqtSlot(result = QUrl) - def getDefaultPath(self): - return QUrl.fromLocalFile(os.path.expanduser("~/")) - + @pyqtSlot(str, result = QUrl) + def getDefaultPath(self, key): + #return QUrl.fromLocalFile(os.path.expanduser("~/")) + default_path = Preferences.getInstance().getValue("local_file/%s" % key) + return QUrl.fromLocalFile(default_path) + + @pyqtSlot(str, str) + def setDefaultPath(self, key, default_path): + Preferences.getInstance().setValue("local_file/%s" % key, default_path) + ## Handle loading of all plugin types (and the backend explicitly) # \sa PluginRegistery def _loadPlugins(self): @@ -377,8 +389,8 @@ class CuraApplication(QtApplication): Selection.selectionChanged.connect(self.onSelectionChanged) root = controller.getScene().getRoot() - self._platform = Scene_Platform(root) + # The platform is a child of BuildVolume self._volume = BuildVolume.BuildVolume(root) self.getRenderer().setBackgroundColor(QColor(245, 245, 245)) @@ -857,3 +869,6 @@ class CuraApplication(QtApplication): @pyqtSlot("QSize") def setMinimumWindowSize(self, size): self.getMainWindow().setMinimumSize(size) + + def getBuildVolume(self): + return self._volume \ No newline at end of file diff --git a/cura/PlatformPhysics.py b/cura/PlatformPhysics.py index 0c9c933899..191f7b0e27 100644 --- a/cura/PlatformPhysics.py +++ b/cura/PlatformPhysics.py @@ -40,6 +40,7 @@ class PlatformPhysics: return root = self._controller.getScene().getRoot() + for node in BreadthFirstIterator(root): if node is root or type(node) is not SceneNode or node.getBoundingBox() is None: continue @@ -58,10 +59,7 @@ class PlatformPhysics: move_vector = Vector() if not (node.getParent() and node.getParent().callDecoration("isGroup")): #If an object is grouped, don't move it down z_offset = node.callDecoration("getZOffset") if node.getDecorator(ZOffsetDecorator.ZOffsetDecorator) else 0 - if bbox.bottom > 0: - move_vector = move_vector.set(y=-bbox.bottom + z_offset) - elif bbox.bottom < z_offset: - move_vector = move_vector.set(y=(-bbox.bottom) - z_offset) + move_vector = move_vector.set(y=-bbox.bottom + z_offset) # If there is no convex hull for the node, start calculating it and continue. if not node.getDecorator(ConvexHullDecorator): diff --git a/plugins/CuraEngineBackend/ProcessSlicedLayersJob.py b/plugins/CuraEngineBackend/ProcessSlicedLayersJob.py index 599ed52fc1..fb1e33366a 100644 --- a/plugins/CuraEngineBackend/ProcessSlicedLayersJob.py +++ b/plugins/CuraEngineBackend/ProcessSlicedLayersJob.py @@ -130,7 +130,10 @@ class ProcessSlicedLayersJob(Job): new_node.addDecorator(decorator) new_node.setMeshData(mesh) - new_node.setParent(self._scene.getRoot()) # Note: After this we can no longer abort! + # Set build volume as parent, the build volume can move as a result of raft settings. + # It makes sense to set the build volume as parent: the print is actually printed on it. + new_node_parent = Application.getInstance().getBuildVolume() + new_node.setParent(new_node_parent) # Note: After this we can no longer abort! settings = Application.getInstance().getGlobalContainerStack() if not settings.getProperty("machine_center_is_zero", "value"): diff --git a/resources/definitions/fdmprinter.def.json b/resources/definitions/fdmprinter.def.json index 839811fe73..d4933be5bb 100644 --- a/resources/definitions/fdmprinter.def.json +++ b/resources/definitions/fdmprinter.def.json @@ -1000,7 +1000,7 @@ "type": "int", "minimum_value": "0", "maximum_value_warning": "4", - "maximum_value": "17", + "maximum_value": "20 - math.log(infill_line_distance) / math.log(2)", "settable_per_mesh": true }, "gradual_infill_step_height": diff --git a/resources/qml/Cura.qml b/resources/qml/Cura.qml index 8404db01fa..c449b4c83e 100644 --- a/resources/qml/Cura.qml +++ b/resources/qml/Cura.qml @@ -638,7 +638,7 @@ UM.MainWindow //TODO: Support multiple file selection, workaround bug in KDE file dialog //selectMultiple: true nameFilters: UM.MeshFileHandler.supportedReadFileTypes; - folder: Printer.getDefaultPath() + folder: CuraApplication.getDefaultPath("dialog_load_path") onAccepted: { //Because several implementations of the file dialog only update the folder @@ -646,6 +646,7 @@ UM.MainWindow var f = folder; folder = f; + CuraApplication.setDefaultPath("dialog_load_path", folder); UM.MeshFileHandler.readLocalFile(fileUrl) var meshName = backgroundItem.getMeshName(fileUrl.toString()) backgroundItem.hasMesh(decodeURIComponent(meshName)) diff --git a/resources/qml/Preferences/MaterialsPage.qml b/resources/qml/Preferences/MaterialsPage.qml index 18059545e2..aaaa4f5e9d 100644 --- a/resources/qml/Preferences/MaterialsPage.qml +++ b/resources/qml/Preferences/MaterialsPage.qml @@ -200,7 +200,7 @@ UM.ManagementPage title: catalog.i18nc("@title:window", "Import Material"); selectExisting: true; nameFilters: Cura.ContainerManager.getContainerNameFilters("material") - folder: CuraApplication.getDefaultPath() + folder: CuraApplication.getDefaultPath("dialog_material_path") onAccepted: { var result = Cura.ContainerManager.importContainer(fileUrl) @@ -221,6 +221,7 @@ UM.ManagementPage messageDialog.icon = StandardIcon.Critical } messageDialog.open() + CuraApplication.setDefaultPath("dialog_material_path", folder) } } @@ -230,7 +231,7 @@ UM.ManagementPage title: catalog.i18nc("@title:window", "Export Material"); selectExisting: false; nameFilters: Cura.ContainerManager.getContainerNameFilters("material") - folder: CuraApplication.getDefaultPath() + folder: CuraApplication.getDefaultPath("dialog_material_path") onAccepted: { if(base.currentItem.metadata.base_file) @@ -255,6 +256,7 @@ UM.ManagementPage messageDialog.text = catalog.i18nc("@info:status", "Successfully exported material to %1").arg(fileUrl) messageDialog.open() } + CuraApplication.setDefaultPath("dialog_material_path", folder) } } diff --git a/resources/qml/Preferences/ProfilesPage.qml b/resources/qml/Preferences/ProfilesPage.qml index 5a56ac8dc3..119a16facc 100644 --- a/resources/qml/Preferences/ProfilesPage.qml +++ b/resources/qml/Preferences/ProfilesPage.qml @@ -291,7 +291,7 @@ UM.ManagementPage title: catalog.i18nc("@title:window", "Import Profile"); selectExisting: true; nameFilters: base.model.getFileNameFilters("profile_reader") - folder: base.model.getDefaultPath() + folder: CuraApplication.getDefaultPath("dialog_profile_path") onAccepted: { var result = base.model.importProfile(fileUrl) @@ -309,6 +309,7 @@ UM.ManagementPage messageDialog.icon = StandardIcon.Critical } messageDialog.open() + CuraApplication.setDefaultPath("dialog_profile_path", folder) } } @@ -318,7 +319,7 @@ UM.ManagementPage title: catalog.i18nc("@title:window", "Export Profile"); selectExisting: false; nameFilters: base.model.getFileNameFilters("profile_writer") - folder: base.model.getDefaultPath() + folder: CuraApplication.getDefaultPath("dialog_profile_path") onAccepted: { var result = base.model.exportProfile(base.currentItem.id, fileUrl, selectedNameFilter) @@ -329,6 +330,7 @@ UM.ManagementPage messageDialog.open() } // else pop-up Message thing from python code + CuraApplication.setDefaultPath("dialog_profile_path", folder) } } }