diff --git a/README.md b/README.md index 077e6d1ab7..0ab3de61a4 100644 --- a/README.md +++ b/README.md @@ -49,6 +49,7 @@ Third party plugins * [Barbarian Plugin](https://github.com/nallath/BarbarianPlugin): Simple scale tool for imperial to metric. * [X3G Writer](https://github.com/Ghostkeeper/X3GWriter): Adds support for exporting X3G files. * [Auto orientation](https://github.com/nallath/CuraOrientationPlugin): Calculate the optimal orientation for a model. +* [OctoPrint Plugin](https://github.com/fieldofview/OctoPrintPlugin): Send printjobs directly to OctoPrint and monitor their progress in Cura. Making profiles for other printers ---------------------------------- diff --git a/cura/BuildVolume.py b/cura/BuildVolume.py index e5300526d4..8263355dda 100644 --- a/cura/BuildVolume.py +++ b/cura/BuildVolume.py @@ -10,6 +10,7 @@ from UM.Application import Application from UM.Resources import Resources from UM.Mesh.MeshBuilder import MeshBuilder 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 @@ -22,6 +23,7 @@ catalog = i18nCatalog("cura") import numpy import copy +import math import UM.Settings.ContainerRegistry @@ -45,6 +47,7 @@ class BuildVolume(SceneNode): self._width = 0 self._height = 0 self._depth = 0 + self._shape = "" self._shader = None @@ -139,6 +142,9 @@ class BuildVolume(SceneNode): def setDepth(self, depth): if depth: self._depth = depth + def setShape(self, shape): + if shape: self._shape = shape + def getDisallowedAreas(self): return self._disallowed_areas @@ -177,27 +183,70 @@ class BuildVolume(SceneNode): min_d = -self._depth / 2 max_d = self._depth / 2 - mb = MeshBuilder() + z_fight_distance = 0.2 # Distance between buildplate and disallowed area meshes to prevent z-fighting - # 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) - mb.addLine(Vector(max_w, min_h, min_d), Vector(max_w, max_h, min_d), color = self.VolumeOutlineColor) + 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.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) + mb.addLine(Vector(max_w, min_h, min_d), Vector(max_w, max_h, min_d), color = self.VolumeOutlineColor) - mb.addLine(Vector(min_w, min_h, max_d), Vector(max_w, min_h, max_d), color = self.VolumeOutlineColor) - mb.addLine(Vector(min_w, min_h, max_d), Vector(min_w, max_h, max_d), color = self.VolumeOutlineColor) - mb.addLine(Vector(min_w, max_h, max_d), Vector(max_w, max_h, max_d), color = self.VolumeOutlineColor) - mb.addLine(Vector(max_w, min_h, max_d), Vector(max_w, max_h, max_d), color = self.VolumeOutlineColor) + mb.addLine(Vector(min_w, min_h, max_d), Vector(max_w, min_h, max_d), color = self.VolumeOutlineColor) + mb.addLine(Vector(min_w, min_h, max_d), Vector(min_w, max_h, max_d), color = self.VolumeOutlineColor) + mb.addLine(Vector(min_w, max_h, max_d), Vector(max_w, max_h, max_d), color = self.VolumeOutlineColor) + mb.addLine(Vector(max_w, min_h, max_d), Vector(max_w, max_h, max_d), color = self.VolumeOutlineColor) - mb.addLine(Vector(min_w, min_h, min_d), Vector(min_w, min_h, max_d), color = self.VolumeOutlineColor) - mb.addLine(Vector(max_w, min_h, min_d), Vector(max_w, min_h, max_d), color = self.VolumeOutlineColor) - mb.addLine(Vector(min_w, max_h, min_d), Vector(min_w, max_h, max_d), color = self.VolumeOutlineColor) - mb.addLine(Vector(max_w, max_h, min_d), Vector(max_w, max_h, max_d), color = self.VolumeOutlineColor) + mb.addLine(Vector(min_w, min_h, min_d), Vector(min_w, min_h, max_d), color = self.VolumeOutlineColor) + mb.addLine(Vector(max_w, min_h, min_d), Vector(max_w, min_h, max_d), color = self.VolumeOutlineColor) + mb.addLine(Vector(min_w, max_h, min_d), Vector(min_w, max_h, max_d), color = self.VolumeOutlineColor) + mb.addLine(Vector(max_w, max_h, min_d), Vector(max_w, max_h, max_d), color = self.VolumeOutlineColor) - self.setMeshData(mb.build()) + self.setMeshData(mb.build()) - mb = MeshBuilder() + # Build plate grid mesh + mb = MeshBuilder() + mb.addQuad( + Vector(min_w, min_h - z_fight_distance, min_d), + Vector(max_w, min_h - z_fight_distance, min_d), + Vector(max_w, min_h - z_fight_distance, max_d), + Vector(min_w, min_h - z_fight_distance, max_d) + ) + + for n in range(0, 6): + v = mb.getVertex(n) + mb.setVertexUVCoordinates(n, v[0], v[2]) + self._grid_mesh = mb.build() + + else: + # Bottom and top 'ellipse' of the build volume + aspect = 1.0 + scale_matrix = Matrix() + if self._width != 0: + # Scale circular meshes by aspect ratio if width != height + aspect = self._height / 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.VolumeOutlineColor) + mb.addArc(max_w, Vector.Unit_Y, center = (0, max_h, 0), color = self.VolumeOutlineColor) + self.setMeshData(mb.build().getTransformed(scale_matrix)) + + # Build plate grid mesh + mb = MeshBuilder() + mb.addVertex(0, min_h - z_fight_distance, 0) + mb.addArc(max_w, Vector.Unit_Y, center = Vector(0, min_h - z_fight_distance, 0)) + sections = mb.getVertexCount() - 1 # Center point is not an arc section + indices = [] + for n in range(0, sections - 1): + indices.append([0, n + 2, n + 1]) + mb.addIndices(numpy.asarray(indices, dtype = numpy.int32)) + mb.calculateNormals() + + for n in range(0, mb.getVertexCount()): + v = mb.getVertex(n) + mb.setVertexUVCoordinates(n, v[0], v[2] * aspect) + self._grid_mesh = mb.build().getTransformed(scale_matrix) # Indication of the machine origin if self._global_container_stack.getProperty("machine_center_is_zero", "value"): @@ -205,6 +254,7 @@ class BuildVolume(SceneNode): else: origin = Vector(min_w, min_h, max_d) + mb = MeshBuilder() mb.addCube( width = self._origin_line_length, height = self._origin_line_width, @@ -228,19 +278,6 @@ class BuildVolume(SceneNode): ) self._origin_mesh = mb.build() - mb = MeshBuilder() - mb.addQuad( - Vector(min_w, min_h - 0.2, min_d), - Vector(max_w, min_h - 0.2, min_d), - 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() - disallowed_area_height = 0.1 disallowed_area_size = 0 if self._disallowed_areas: @@ -353,6 +390,7 @@ class BuildVolume(SceneNode): 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._updateDisallowedAreas() self._updateRaftThickness() @@ -581,34 +619,79 @@ class BuildVolume(SceneNode): 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 - 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))) + + 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 diff --git a/cura/CuraApplication.py b/cura/CuraApplication.py index e8c8ba981a..6ed7759c34 100644 --- a/cura/CuraApplication.py +++ b/cura/CuraApplication.py @@ -50,43 +50,12 @@ from PyQt5.QtGui import QColor, QIcon from PyQt5.QtWidgets import QMessageBox from PyQt5.QtQml import qmlRegisterUncreatableType, qmlRegisterSingletonType, qmlRegisterType -from contextlib import contextmanager - import sys import os.path import numpy import copy import urllib import os -import time - -CONFIG_LOCK_FILENAME = "cura.lock" - -## Contextmanager to create a lock file and remove it afterwards. -@contextmanager -def lockFile(filename): - try: - with open(filename, 'w') as lock_file: - lock_file.write("Lock file - Cura is currently writing") - except: - Logger.log("e", "Could not create lock file [%s]" % filename) - yield - try: - if os.path.exists(filename): - os.remove(filename) - except: - Logger.log("e", "Could not delete lock file [%s]" % filename) - - -## Wait for a lock file to disappear -# the maximum allowable age is settable; if the file is too old, it will be ignored too -def waitFileDisappear(filename, max_age_seconds=10, msg=""): - now = time.time() - while os.path.exists(filename) and now < os.path.getmtime(filename) + max_age_seconds and now > os.path.getmtime(filename): - if msg: - Logger.log("d", msg) - time.sleep(1) - now = time.time() numpy.seterr(all="ignore") @@ -235,10 +204,8 @@ class CuraApplication(QtApplication): empty_quality_changes_container.addMetaDataEntry("type", "quality_changes") ContainerRegistry.getInstance().addContainer(empty_quality_changes_container) - # Set the filename to create if cura is writing in the config dir. - self._config_lock_filename = os.path.join(Resources.getConfigStoragePath(), CONFIG_LOCK_FILENAME) - self.waitConfigLockFile() - ContainerRegistry.getInstance().load() + with ContainerRegistry.getInstance().lockFile(): + ContainerRegistry.getInstance().load() Preferences.getInstance().addPreference("cura/active_mode", "simple") Preferences.getInstance().addPreference("cura/recent_files", "") @@ -264,6 +231,8 @@ class CuraApplication(QtApplication): shell wall_thickness top_bottom_thickness + z_seam_x + z_seam_y infill infill_sparse_density material @@ -318,12 +287,6 @@ class CuraApplication(QtApplication): self._recent_files.append(QUrl.fromLocalFile(f)) - ## Lock file check: if (another) Cura is writing in the Config dir. - # one may not be able to read a valid set of files while writing. Not entirely fool-proof, - # but works when you start Cura shortly after shutting down. - def waitConfigLockFile(self): - waitFileDisappear(self._config_lock_filename, max_age_seconds=10, msg="Waiting for Cura to finish writing in the config dir...") - def _onEngineCreated(self): self._engine.addImageProvider("camera", CameraImageProvider.CameraImageProvider()) @@ -351,11 +314,8 @@ class CuraApplication(QtApplication): if not self._started: # Do not do saving during application start return - self.waitConfigLockFile() - - # When starting Cura, we check for the lockFile which is created and deleted here - with lockFile(self._config_lock_filename): - + # Lock file for "more" atomically loading and saving to/from config dir. + with ContainerRegistry.getInstance().lockFile(): for instance in ContainerRegistry.getInstance().findInstanceContainers(): if not instance.isDirty(): continue @@ -1000,7 +960,7 @@ class CuraApplication(QtApplication): def _reloadMeshFinished(self, job): # TODO; This needs to be fixed properly. We now make the assumption that we only load a single mesh! - mesh_data = job.getResult().getMeshData() + mesh_data = job.getResult()[0].getMeshData() if mesh_data: job._node.setMeshData(mesh_data) else: diff --git a/plugins/3MFReader/ThreeMFWorkspaceReader.py b/plugins/3MFReader/ThreeMFWorkspaceReader.py index e1f0173235..f5d7466ad4 100644 --- a/plugins/3MFReader/ThreeMFWorkspaceReader.py +++ b/plugins/3MFReader/ThreeMFWorkspaceReader.py @@ -128,11 +128,6 @@ class ThreeMFWorkspaceReader(WorkspaceReader): return WorkspaceReader.PreReadResult.accepted def read(self, file_name): - # Load all the nodes / meshdata of the workspace - nodes = self._3mf_mesh_reader.read(file_name) - if nodes is None: - nodes = [] - archive = zipfile.ZipFile(file_name, "r") cura_file_names = [name for name in archive.namelist() if name.startswith("Cura/")] @@ -144,8 +139,19 @@ class ThreeMFWorkspaceReader(WorkspaceReader): # Copy a number of settings from the temp preferences to the global global_preferences = Preferences.getInstance() - global_preferences.setValue("general/visible_settings", temp_preferences.getValue("general/visible_settings")) - global_preferences.setValue("cura/categories_expanded", temp_preferences.getValue("cura/categories_expanded")) + + visible_settings = temp_preferences.getValue("general/visible_settings") + if visible_settings is None: + Logger.log("w", "Workspace did not contain visible settings. Leaving visibility unchanged") + else: + global_preferences.setValue("general/visible_settings", visible_settings) + + categories_expanded = temp_preferences.getValue("cura/categories_expanded") + if categories_expanded is None: + Logger.log("w", "Workspace did not contain expanded categories. Leaving them unchanged") + else: + global_preferences.setValue("cura/categories_expanded", categories_expanded) + Application.getInstance().expandedCategoriesChanged.emit() # Notify the GUI of the change self._id_mapping = {} @@ -388,6 +394,11 @@ class ThreeMFWorkspaceReader(WorkspaceReader): # Actually change the active machine. Application.getInstance().setGlobalContainerStack(global_stack) + + # Load all the nodes / meshdata of the workspace + nodes = self._3mf_mesh_reader.read(file_name) + if nodes is None: + nodes = [] return nodes def _stripFileToId(self, file): diff --git a/plugins/3MFWriter/ThreeMFWriter.py b/plugins/3MFWriter/ThreeMFWriter.py index d86b119276..882740c4ed 100644 --- a/plugins/3MFWriter/ThreeMFWriter.py +++ b/plugins/3MFWriter/ThreeMFWriter.py @@ -5,7 +5,7 @@ from UM.Mesh.MeshWriter import MeshWriter from UM.Math.Vector import Vector from UM.Logger import Logger from UM.Math.Matrix import Matrix -from UM.Settings.SettingRelation import RelationType +from UM.Application import Application try: import xml.etree.cElementTree as ET @@ -78,6 +78,12 @@ class ThreeMFWriter(MeshWriter): model_relation_element = ET.SubElement(relations_element, "Relationship", Target = "/3D/3dmodel.model", Id = "rel0", Type = "http://schemas.microsoft.com/3dmanufacturing/2013/01/3dmodel") model = ET.Element("model", unit = "millimeter", xmlns = self._namespaces["3mf"]) + + # Add the version of Cura this was created with. As "CuraVersion" is not a recognised metadata name + # by 3mf itself, we place it in our own namespace. + version_metadata = ET.SubElement(model, "metadata", xmlns = self._namespaces["cura"], name = "CuraVersion") + version_metadata.text = Application.getInstance().getVersion() + resources = ET.SubElement(model, "resources") build = ET.SubElement(model, "build") diff --git a/plugins/CuraEngineBackend/StartSliceJob.py b/plugins/CuraEngineBackend/StartSliceJob.py index 0319186518..dc9c4c2e06 100644 --- a/plugins/CuraEngineBackend/StartSliceJob.py +++ b/plugins/CuraEngineBackend/StartSliceJob.py @@ -163,22 +163,17 @@ class StartSliceJob(Job): obj.id = id(object) verts = mesh_data.getVertices() indices = mesh_data.getIndices() + if indices is not None: - #TODO: This is a very slow way of doing it! It also locks up the GUI. - flat_vert_list = [] - for face in indices: - for vert_index in face: - flat_vert_list.append(verts[vert_index]) - Job.yieldThread() - verts = numpy.array(flat_vert_list) + flat_verts = numpy.take(verts, indices.flatten(), axis=0) else: - verts = numpy.array(verts) + flat_verts = numpy.array(verts) # Convert from Y up axes to Z up axes. Equals a 90 degree rotation. - verts[:, [1, 2]] = verts[:, [2, 1]] - verts[:, 1] *= -1 + flat_verts[:, [1, 2]] = flat_verts[:, [2, 1]] + flat_verts[:, 1] *= -1 - obj.vertices = verts + obj.vertices = flat_verts self._handlePerObjectSettings(object, obj) diff --git a/plugins/MachineSettingsAction/MachineSettingsAction.qml b/plugins/MachineSettingsAction/MachineSettingsAction.qml index ea299438f0..26bbccd44a 100644 --- a/plugins/MachineSettingsAction/MachineSettingsAction.qml +++ b/plugins/MachineSettingsAction/MachineSettingsAction.qml @@ -120,19 +120,73 @@ Cura.MachineAction Column { - CheckBox + Row { - id: heatedBedCheckBox - text: catalog.i18nc("@option:check", "Heated Bed") - checked: String(machineHeatedBedProvider.properties.value).toLowerCase() != 'false' - onClicked: machineHeatedBedProvider.setPropertyValue("value", checked) + spacing: UM.Theme.getSize("default_margin").width + + Label + { + text: catalog.i18nc("@label", "Build Plate Shape") + } + + ComboBox + { + id: shapeComboBox + model: ListModel + { + id: shapesModel + Component.onCompleted: + { + // Options come in as a string-representation of an OrderedDict + var options = machineShapeProvider.properties.options.match(/^OrderedDict\(\[\((.*)\)\]\)$/); + if(options) + { + options = options[1].split("), (") + for(var i = 0; i < options.length; i++) + { + var option = options[i].substring(1, options[i].length - 1).split("', '") + shapesModel.append({text: option[1], value: option[0]}); + } + } + } + } + currentIndex: + { + var currentValue = machineShapeProvider.properties.value; + var index = 0; + for(var i = 0; i < shapesModel.count; i++) + { + if(shapesModel.get(i).value == currentValue) { + index = i; + break; + } + } + return index + } + onActivated: + { + machineShapeProvider.setPropertyValue("value", shapesModel.get(index).value); + manager.forceUpdate(); + } + } } CheckBox { id: centerIsZeroCheckBox text: catalog.i18nc("@option:check", "Machine Center is Zero") checked: String(machineCenterIsZeroProvider.properties.value).toLowerCase() != 'false' - onClicked: machineCenterIsZeroProvider.setPropertyValue("value", checked) + onClicked: + { + machineCenterIsZeroProvider.setPropertyValue("value", checked); + manager.forceUpdate(); + } + } + CheckBox + { + id: heatedBedCheckBox + text: catalog.i18nc("@option:check", "Heated Bed") + checked: String(machineHeatedBedProvider.properties.value).toLowerCase() != 'false' + onClicked: machineHeatedBedProvider.setPropertyValue("value", checked) } } @@ -428,6 +482,16 @@ Cura.MachineAction storeIndex: manager.containerIndex } + UM.SettingPropertyProvider + { + id: machineShapeProvider + + containerStackId: Cura.MachineManager.activeMachineId + key: "machine_shape" + watchedProperties: [ "value", "options" ] + storeIndex: manager.containerIndex + } + UM.SettingPropertyProvider { id: machineHeatedBedProvider diff --git a/plugins/UM3NetworkPrinting/NetworkPrinterOutputDevice.py b/plugins/UM3NetworkPrinting/NetworkPrinterOutputDevice.py index 20552a6843..19692492e4 100644 --- a/plugins/UM3NetworkPrinting/NetworkPrinterOutputDevice.py +++ b/plugins/UM3NetworkPrinting/NetworkPrinterOutputDevice.py @@ -532,6 +532,7 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): Application.getInstance().showPrintMonitor.emit(True) self._print_finished = True + self.writeStarted.emit(self) self._gcode = getattr(Application.getInstance().getController().getScene(), "gcode_list") print_information = Application.getInstance().getPrintInformation() diff --git a/plugins/X3DReader/X3DReader.py b/plugins/X3DReader/X3DReader.py index 4ce21f3f8c..ba31c9ea86 100644 --- a/plugins/X3DReader/X3DReader.py +++ b/plugins/X3DReader/X3DReader.py @@ -700,7 +700,11 @@ class X3DReader(MeshReader): if not c is None: pt = c.attrib.get("point") if pt: - co = [float(x) for x in pt.split()] + # allow the list of float values in 'point' attribute to + # be separated by commas or whitespace as per spec of + # XML encoding of X3D + # Ref ISO/IEC 19776-1:2015 : Section 5.1.2 + co = [float(x) for vec in pt.split(',') for x in vec.split()] num_verts = len(co) // 3 self.verts = numpy.empty((4, num_verts), dtype=numpy.float32) self.verts[3,:] = numpy.ones((num_verts), dtype=numpy.float32) diff --git a/resources/definitions/fdmprinter.def.json b/resources/definitions/fdmprinter.def.json index 2d77d62670..e3af67e189 100644 --- a/resources/definitions/fdmprinter.def.json +++ b/resources/definitions/fdmprinter.def.json @@ -137,6 +137,21 @@ "settable_per_extruder": false, "settable_per_meshgroup": false }, + "machine_shape": + { + "label": "Build plate shape", + "description": "The shape of the build plate without taking unprintable areas into account.", + "default_value": "rectangular", + "type": "enum", + "options": + { + "rectangular": "Rectangular", + "elliptic": "Elliptic" + }, + "settable_per_mesh": false, + "settable_per_extruder": false, + "settable_per_meshgroup": false + }, "machine_height": { "label": "Machine height", @@ -946,17 +961,39 @@ "z_seam_type": { "label": "Z Seam Alignment", - "description": "Starting point of each path in a layer. When paths in consecutive layers start at the same point a vertical seam may show on the print. When aligning these at the back, the seam is easiest to remove. When placed randomly the inaccuracies at the paths' start will be less noticeable. When taking the shortest path the print will be quicker.", + "description": "Starting point of each path in a layer. When paths in consecutive layers start at the same point a vertical seam may show on the print. When aligning these near a user specified location, the seam is easiest to remove. When placed randomly the inaccuracies at the paths' start will be less noticeable. When taking the shortest path the print will be quicker.", "type": "enum", "options": { - "back": "Back", + "back": "User Specified", "shortest": "Shortest", "random": "Random" }, "default_value": "shortest", "settable_per_mesh": true }, + "z_seam_x": + { + "label": "Z Seam X", + "description": "The X coordinate of the position near where to start printing each part in a layer.", + "unit": "mm", + "type": "float", + "default_value": 100.0, + "value": "machine_width / 2", + "enabled": "z_seam_type == 'back'", + "settable_per_mesh": true + }, + "z_seam_y": + { + "label": "Z Seam Y", + "description": "The Y coordinate of the position near where to start printing each part in a layer.", + "unit": "mm", + "type": "float", + "default_value": 100.0, + "value": "machine_depth / 2", + "enabled": "z_seam_type == 'back'", + "settable_per_mesh": true + }, "skin_no_small_gaps_heuristic": { "label": "Ignore Small Z Gaps", @@ -996,7 +1033,7 @@ "default_value": 2, "minimum_value": "0", "minimum_value_warning": "infill_line_width", - "value": "0 if infill_sparse_density == 0 else (infill_line_width * 100) / infill_sparse_density * (2 if infill_pattern == 'grid' else (3 if infill_pattern == 'triangles' or infill_pattern == 'cubic' else (4 if infill_pattern == 'tetrahedral' else 1)))", + "value": "0 if infill_sparse_density == 0 else (infill_line_width * 100) / infill_sparse_density * (2 if infill_pattern == 'grid' else (3 if infill_pattern == 'triangles' or infill_pattern == 'cubic' or infill_pattern == 'cubicsubdiv' else (4 if infill_pattern == 'tetrahedral' else 1)))", "settable_per_mesh": true } } @@ -1012,6 +1049,7 @@ "lines": "Lines", "triangles": "Triangles", "cubic": "Cubic", + "cubicsubdiv": "Cubic Subdivision", "tetrahedral": "Tetrahedral", "concentric": "Concentric", "concentric_3d": "Concentric 3D", @@ -1022,6 +1060,32 @@ "value": "'lines' if infill_sparse_density > 25 else 'grid'", "settable_per_mesh": true }, + "sub_div_rad_mult": + { + "label": "Cubic Subdivision Radius", + "description": "A multiplier on the radius from the center of each cube to check for the boundary of the model, as to decide whether this cube should be subdivided. Larger values lead to more subdivisions, i.e. more small cubes.", + "unit": "%", + "type": "float", + "default_value": 100, + "minimum_value": "0", + "minimum_value_warning": "100", + "maximum_value_warning": "200", + "enabled": "infill_sparse_density > 0 and infill_pattern == 'cubicsubdiv'", + "settable_per_mesh": true + }, + "sub_div_rad_add": + { + "label": "Cubic Subdivision Shell", + "description": "An addition to the radius from the center of each cube to check for the boundary of the model, as to decide whether this cube should be subdivided. Larger values lead to a thicker shell of small cubes near the boundary of the model.", + "unit": "mm", + "type": "float", + "default_value": 0.4, + "value": "wall_line_width_x", + "minimum_value_warning": "-1 * infill_line_distance", + "maximum_value_warning": "5 * infill_line_distance", + "enabled": "infill_sparse_density > 0 and infill_pattern == 'cubicsubdiv'", + "settable_per_mesh": true + }, "infill_overlap": { "label": "Infill Overlap Percentage", @@ -1116,7 +1180,7 @@ "minimum_value": "0", "maximum_value_warning": "4", "maximum_value": "20 - math.log(infill_line_distance) / math.log(2)", - "enabled": "infill_sparse_density > 0", + "enabled": "infill_sparse_density > 0 and infill_pattern != 'cubicsubdiv'", "settable_per_mesh": true }, "gradual_infill_step_height": @@ -1129,7 +1193,7 @@ "minimum_value": "0.0001", "minimum_value_warning": "3 * resolveOrValue('layer_height')", "maximum_value_warning": "100", - "enabled": "infill_sparse_density > 0 and gradual_infill_steps > 0", + "enabled": "infill_sparse_density > 0 and gradual_infill_steps > 0 and infill_pattern != 'cubicsubdiv'", "settable_per_mesh": true }, "infill_before_walls": @@ -2434,20 +2498,6 @@ "settable_per_extruder": true, "children": { - "cool_fan_speed_0": - { - "label": "Initial Fan Speed", - "description": "The speed at which the fans spin at the start of the print. In subsequent layers the fan speed is gradually increased up to the layer corresponding to Regular Fan Speed at Height.", - "unit": "%", - "type": "float", - "minimum_value": "0", - "maximum_value": "100", - "value": "cool_fan_speed", - "default_value": 100, - "enabled": "cool_fan_enabled", - "settable_per_mesh": false, - "settable_per_extruder": true - }, "cool_fan_speed_min": { "label": "Regular Fan Speed", @@ -2490,6 +2540,19 @@ "settable_per_mesh": false, "settable_per_extruder": true }, + "cool_fan_speed_0": + { + "label": "Initial Fan Speed", + "description": "The speed at which the fans spin at the start of the print. In subsequent layers the fan speed is gradually increased up to the layer corresponding to Regular Fan Speed at Height.", + "unit": "%", + "type": "float", + "minimum_value": "0", + "maximum_value": "100", + "default_value": 0, + "enabled": "cool_fan_enabled", + "settable_per_mesh": false, + "settable_per_extruder": true + }, "cool_fan_full_at_height": { "label": "Regular Fan Speed at Height", @@ -3616,7 +3679,6 @@ "type": "category", "icon": "category_dual", "description": "Settings used for printing with multiple extruders.", - "enabled": "machine_extruder_count > 1", "children": { "prime_tower_enable": @@ -3742,17 +3804,6 @@ "settable_per_mesh": false, "settable_per_extruder": true }, - "multiple_mesh_overlap": - { - "label": "Dual Extrusion Overlap", - "description": "Make the models printed with different extruder trains overlap a bit. This makes the different materials bond together better.", - "type": "float", - "unit": "mm", - "default_value": 0.15, - "minimum_value": "0", - "maximum_value_warning": "1.0", - "settable_per_mesh": true - }, "ooze_shield_enabled": { "label": "Enable Ooze Shield", @@ -3833,6 +3884,17 @@ "default_value": false, "settable_per_mesh": true }, + "multiple_mesh_overlap": + { + "label": "Merged Meshes Overlap", + "description": "Make meshes which are touching each other overlap a bit. This makes them bond together better.", + "type": "float", + "unit": "mm", + "default_value": 0.15, + "minimum_value": "0", + "maximum_value_warning": "1.0", + "settable_per_mesh": true + }, "carve_multiple_volumes": { "label": "Remove Mesh Intersection", @@ -4106,6 +4168,7 @@ "type": "bool", "default_value": false, "enabled": "support_enable", + "limit_to_extruder": "support_infill_extruder_nr", "settable_per_mesh": true }, "support_conical_angle": diff --git a/resources/qml/Actions.qml b/resources/qml/Actions.qml index bb4e28eae7..be8a7b8c49 100644 --- a/resources/qml/Actions.qml +++ b/resources/qml/Actions.qml @@ -290,7 +290,7 @@ Item Action { id: loadWorkspaceAction - text: catalog.i18nc("@action:inmenu menubar:file","&Open Workspace..."); + text: catalog.i18nc("@action:inmenu menubar:file","&Import project..."); } Action diff --git a/resources/qml/Cura.qml b/resources/qml/Cura.qml index 85be3342e9..5b5e71b07b 100644 --- a/resources/qml/Cura.qml +++ b/resources/qml/Cura.qml @@ -110,7 +110,7 @@ UM.MainWindow MenuItem { id: saveWorkspaceMenu - text: catalog.i18nc("@title:menu menubar:file","Save Workspace") + text: catalog.i18nc("@title:menu menubar:file","Export project") onTriggered: UM.OutputDeviceManager.requestWriteToDevice("local_file", PrintInformation.jobName, { "filter_by_machine": false, "file_type": "workspace" }); } diff --git a/resources/qml/Sidebar.qml b/resources/qml/Sidebar.qml index 9c0d641d77..77e82b5f92 100644 --- a/resources/qml/Sidebar.qml +++ b/resources/qml/Sidebar.qml @@ -166,15 +166,6 @@ Rectangle anchors.topMargin: UM.Theme.getSize("default_margin").height } - currentModeIndex: - { - var index = parseInt(UM.Preferences.getValue("cura/active_mode")) - if(index) - { - return index; - } - return 0; - } onCurrentModeIndexChanged: { UM.Preferences.setValue("cura/active_mode", currentModeIndex); @@ -268,7 +259,7 @@ Rectangle height: settingsModeSelection.height width: visible ? height : 0 - visible: !monitoringPrint && modesListModel.get(base.currentModeIndex).showFilterButton + visible: !monitoringPrint && modesListModel.get(base.currentModeIndex) != undefined && modesListModel.get(base.currentModeIndex).showFilterButton opacity: visible ? 1 : 0 onClicked: sidebarContents.currentItem.toggleFilterField() @@ -284,7 +275,7 @@ Rectangle } label: UM.RecolorImage { - anchors.verticalCenter: control.verticalCenter + anchors.verticalCenter: parent.verticalCenter anchors.right: parent.right anchors.rightMargin: UM.Theme.getSize("default_margin").width / 2 @@ -420,6 +411,12 @@ Rectangle modesListModel.append({ text: catalog.i18nc("@title:tab", "Recommended"), item: sidebarSimple, showFilterButton: false }) modesListModel.append({ text: catalog.i18nc("@title:tab", "Custom"), item: sidebarAdvanced, showFilterButton: true }) sidebarContents.push({ "item": modesListModel.get(base.currentModeIndex).item, "immediate": true }); + + var index = parseInt(UM.Preferences.getValue("cura/active_mode")) + if(index) + { + currentModeIndex = index; + } } UM.SettingPropertyProvider