Merge branch 'master' into feature_config_dir_lock

This commit is contained in:
Jack Ha 2016-11-29 16:12:56 +01:00
commit 4d9416aae8
88 changed files with 3883 additions and 370 deletions

4
.gitignore vendored
View file

@ -31,5 +31,5 @@ debian*
plugins/Doodle3D-cura-plugin
plugins/GodMode
plugins/PostProcessingPlugin
plugins/UM3NetworkPrinting
plugins/X3GWriter
plugins/X3GWriter

View file

@ -1,4 +1,3 @@
project(cura NONE)
cmake_minimum_required(VERSION 2.8.12)

View file

@ -658,4 +658,4 @@ specific requirements.
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU AGPL, see
<http://www.gnu.org/licenses/>.
<http://www.gnu.org/licenses/>.

View file

@ -28,6 +28,8 @@ Dependencies
This will be needed at runtime to perform the actual slicing.
* [PySerial](https://github.com/pyserial/pyserial)
Only required for USB printing support.
* [python-zeroconf](https://github.com/jstasiak/python-zeroconf)
Only required to detect mDNS-enabled printers
Configuring Cura
----------------
@ -50,7 +52,7 @@ Third party plugins
Making profiles for other printers
----------------------------------
There are two ways of doing it. You can either use the generator [here](http://quillford.github.io/CuraProfileMaker/) or you can use [this](https://github.com/Ultimaker/Cura/blob/master/resources/machines/ultimaker_original.json) as a template.
There are two ways of doing it. You can either use the generator [here](http://quillford.github.io/CuraProfileMaker/) or you can use [this](https://github.com/Ultimaker/Cura/blob/master/resources/definitions/ultimaker_original.def.json) as a template.
* Change the machine ID to something unique
* Change the machine_name to your printer's name
@ -62,4 +64,4 @@ There are two ways of doing it. You can either use the generator [here](http://q
* Set the start and end gcode in machine_start_gcode and machine_end_gcode
* If your printer has a heated bed, set visible to true under material_bed_temperature
Once you are done, put the profile you have made into resources/machines, or in machines in your cura profile folder.
Once you are done, put the profile you have made into resources/definitions, or in definitions in your cura profile folder.

View file

@ -442,10 +442,7 @@ class BuildVolume(SceneNode):
if collision:
break
if not collision:
#Prime areas are valid. Add as normal.
result_areas[extruder_id].extend(prime_areas[extruder_id])
result_areas[extruder_id].extend(prime_areas[extruder_id])
nozzle_disallowed_areas = extruder.getProperty("nozzle_disallowed_areas", "value")
for area in nozzle_disallowed_areas:

View file

@ -100,7 +100,7 @@ class CuraApplication(QtApplication):
# For settings which are not settable_per_mesh and not settable_per_extruder:
# A function which determines the glabel/meshgroup value by looking at the values of the setting in all (used) extruders
SettingDefinition.addSupportedProperty("resolve", DefinitionPropertyType.Function, default = None)
SettingDefinition.addSupportedProperty("resolve", DefinitionPropertyType.Function, default = None, depends_on = "value")
SettingDefinition.addSettingType("extruder", None, str, Validator)
@ -338,6 +338,8 @@ class CuraApplication(QtApplication):
path = Resources.getStoragePath(self.ResourceTypes.UserInstanceContainer, file_name)
elif instance_type == "variant":
path = Resources.getStoragePath(self.ResourceTypes.VariantInstanceContainer, file_name)
elif instance_type == "definition_changes":
path = Resources.getStoragePath(self.ResourceTypes.MachineStack, file_name)
if path:
instance.setPath(path)
@ -590,7 +592,7 @@ class CuraApplication(QtApplication):
if not scene_bounding_box:
scene_bounding_box = AxisAlignedBox.Null
if repr(self._scene_bounding_box) != repr(scene_bounding_box):
if repr(self._scene_bounding_box) != repr(scene_bounding_box) and scene_bounding_box.isValid():
self._scene_bounding_box = scene_bounding_box
self.sceneBoundingBoxChanged.emit()
@ -692,6 +694,8 @@ class CuraApplication(QtApplication):
continue # Node that doesnt have a mesh and is not a group.
if node.getParent() and node.getParent().callDecoration("isGroup"):
continue # Grouped nodes don't need resetting as their parent (the group) is resetted)
if not node.isSelectable():
continue # i.e. node with layer data
Selection.add(node)
## Delete all nodes containing mesh data in the scene.
@ -731,6 +735,8 @@ class CuraApplication(QtApplication):
continue # Node that doesnt have a mesh and is not a group.
if node.getParent() and node.getParent().callDecoration("isGroup"):
continue # Grouped nodes don't need resetting as their parent (the group) is resetted)
if not node.isSelectable():
continue # i.e. node with layer data
nodes.append(node)
if nodes:
@ -757,6 +763,8 @@ class CuraApplication(QtApplication):
continue # Node that doesnt have a mesh and is not a group.
if node.getParent() and node.getParent().callDecoration("isGroup"):
continue # Grouped nodes don't need resetting as their parent (the group) is resetted)
if not node.isSelectable():
continue # i.e. node with layer data
nodes.append(node)
if nodes:

View file

@ -49,7 +49,9 @@ class PrinterOutputDevice(QObject, OutputDevice):
self._printer_state = ""
self._printer_type = "unknown"
def requestWrite(self, nodes, file_name = None, filter_by_machine = False):
self._camera_active = False
def requestWrite(self, nodes, file_name = None, filter_by_machine = False, file_handler = None):
raise NotImplementedError("requestWrite needs to be implemented")
## Signals
@ -136,6 +138,7 @@ class PrinterOutputDevice(QObject, OutputDevice):
@pyqtSlot()
def startCamera(self):
self._camera_active = True
self._startCamera()
def _startCamera(self):
@ -143,6 +146,7 @@ class PrinterOutputDevice(QObject, OutputDevice):
@pyqtSlot()
def stopCamera(self):
self._camera_active = False
self._stopCamera()
def _stopCamera(self):

View file

@ -152,6 +152,18 @@ class ExtruderManager(QObject):
if changed:
self.extrudersChanged.emit(machine_id)
def registerExtruder(self, extruder_train, machine_id):
changed = False
if machine_id not in self._extruder_trains:
self._extruder_trains[machine_id] = {}
changed = True
if extruder_train:
self._extruder_trains[machine_id][extruder_train.getMetaDataEntry("position")] = extruder_train
changed = True
if changed:
self.extrudersChanged.emit(machine_id)
## Creates a container stack for an extruder train.
#
# The container stack has an extruder definition at the bottom, which is

View file

@ -246,6 +246,7 @@ class MachineManager(QObject):
quality = self._global_container_stack.findContainer({"type": "quality"})
quality.nameChanged.connect(self._onQualityNameChanged)
self._updateStacksHaveErrors()
## Update self._stacks_valid according to _checkStacksForErrors and emit if change.
def _updateStacksHaveErrors(self):
@ -277,7 +278,7 @@ class MachineManager(QObject):
def _onInstanceContainersChanged(self, container):
container_type = container.getMetaDataEntry("type")
if container_type == "material":
self.activeMaterialChanged.emit()
elif container_type == "variant":
@ -285,6 +286,8 @@ class MachineManager(QObject):
elif container_type == "quality":
self.activeQualityChanged.emit()
self._updateStacksHaveErrors()
def _onPropertyChanged(self, key, property_name):
if property_name == "value":
# Notify UI items, such as the "changed" star in profile pull down menu.
@ -297,6 +300,16 @@ class MachineManager(QObject):
changed_validation_state = self._active_container_stack.getProperty(key, property_name)
else:
changed_validation_state = self._global_container_stack.getProperty(key, property_name)
if changed_validation_state is None:
# Setting is not validated. This can happen if there is only a setting definition.
# We do need to validate it, because a setting defintions value can be set by a function, which could
# be an invalid setting.
definition = self._active_container_stack.getSettingDefinition(key)
validator_type = UM.Settings.SettingDefinition.getValidatorForType(definition.type)
if validator_type:
validator = validator_type(key)
changed_validation_state = validator(self._active_container_stack)
if changed_validation_state in (UM.Settings.ValidatorState.Exception, UM.Settings.ValidatorState.MaximumError, UM.Settings.ValidatorState.MinimumError):
self._stacks_have_errors = True
self.stacksValidationChanged.emit()
@ -871,7 +884,7 @@ class MachineManager(QObject):
def _askUserToKeepOrClearCurrentSettings(self):
# Ask the user if the user profile should be cleared or not (discarding the current settings)
# In Simple Mode we assume the user always wants to keep the (limited) current settings
details_text = catalog.i18nc("@label", "You made changes to the following setting(s):")
details_text = catalog.i18nc("@label", "You made changes to the following setting(s)/override(s):")
# user changes in global stack
details_list = [setting.definition.label for setting in self._global_container_stack.getTop().findInstances(**{})]
@ -886,14 +899,19 @@ class MachineManager(QObject):
# Format to output string
details = "\n ".join([details_text, ] + details_list)
Application.getInstance().messageBox(catalog.i18nc("@window:title", "Switched profiles"),
catalog.i18nc("@label",
"Do you want to transfer your changed settings to this profile?"),
catalog.i18nc("@label",
"If you transfer your settings they will override settings in the profile."),
details,
buttons=QMessageBox.Yes + QMessageBox.No, icon=QMessageBox.Question,
callback=self._keepUserSettingsDialogCallback)
num_changed_settings = len(details_list)
Application.getInstance().messageBox(
catalog.i18nc("@window:title", "Switched profiles"),
catalog.i18nc(
"@label",
"Do you want to transfer your %d changed setting(s)/override(s) to this profile?") % num_changed_settings,
catalog.i18nc(
"@label",
"If you transfer your settings they will override settings in the profile. If you don't transfer these settings, they will be lost."),
details,
buttons=QMessageBox.Yes + QMessageBox.No,
icon=QMessageBox.Question,
callback=self._keepUserSettingsDialogCallback)
def _keepUserSettingsDialogCallback(self, button):
if button == QMessageBox.Yes:

View file

@ -178,6 +178,12 @@ class QualitySettingsModel(UM.Qt.ListModel.ListModel):
profile_value_source = container.getMetaDataEntry("type")
profile_value = new_value
# Global tab should use resolve (if there is one)
if not self._extruder_id:
resolve_value = global_container_stack.getProperty(definition.key, "resolve")
if resolve_value is not None and profile_value is not None:
profile_value = resolve_value
user_value = None
if not self._extruder_id:
user_value = global_container_stack.getTop().getProperty(definition.key, "value")

View file

@ -85,7 +85,7 @@ class SettingInheritanceManager(QObject):
self._update() # Ensure that the settings_with_inheritance_warning list is populated.
def _onPropertyChanged(self, key, property_name):
if property_name == "value" and self._global_container_stack:
if (property_name == "value" or property_name == "enabled") and self._global_container_stack:
definitions = self._global_container_stack.getBottom().findDefinitions(key = key)
if not definitions:
return
@ -167,7 +167,16 @@ class SettingInheritanceManager(QObject):
continue
if value is not None:
# If a setting doesn't use any keys, it won't change it's value, so treat it as if it's a fixed value
has_setting_function = isinstance(value, UM.Settings.SettingFunction) and len(value.getUsedSettingKeys()) > 0
has_setting_function = isinstance(value, UM.Settings.SettingFunction)
if has_setting_function:
for setting_key in value.getUsedSettingKeys():
if setting_key in self._active_container_stack.getAllKeys():
break # We found an actual setting. So has_setting_function can remain true
else:
# All of the setting_keys turned out to not be setting keys at all!
# This can happen due enum keys also being marked as settings.
has_setting_function = False
if has_setting_function is False:
has_non_function_value = True
continue

View file

@ -84,20 +84,20 @@ class ThreeMFReader(MeshReader):
definition = QualityManager.getInstance().getParentMachineDefinition(global_container_stack.getBottom())
node.callDecoration("getStack").getTop().setDefinition(definition)
setting_container = node.callDecoration("getStack").getTop()
for setting in xml_settings:
setting_key = setting.get("key")
setting_value = setting.text
setting_container = node.callDecoration("getStack").getTop()
for setting in xml_settings:
setting_key = setting.get("key")
setting_value = setting.text
# Extruder_nr is a special case.
if setting_key == "extruder_nr":
extruder_stack = ExtruderManager.getInstance().getExtruderStack(int(setting_value))
if extruder_stack:
node.callDecoration("setActiveExtruder", extruder_stack.getId())
else:
Logger.log("w", "Unable to find extruder in position %s", setting_value)
continue
setting_container.setProperty(setting_key,"value", setting_value)
# Extruder_nr is a special case.
if setting_key == "extruder_nr":
extruder_stack = ExtruderManager.getInstance().getExtruderStack(int(setting_value))
if extruder_stack:
node.callDecoration("setActiveExtruder", extruder_stack.getId())
else:
Logger.log("w", "Unable to find extruder in position %s", setting_value)
continue
setting_container.setProperty(setting_key,"value", setting_value)
if len(node.getChildren()) > 0:
group_decorator = GroupDecorator()

View file

@ -0,0 +1,405 @@
from UM.Workspace.WorkspaceReader import WorkspaceReader
from UM.Application import Application
from UM.Logger import Logger
from UM.i18n import i18nCatalog
from UM.Settings.ContainerStack import ContainerStack
from UM.Settings.DefinitionContainer import DefinitionContainer
from UM.Settings.InstanceContainer import InstanceContainer
from UM.Settings.ContainerRegistry import ContainerRegistry
from UM.MimeTypeDatabase import MimeTypeDatabase
from UM.Job import Job
from UM.Preferences import Preferences
from .WorkspaceDialog import WorkspaceDialog
from cura.Settings.ExtruderManager import ExtruderManager
import zipfile
import io
import configparser
i18n_catalog = i18nCatalog("cura")
## Base implementation for reading 3MF workspace files.
class ThreeMFWorkspaceReader(WorkspaceReader):
def __init__(self):
super().__init__()
self._supported_extensions = [".3mf"]
self._dialog = WorkspaceDialog()
self._3mf_mesh_reader = None
self._container_registry = ContainerRegistry.getInstance()
self._definition_container_suffix = ContainerRegistry.getMimeTypeForContainer(DefinitionContainer).preferredSuffix
self._material_container_suffix = None # We have to wait until all other plugins are loaded before we can set it
self._instance_container_suffix = ContainerRegistry.getMimeTypeForContainer(InstanceContainer).preferredSuffix
self._container_stack_suffix = ContainerRegistry.getMimeTypeForContainer(ContainerStack).preferredSuffix
self._resolve_strategies = {}
self._id_mapping = {}
## Get a unique name based on the old_id. This is different from directly calling the registry in that it caches results.
# This has nothing to do with speed, but with getting consistent new naming for instances & objects.
def getNewId(self, old_id):
if old_id not in self._id_mapping:
self._id_mapping[old_id] = self._container_registry.uniqueName(old_id)
return self._id_mapping[old_id]
def preRead(self, file_name):
self._3mf_mesh_reader = Application.getInstance().getMeshFileHandler().getReaderForFile(file_name)
if self._3mf_mesh_reader and self._3mf_mesh_reader.preRead(file_name) == WorkspaceReader.PreReadResult.accepted:
pass
else:
Logger.log("w", "Could not find reader that was able to read the scene data for 3MF workspace")
return WorkspaceReader.PreReadResult.failed
# Check if there are any conflicts, so we can ask the user.
archive = zipfile.ZipFile(file_name, "r")
cura_file_names = [name for name in archive.namelist() if name.startswith("Cura/")]
container_stack_files = [name for name in cura_file_names if name.endswith(self._container_stack_suffix)]
self._resolve_strategies = {"machine": None, "quality_changes": None, "material": None}
machine_conflict = False
quality_changes_conflict = False
for container_stack_file in container_stack_files:
container_id = self._stripFileToId(container_stack_file)
stacks = self._container_registry.findContainerStacks(id=container_id)
if stacks:
# Check if there are any changes at all in any of the container stacks.
id_list = self._getContainerIdListFromSerialized(archive.open(container_stack_file).read().decode("utf-8"))
for index, container_id in enumerate(id_list):
if stacks[0].getContainer(index).getId() != container_id:
machine_conflict = True
break
Job.yieldThread()
material_conflict = False
xml_material_profile = self._getXmlProfileClass()
if self._material_container_suffix is None:
self._material_container_suffix = ContainerRegistry.getMimeTypeForContainer(xml_material_profile).preferredSuffix
if xml_material_profile:
material_container_files = [name for name in cura_file_names if name.endswith(self._material_container_suffix)]
for material_container_file in material_container_files:
container_id = self._stripFileToId(material_container_file)
materials = self._container_registry.findInstanceContainers(id=container_id)
if materials and not materials[0].isReadOnly(): # Only non readonly materials can be in conflict
material_conflict = True
Job.yieldThread()
# Check if any quality_changes instance container is in conflict.
instance_container_files = [name for name in cura_file_names if name.endswith(self._instance_container_suffix)]
for instance_container_file in instance_container_files:
container_id = self._stripFileToId(instance_container_file)
instance_container = InstanceContainer(container_id)
# Deserialize InstanceContainer by converting read data from bytes to string
instance_container.deserialize(archive.open(instance_container_file).read().decode("utf-8"))
container_type = instance_container.getMetaDataEntry("type")
if container_type == "quality_changes":
# Check if quality changes already exists.
quality_changes = self._container_registry.findInstanceContainers(id = container_id)
if quality_changes:
# Check if there really is a conflict by comparing the values
if quality_changes[0] != instance_container:
quality_changes_conflict = True
break
Job.yieldThread()
try:
archive.open("Cura/preferences.cfg")
except KeyError:
# If there is no preferences file, it's not a workspace, so notify user of failure.
Logger.log("w", "File %s is not a valid workspace.", file_name)
return WorkspaceReader.PreReadResult.failed
if machine_conflict or quality_changes_conflict or material_conflict:
# There is a conflict; User should choose to either update the existing data, add everything as new data or abort
self._dialog.setMachineConflict(machine_conflict)
self._dialog.setQualityChangesConflict(quality_changes_conflict)
self._dialog.setMaterialConflict(material_conflict)
self._dialog.show()
# Block until the dialog is closed.
self._dialog.waitForClose()
if self._dialog.getResult() == {}:
return WorkspaceReader.PreReadResult.cancelled
self._resolve_strategies = self._dialog.getResult()
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/")]
# Create a shadow copy of the preferences (we don't want all of the preferences, but we do want to re-use its
# parsing code.
temp_preferences = Preferences()
temp_preferences.readFromFile(io.TextIOWrapper(archive.open("Cura/preferences.cfg"))) # We need to wrap it, else the archive parser breaks.
# 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"))
Application.getInstance().expandedCategoriesChanged.emit() # Notify the GUI of the change
self._id_mapping = {}
# We don't add containers right away, but wait right until right before the stack serialization.
# We do this so that if something goes wrong, it's easier to clean up.
containers_to_add = []
# TODO: For the moment we use pretty naive existence checking. If the ID is the same, we assume in quite a few
# TODO: cases that the container loaded is the same (most notable in materials & definitions).
# TODO: It might be possible that we need to add smarter checking in the future.
Logger.log("d", "Workspace loading is checking definitions...")
# Get all the definition files & check if they exist. If not, add them.
definition_container_files = [name for name in cura_file_names if name.endswith(self._definition_container_suffix)]
for definition_container_file in definition_container_files:
container_id = self._stripFileToId(definition_container_file)
definitions = self._container_registry.findDefinitionContainers(id=container_id)
if not definitions:
definition_container = DefinitionContainer(container_id)
definition_container.deserialize(archive.open(definition_container_file).read().decode("utf-8"))
self._container_registry.addContainer(definition_container)
Job.yieldThread()
Logger.log("d", "Workspace loading is checking materials...")
material_containers = []
# Get all the material files and check if they exist. If not, add them.
xml_material_profile = self._getXmlProfileClass()
if self._material_container_suffix is None:
self._material_container_suffix = ContainerRegistry.getMimeTypeForContainer(xml_material_profile).suffixes[0]
if xml_material_profile:
material_container_files = [name for name in cura_file_names if name.endswith(self._material_container_suffix)]
for material_container_file in material_container_files:
container_id = self._stripFileToId(material_container_file)
materials = self._container_registry.findInstanceContainers(id=container_id)
if not materials:
material_container = xml_material_profile(container_id)
material_container.deserialize(archive.open(material_container_file).read().decode("utf-8"))
containers_to_add.append(material_container)
else:
if not materials[0].isReadOnly(): # Only create new materials if they are not read only.
if self._resolve_strategies["material"] == "override":
materials[0].deserialize(archive.open(material_container_file).read().decode("utf-8"))
elif self._resolve_strategies["material"] == "new":
# Note that we *must* deserialize it with a new ID, as multiple containers will be
# auto created & added.
material_container = xml_material_profile(self.getNewId(container_id))
material_container.deserialize(archive.open(material_container_file).read().decode("utf-8"))
containers_to_add.append(material_container)
material_containers.append(material_container)
Job.yieldThread()
Logger.log("d", "Workspace loading is checking instance containers...")
# Get quality_changes and user profiles saved in the workspace
instance_container_files = [name for name in cura_file_names if name.endswith(self._instance_container_suffix)]
user_instance_containers = []
quality_changes_instance_containers = []
for instance_container_file in instance_container_files:
container_id = self._stripFileToId(instance_container_file)
instance_container = InstanceContainer(container_id)
# Deserialize InstanceContainer by converting read data from bytes to string
instance_container.deserialize(archive.open(instance_container_file).read().decode("utf-8"))
container_type = instance_container.getMetaDataEntry("type")
Job.yieldThread()
if container_type == "user":
# Check if quality changes already exists.
user_containers = self._container_registry.findInstanceContainers(id=container_id)
if not user_containers:
containers_to_add.append(instance_container)
else:
if self._resolve_strategies["machine"] == "override" or self._resolve_strategies["machine"] is None:
user_containers[0].deserialize(archive.open(instance_container_file).read().decode("utf-8"))
elif self._resolve_strategies["machine"] == "new":
# The machine is going to get a spiffy new name, so ensure that the id's of user settings match.
extruder_id = instance_container.getMetaDataEntry("extruder", None)
if extruder_id:
new_id = self.getNewId(extruder_id) + "_current_settings"
instance_container._id = new_id
instance_container.setName(new_id)
instance_container.setMetaDataEntry("extruder", self.getNewId(extruder_id))
containers_to_add.append(instance_container)
machine_id = instance_container.getMetaDataEntry("machine", None)
if machine_id:
new_id = self.getNewId(machine_id) + "_current_settings"
instance_container._id = new_id
instance_container.setName(new_id)
instance_container.setMetaDataEntry("machine", self.getNewId(machine_id))
containers_to_add.append(instance_container)
user_instance_containers.append(instance_container)
elif container_type == "quality_changes":
# Check if quality changes already exists.
quality_changes = self._container_registry.findInstanceContainers(id = container_id)
if not quality_changes:
containers_to_add.append(instance_container)
else:
if self._resolve_strategies["quality_changes"] == "override":
quality_changes[0].deserialize(archive.open(instance_container_file).read().decode("utf-8"))
elif self._resolve_strategies["quality_changes"] is None:
# The ID already exists, but nothing in the values changed, so do nothing.
pass
quality_changes_instance_containers.append(instance_container)
else:
continue
# Add all the containers right before we try to add / serialize the stack
for container in containers_to_add:
self._container_registry.addContainer(container)
# Get the stack(s) saved in the workspace.
Logger.log("d", "Workspace loading is checking stacks containers...")
container_stack_files = [name for name in cura_file_names if name.endswith(self._container_stack_suffix)]
global_stack = None
extruder_stacks = []
container_stacks_added = []
try:
for container_stack_file in container_stack_files:
container_id = self._stripFileToId(container_stack_file)
# Check if a stack by this ID already exists;
container_stacks = self._container_registry.findContainerStacks(id=container_id)
if container_stacks:
stack = container_stacks[0]
if self._resolve_strategies["machine"] == "override":
container_stacks[0].deserialize(archive.open(container_stack_file).read().decode("utf-8"))
elif self._resolve_strategies["machine"] == "new":
new_id = self.getNewId(container_id)
stack = ContainerStack(new_id)
stack.deserialize(archive.open(container_stack_file).read().decode("utf-8"))
# Ensure a unique ID and name
stack._id = new_id
# Extruder stacks are "bound" to a machine. If we add the machine as a new one, the id of the
# bound machine also needs to change.
if stack.getMetaDataEntry("machine", None):
stack.setMetaDataEntry("machine", self.getNewId(stack.getMetaDataEntry("machine")))
if stack.getMetaDataEntry("type") != "extruder_train":
# Only machines need a new name, stacks may be non-unique
stack.setName(self._container_registry.uniqueName(stack.getName()))
container_stacks_added.append(stack)
self._container_registry.addContainer(stack)
else:
Logger.log("w", "Resolve strategy of %s for machine is not supported", self._resolve_strategies["machine"])
else:
stack = ContainerStack(container_id)
# Deserialize stack by converting read data from bytes to string
stack.deserialize(archive.open(container_stack_file).read().decode("utf-8"))
container_stacks_added.append(stack)
self._container_registry.addContainer(stack)
if stack.getMetaDataEntry("type") == "extruder_train":
extruder_stacks.append(stack)
else:
global_stack = stack
Job.yieldThread()
except:
Logger.log("W", "We failed to serialize the stack. Trying to clean up.")
# Something went really wrong. Try to remove any data that we added.
for container in containers_to_add:
self._container_registry.getInstance().removeContainer(container.getId())
for container in container_stacks_added:
self._container_registry.getInstance().removeContainer(container.getId())
return None
if self._resolve_strategies["machine"] == "new":
# A new machine was made, but it was serialized with the wrong user container. Fix that now.
for container in user_instance_containers:
extruder_id = container.getMetaDataEntry("extruder", None)
if extruder_id:
for extruder in extruder_stacks:
if extruder.getId() == extruder_id:
extruder.replaceContainer(0, container)
continue
machine_id = container.getMetaDataEntry("machine", None)
if machine_id:
if global_stack.getId() == machine_id:
global_stack.replaceContainer(0, container)
continue
if self._resolve_strategies["quality_changes"] == "new":
# Quality changes needs to get a new ID, added to registry and to the right stacks
for container in quality_changes_instance_containers:
old_id = container.getId()
container.setName(self._container_registry.uniqueName(container.getName()))
# We're not really supposed to change the ID in normal cases, but this is an exception.
container._id = self.getNewId(container.getId())
# The container was not added yet, as it didn't have an unique ID. It does now, so add it.
self._container_registry.addContainer(container)
# Replace the quality changes container
old_container = global_stack.findContainer({"type": "quality_changes"})
if old_container.getId() == old_id:
quality_changes_index = global_stack.getContainerIndex(old_container)
global_stack.replaceContainer(quality_changes_index, container)
continue
for stack in extruder_stacks:
old_container = stack.findContainer({"type": "quality_changes"})
if old_container.getId() == old_id:
quality_changes_index = stack.getContainerIndex(old_container)
stack.replaceContainer(quality_changes_index, container)
if self._resolve_strategies["material"] == "new":
for material in material_containers:
old_material = global_stack.findContainer({"type": "material"})
if old_material.getId() in self._id_mapping:
material_index = global_stack.getContainerIndex(old_material)
global_stack.replaceContainer(material_index, material)
continue
for stack in extruder_stacks:
old_material = stack.findContainer({"type": "material"})
if old_material.getId() in self._id_mapping:
material_index = stack.getContainerIndex(old_material)
stack.replaceContainer(material_index, material)
continue
for stack in extruder_stacks:
ExtruderManager.getInstance().registerExtruder(stack, global_stack.getId())
else:
# Machine has no extruders, but it needs to be registered with the extruder manager.
ExtruderManager.getInstance().registerExtruder(None, global_stack.getId())
Logger.log("d", "Workspace loading is notifying rest of the code of changes...")
# Notify everything/one that is to notify about changes.
for container in global_stack.getContainers():
global_stack.containersChanged.emit(container)
Job.yieldThread()
for stack in extruder_stacks:
stack.setNextStack(global_stack)
for container in stack.getContainers():
stack.containersChanged.emit(container)
Job.yieldThread()
# Actually change the active machine.
Application.getInstance().setGlobalContainerStack(global_stack)
return nodes
def _stripFileToId(self, file):
return file.replace("Cura/", "").split(".")[0]
def _getXmlProfileClass(self):
return self._container_registry.getContainerForMimeType(MimeTypeDatabase.getMimeType("application/x-ultimaker-material-profile"))
## Get the list of ID's of all containers in a container stack by partially parsing it's serialized data.
def _getContainerIdListFromSerialized(self, serialized):
parser = configparser.ConfigParser(interpolation=None, empty_lines_in_values=False)
parser.read_string(serialized)
container_string = parser["general"].get("containers", "")
container_list = container_string.split(",")
return [container_id for container_id in container_list if container_id != ""]

View file

@ -0,0 +1,134 @@
from PyQt5.QtCore import Qt, QUrl, pyqtSignal, pyqtSlot, QObject, pyqtProperty, QCoreApplication
from PyQt5.QtQml import QQmlComponent, QQmlContext
from UM.PluginRegistry import PluginRegistry
from UM.Application import Application
from UM.Logger import Logger
import os
import threading
import time
class WorkspaceDialog(QObject):
showDialogSignal = pyqtSignal()
def __init__(self, parent = None):
super().__init__(parent)
self._component = None
self._context = None
self._view = None
self._qml_url = "WorkspaceDialog.qml"
self._lock = threading.Lock()
self._default_strategy = "override"
self._result = {"machine": self._default_strategy,
"quality_changes": self._default_strategy,
"material": self._default_strategy}
self._visible = False
self.showDialogSignal.connect(self.__show)
self._has_quality_changes_conflict = False
self._has_machine_conflict = False
self._has_material_conflict = False
machineConflictChanged = pyqtSignal()
qualityChangesConflictChanged = pyqtSignal()
materialConflictChanged = pyqtSignal()
@pyqtProperty(bool, notify = machineConflictChanged)
def machineConflict(self):
return self._has_machine_conflict
@pyqtProperty(bool, notify=qualityChangesConflictChanged)
def qualityChangesConflict(self):
return self._has_quality_changes_conflict
@pyqtProperty(bool, notify=materialConflictChanged)
def materialConflict(self):
return self._has_material_conflict
@pyqtSlot(str, str)
def setResolveStrategy(self, key, strategy):
if key in self._result:
self._result[key] = strategy
def setMaterialConflict(self, material_conflict):
self._has_material_conflict = material_conflict
self.materialConflictChanged.emit()
def setMachineConflict(self, machine_conflict):
self._has_machine_conflict = machine_conflict
self.machineConflictChanged.emit()
def setQualityChangesConflict(self, quality_changes_conflict):
self._has_quality_changes_conflict = quality_changes_conflict
self.qualityChangesConflictChanged.emit()
def getResult(self):
if "machine" in self._result and not self._has_machine_conflict:
self._result["machine"] = None
if "quality_changes" in self._result and not self._has_quality_changes_conflict:
self._result["quality_changes"] = None
if "material" in self._result and not self._has_material_conflict:
self._result["material"] = None
return self._result
def _createViewFromQML(self):
path = QUrl.fromLocalFile(os.path.join(PluginRegistry.getInstance().getPluginPath("3MFReader"), self._qml_url))
self._component = QQmlComponent(Application.getInstance()._engine, path)
self._context = QQmlContext(Application.getInstance()._engine.rootContext())
self._context.setContextProperty("manager", self)
self._view = self._component.create(self._context)
if self._view is None:
Logger.log("c", "QQmlComponent status %s", self._component.status())
Logger.log("c", "QQmlComponent error string %s", self._component.errorString())
def show(self):
# Emit signal so the right thread actually shows the view.
if threading.current_thread() != threading.main_thread():
self._lock.acquire()
# Reset the result
self._result = {"machine": self._default_strategy,
"quality_changes": self._default_strategy,
"material": self._default_strategy}
self._visible = True
self.showDialogSignal.emit()
@pyqtSlot()
## Used to notify the dialog so the lock can be released.
def notifyClosed(self):
if self._result is None:
self._result = {}
self._lock.release()
def hide(self):
self._visible = False
self._lock.release()
self._view.hide()
@pyqtSlot()
def onOkButtonClicked(self):
self._view.hide()
self.hide()
@pyqtSlot()
def onCancelButtonClicked(self):
self._view.hide()
self.hide()
self._result = {}
## Block thread until the dialog is closed.
def waitForClose(self):
if self._visible:
if threading.current_thread() != threading.main_thread():
self._lock.acquire()
self._lock.release()
else:
# If this is not run from a separate thread, we need to ensure that the events are still processed.
while self._visible:
time.sleep(1 / 50)
QCoreApplication.processEvents() # Ensure that the GUI does not freeze.
def __show(self):
if self._view is None:
self._createViewFromQML()
if self._view:
self._view.show()

View file

@ -0,0 +1,172 @@
// Copyright (c) 2016 Ultimaker B.V.
// Cura is released under the terms of the AGPLv3 or higher.
import QtQuick 2.1
import QtQuick.Controls 1.1
import QtQuick.Layouts 1.1
import QtQuick.Window 2.1
import UM 1.1 as UM
UM.Dialog
{
title: catalog.i18nc("@title:window", "Import workspace conflict")
width: 350 * Screen.devicePixelRatio;
minimumWidth: 350 * Screen.devicePixelRatio;
maximumWidth: 350 * Screen.devicePixelRatio;
height: 250 * Screen.devicePixelRatio;
minimumHeight: 250 * Screen.devicePixelRatio;
maximumHeight: 250 * Screen.devicePixelRatio;
onClosing: manager.notifyClosed()
onVisibleChanged:
{
if(visible)
{
machineResolveComboBox.currentIndex = 0
qualityChangesResolveComboBox.currentIndex = 0
materialConflictComboBox.currentIndex = 0
}
}
Item
{
anchors.fill: parent
UM.I18nCatalog
{
id: catalog;
name: "cura";
}
ListModel
{
id: resolveStrategiesModel
// Instead of directly adding the list elements, we add them afterwards.
// This is because it's impossible to use setting function results to be bound to listElement properties directly.
// See http://stackoverflow.com/questions/7659442/listelement-fields-as-properties
Component.onCompleted:
{
append({"key": "override", "label": catalog.i18nc("@action:ComboBox option", "Override existing")});
append({"key": "new", "label": catalog.i18nc("@action:ComboBox option", "Create new")});
}
}
Column
{
anchors.fill: parent
Label
{
id: infoLabel
width: parent.width
text: catalog.i18nc("@action:label", "Cura detected a number of conflicts while importing the workspace. How would you like to resolve these?")
wrapMode: Text.Wrap
height: 50
}
UM.TooltipArea
{
id: machineResolveTooltip
width: parent.width
height: visible ? 25 : 0
text: catalog.i18nc("@info:tooltip", "How should the conflict in the machine be resolved?")
visible: manager.machineConflict
Row
{
width: parent.width
height: childrenRect.height
Label
{
text: catalog.i18nc("@action:label","Machine")
width: 150
}
ComboBox
{
model: resolveStrategiesModel
textRole: "label"
id: machineResolveComboBox
onActivated:
{
manager.setResolveStrategy("machine", resolveStrategiesModel.get(index).key)
}
}
}
}
UM.TooltipArea
{
id: qualityChangesResolveTooltip
width: parent.width
height: visible ? 25 : 0
text: catalog.i18nc("@info:tooltip", "How should the conflict in the profile be resolved?")
visible: manager.qualityChangesConflict
Row
{
width: parent.width
height: childrenRect.height
Label
{
text: catalog.i18nc("@action:label","Profile")
width: 150
}
ComboBox
{
model: resolveStrategiesModel
textRole: "label"
id: qualityChangesResolveComboBox
onActivated:
{
manager.setResolveStrategy("quality_changes", resolveStrategiesModel.get(index).key)
}
}
}
}
UM.TooltipArea
{
id: materialResolveTooltip
width: parent.width
height: visible ? 25 : 0
text: catalog.i18nc("@info:tooltip", "How should the conflict in the material(s) be resolved?")
visible: manager.materialConflict
Row
{
width: parent.width
height: childrenRect.height
Label
{
text: catalog.i18nc("@action:label","Material")
width: 150
}
ComboBox
{
model: resolveStrategiesModel
textRole: "label"
id: materialResolveComboBox
onActivated:
{
manager.setResolveStrategy("material", resolveStrategiesModel.get(index).key)
}
}
}
}
}
}
rightButtons: [
Button
{
id: ok_button
text: catalog.i18nc("@action:button","OK");
onClicked: { manager.onOkButtonClicked() }
enabled: true
},
Button
{
id: cancel_button
text: catalog.i18nc("@action:button","Cancel");
onClicked: { manager.onCancelButtonClicked() }
enabled: true
}
]
}

View file

@ -2,10 +2,11 @@
# Cura is released under the terms of the AGPLv3 or higher.
from . import ThreeMFReader
from . import ThreeMFWorkspaceReader
from UM.i18n import i18nCatalog
catalog = i18nCatalog("cura")
def getMetaData():
return {
"plugin": {
@ -20,8 +21,17 @@ def getMetaData():
"extension": "3mf",
"description": catalog.i18nc("@item:inlistbox", "3MF File")
}
],
"workspace_reader":
[
{
"extension": "3mf",
"description": catalog.i18nc("@item:inlistbox", "3MF File")
}
]
}
def register(app):
return { "mesh_reader": ThreeMFReader.ThreeMFReader() }
return {"mesh_reader": ThreeMFReader.ThreeMFReader(),
"workspace_reader": ThreeMFWorkspaceReader.ThreeMFWorkspaceReader()}

View file

@ -0,0 +1,78 @@
from UM.Workspace.WorkspaceWriter import WorkspaceWriter
from UM.Application import Application
from UM.Preferences import Preferences
from UM.Settings.ContainerRegistry import ContainerRegistry
from cura.Settings.ExtruderManager import ExtruderManager
import zipfile
from io import StringIO
class ThreeMFWorkspaceWriter(WorkspaceWriter):
def __init__(self):
super().__init__()
def write(self, stream, nodes, mode=WorkspaceWriter.OutputMode.BinaryMode):
mesh_writer = Application.getInstance().getMeshFileHandler().getWriter("3MFWriter")
if not mesh_writer: # We need to have the 3mf mesh writer, otherwise we can't save the entire workspace
return False
# Indicate that the 3mf mesh writer should not close the archive just yet (we still need to add stuff to it).
mesh_writer.setStoreArchive(True)
mesh_writer.write(stream, nodes, mode)
archive = mesh_writer.getArchive()
if archive is None: # This happens if there was no mesh data to write.
archive = zipfile.ZipFile(stream, "w", compression = zipfile.ZIP_DEFLATED)
global_container_stack = Application.getInstance().getGlobalContainerStack()
# Add global container stack data to the archive.
self._writeContainerToArchive(global_container_stack, archive)
# Also write all containers in the stack to the file
for container in global_container_stack.getContainers():
self._writeContainerToArchive(container, archive)
# Check if the machine has extruders and save all that data as well.
for extruder_stack in ExtruderManager.getInstance().getMachineExtruders(global_container_stack.getId()):
self._writeContainerToArchive(extruder_stack, archive)
for container in extruder_stack.getContainers():
self._writeContainerToArchive(container, archive)
# Write preferences to archive
preferences_file = zipfile.ZipInfo("Cura/preferences.cfg")
preferences_string = StringIO()
Preferences.getInstance().writeToFile(preferences_string)
archive.writestr(preferences_file, preferences_string.getvalue())
# Close the archive & reset states.
archive.close()
mesh_writer.setStoreArchive(False)
return True
## Helper function that writes ContainerStacks, InstanceContainers and DefinitionContainers to the archive.
# \param container That follows the \type{ContainerInterface} to archive.
# \param archive The archive to write to.
@staticmethod
def _writeContainerToArchive(container, archive):
if type(container) == type(ContainerRegistry.getInstance().getEmptyInstanceContainer()):
return # Empty file, do nothing.
file_suffix = ContainerRegistry.getMimeTypeForContainer(type(container)).preferredSuffix
# Some containers have a base file, which should then be the file to use.
if "base_file" in container.getMetaData():
base_file = container.getMetaDataEntry("base_file")
container = ContainerRegistry.getInstance().findContainers(id = base_file)[0]
file_name = "Cura/%s.%s" % (container.getId(), file_suffix)
if file_name in archive.namelist():
return # File was already saved, no need to do it again. Uranium guarantees unique ID's, so this should hold.
file_in_archive = zipfile.ZipInfo(file_name)
# For some reason we have to set the compress type of each file as well (it doesn't keep the type of the entire archive)
file_in_archive.compress_type = zipfile.ZIP_DEFLATED
archive.writestr(file_in_archive, container.serialize())

View file

@ -0,0 +1,201 @@
# Copyright (c) 2015 Ultimaker B.V.
# Uranium is released under the terms of the AGPLv3 or higher.
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
try:
import xml.etree.cElementTree as ET
except ImportError:
Logger.log("w", "Unable to load cElementTree, switching to slower version")
import xml.etree.ElementTree as ET
import zipfile
import UM.Application
class ThreeMFWriter(MeshWriter):
def __init__(self):
super().__init__()
self._namespaces = {
"3mf": "http://schemas.microsoft.com/3dmanufacturing/core/2015/02",
"content-types": "http://schemas.openxmlformats.org/package/2006/content-types",
"relationships": "http://schemas.openxmlformats.org/package/2006/relationships",
"cura": "http://software.ultimaker.com/xml/cura/3mf/2015/10"
}
self._unit_matrix_string = self._convertMatrixToString(Matrix())
self._archive = None
self._store_archive = False
def _convertMatrixToString(self, matrix):
result = ""
result += str(matrix._data[0,0]) + " "
result += str(matrix._data[1,0]) + " "
result += str(matrix._data[2,0]) + " "
result += str(matrix._data[0,1]) + " "
result += str(matrix._data[1,1]) + " "
result += str(matrix._data[2,1]) + " "
result += str(matrix._data[0,2]) + " "
result += str(matrix._data[1,2]) + " "
result += str(matrix._data[2,2]) + " "
result += str(matrix._data[0,3]) + " "
result += str(matrix._data[1,3]) + " "
result += str(matrix._data[2,3]) + " "
return result
## Should we store the archive
# Note that if this is true, the archive will not be closed.
# The object that set this parameter is then responsible for closing it correctly!
def setStoreArchive(self, store_archive):
self._store_archive = store_archive
def getArchive(self):
return self._archive
def write(self, stream, nodes, mode = MeshWriter.OutputMode.BinaryMode):
self._archive = None # Reset archive
archive = zipfile.ZipFile(stream, "w", compression = zipfile.ZIP_DEFLATED)
try:
model_file = zipfile.ZipInfo("3D/3dmodel.model")
# Because zipfile is stupid and ignores archive-level compression settings when writing with ZipInfo.
model_file.compress_type = zipfile.ZIP_DEFLATED
# Create content types file
content_types_file = zipfile.ZipInfo("[Content_Types].xml")
content_types_file.compress_type = zipfile.ZIP_DEFLATED
content_types = ET.Element("Types", xmlns = self._namespaces["content-types"])
rels_type = ET.SubElement(content_types, "Default", Extension = "rels", ContentType = "application/vnd.openxmlformats-package.relationships+xml")
model_type = ET.SubElement(content_types, "Default", Extension = "model", ContentType = "application/vnd.ms-package.3dmanufacturing-3dmodel+xml")
# Create _rels/.rels file
relations_file = zipfile.ZipInfo("_rels/.rels")
relations_file.compress_type = zipfile.ZIP_DEFLATED
relations_element = ET.Element("Relationships", xmlns = self._namespaces["relationships"])
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"])
resources = ET.SubElement(model, "resources")
build = ET.SubElement(model, "build")
added_nodes = []
index = 0 # Ensure index always exists (even if there are no nodes to write)
# Write all nodes with meshData to the file as objects inside the resource tag
for index, n in enumerate(MeshWriter._meshNodes(nodes)):
added_nodes.append(n) # Save the nodes that have mesh data
object = ET.SubElement(resources, "object", id = str(index+1), type = "model")
mesh = ET.SubElement(object, "mesh")
mesh_data = n.getMeshData()
vertices = ET.SubElement(mesh, "vertices")
verts = mesh_data.getVertices()
if verts is None:
Logger.log("d", "3mf writer can't write nodes without mesh data. Skipping this node.")
continue # No mesh data, nothing to do.
if mesh_data.hasIndices():
for face in mesh_data.getIndices():
v1 = verts[face[0]]
v2 = verts[face[1]]
v3 = verts[face[2]]
xml_vertex1 = ET.SubElement(vertices, "vertex", x = str(v1[0]), y = str(v1[1]), z = str(v1[2]))
xml_vertex2 = ET.SubElement(vertices, "vertex", x = str(v2[0]), y = str(v2[1]), z = str(v2[2]))
xml_vertex3 = ET.SubElement(vertices, "vertex", x = str(v3[0]), y = str(v3[1]), z = str(v3[2]))
triangles = ET.SubElement(mesh, "triangles")
for face in mesh_data.getIndices():
triangle = ET.SubElement(triangles, "triangle", v1 = str(face[0]) , v2 = str(face[1]), v3 = str(face[2]))
else:
triangles = ET.SubElement(mesh, "triangles")
for idx, vert in enumerate(verts):
xml_vertex = ET.SubElement(vertices, "vertex", x = str(vert[0]), y = str(vert[1]), z = str(vert[2]))
# If we have no faces defined, assume that every three subsequent vertices form a face.
if idx % 3 == 0:
triangle = ET.SubElement(triangles, "triangle", v1 = str(idx), v2 = str(idx + 1), v3 = str(idx + 2))
# Handle per object settings
stack = n.callDecoration("getStack")
if stack is not None:
changed_setting_keys = set(stack.getTop().getAllKeys())
# Ensure that we save the extruder used for this object.
if stack.getProperty("machine_extruder_count", "value") > 1:
changed_setting_keys.add("extruder_nr")
settings_xml = ET.SubElement(object, "settings", xmlns=self._namespaces["cura"])
# Get values for all changed settings & save them.
for key in changed_setting_keys:
setting_xml = ET.SubElement(settings_xml, "setting", key = key)
setting_xml.text = str(stack.getProperty(key, "value"))
# Add one to the index as we haven't incremented the last iteration.
index += 1
nodes_to_add = set()
for node in added_nodes:
# Check the parents of the nodes with mesh_data and ensure that they are also added.
parent_node = node.getParent()
while parent_node is not None:
if parent_node.callDecoration("isGroup"):
nodes_to_add.add(parent_node)
parent_node = parent_node.getParent()
else:
parent_node = None
# Sort all the nodes by depth (so nodes with the highest depth are done first)
sorted_nodes_to_add = sorted(nodes_to_add, key=lambda node: node.getDepth(), reverse = True)
# We have already saved the nodes with mesh data, but now we also want to save nodes required for the scene
for node in sorted_nodes_to_add:
object = ET.SubElement(resources, "object", id=str(index + 1), type="model")
components = ET.SubElement(object, "components")
for child in node.getChildren():
if child in added_nodes:
component = ET.SubElement(components, "component", objectid = str(added_nodes.index(child) + 1), transform = self._convertMatrixToString(child.getLocalTransformation()))
index += 1
added_nodes.append(node)
# Create a transformation Matrix to convert from our worldspace into 3MF.
# First step: flip the y and z axis.
transformation_matrix = Matrix()
transformation_matrix._data[1, 1] = 0
transformation_matrix._data[1, 2] = -1
transformation_matrix._data[2, 1] = 1
transformation_matrix._data[2, 2] = 0
global_container_stack = UM.Application.getInstance().getGlobalContainerStack()
# Second step: 3MF defines the left corner of the machine as center, whereas cura uses the center of the
# build volume.
if global_container_stack:
translation_vector = Vector(x=global_container_stack.getProperty("machine_width", "value") / 2,
y=global_container_stack.getProperty("machine_depth", "value") / 2,
z=0)
translation_matrix = Matrix()
translation_matrix.setByTranslation(translation_vector)
transformation_matrix.preMultiply(translation_matrix)
# Find out what the final build items are and add them.
for node in added_nodes:
if node.getParent().callDecoration("isGroup") is None:
node_matrix = node.getLocalTransformation()
ET.SubElement(build, "item", objectid = str(added_nodes.index(node) + 1), transform = self._convertMatrixToString(node_matrix.preMultiply(transformation_matrix)))
archive.writestr(model_file, b'<?xml version="1.0" encoding="UTF-8"?> \n' + ET.tostring(model))
archive.writestr(content_types_file, b'<?xml version="1.0" encoding="UTF-8"?> \n' + ET.tostring(content_types))
archive.writestr(relations_file, b'<?xml version="1.0" encoding="UTF-8"?> \n' + ET.tostring(relations_element))
except Exception as e:
Logger.logException("e", "Error writing zip file")
return False
finally:
if not self._store_archive:
archive.close()
else:
self._archive = archive
return True

View file

@ -0,0 +1,38 @@
# Copyright (c) 2015 Ultimaker B.V.
# Uranium is released under the terms of the AGPLv3 or higher.
from UM.i18n import i18nCatalog
from . import ThreeMFWorkspaceWriter
from . import ThreeMFWriter
i18n_catalog = i18nCatalog("uranium")
def getMetaData():
return {
"plugin": {
"name": i18n_catalog.i18nc("@label", "3MF Writer"),
"author": "Ultimaker",
"version": "1.0",
"description": i18n_catalog.i18nc("@info:whatsthis", "Provides support for writing 3MF files."),
"api": 3
},
"mesh_writer": {
"output": [{
"extension": "3mf",
"description": i18n_catalog.i18nc("@item:inlistbox", "3MF file"),
"mime_type": "application/vnd.ms-package.3dmanufacturing-3dmodel+xml",
"mode": ThreeMFWriter.ThreeMFWriter.OutputMode.BinaryMode
}]
},
"workspace_writer": {
"output": [{
"extension": "3mf",
"description": i18n_catalog.i18nc("@item:inlistbox", "3MF file"),
"mime_type": "application/vnd.ms-package.3dmanufacturing-3dmodel+xml",
"mode": ThreeMFWorkspaceWriter.ThreeMFWorkspaceWriter.OutputMode.BinaryMode
}]
}
}
def register(app):
return {"mesh_writer": ThreeMFWriter.ThreeMFWriter(), "workspace_writer": ThreeMFWorkspaceWriter.ThreeMFWorkspaceWriter()}

View file

@ -1,14 +1,14 @@
# Copyright (c) 2016 Ultimaker B.V.
# Cura is released under the terms of the AGPLv3 or higher.
from PyQt5.QtCore import pyqtSlot
from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot
from cura.MachineAction import MachineAction
import cura.Settings.CuraContainerRegistry
import UM.Application
import UM.Settings.InstanceContainer
import UM.Settings.DefinitionContainer
import UM.Settings.ContainerRegistry
import UM.Logger
import UM.i18n
@ -19,23 +19,44 @@ class MachineSettingsAction(MachineAction):
super().__init__("MachineSettingsAction", catalog.i18nc("@action", "Machine Settings"))
self._qml_url = "MachineSettingsAction.qml"
cura.Settings.CuraContainerRegistry.getInstance().containerAdded.connect(self._onContainerAdded)
self._container_index = 0
self._container_registry = UM.Settings.ContainerRegistry.getInstance()
self._container_registry.containerAdded.connect(self._onContainerAdded)
def _reset(self):
global_container_stack = UM.Application.getInstance().getGlobalContainerStack()
if global_container_stack:
variant = global_container_stack.findContainer({"type": "variant"})
if variant and variant.getId() == "empty_variant":
variant_index = global_container_stack.getContainerIndex(variant)
self._createVariant(global_container_stack, variant_index)
if not global_container_stack:
return
def _createVariant(self, global_container_stack, variant_index):
# Create and switch to a variant to store the settings in
new_variant = UM.Settings.InstanceContainer(global_container_stack.getName() + "_variant")
new_variant.addMetaDataEntry("type", "variant")
new_variant.setDefinition(global_container_stack.getBottom())
UM.Settings.ContainerRegistry.getInstance().addContainer(new_variant)
global_container_stack.replaceContainer(variant_index, new_variant)
# Make sure there is a definition_changes container to store the machine settings
definition_changes_container = global_container_stack.findContainer({"type": "definition_changes"})
if not definition_changes_container:
definition_changes_container = self._createDefinitionChangesContainer(global_container_stack)
# Notify the UI in which container to store the machine settings data
container_index = global_container_stack.getContainerIndex(definition_changes_container)
if container_index != self._container_index:
self._container_index = container_index
self.containerIndexChanged.emit()
def _createDefinitionChangesContainer(self, global_container_stack, container_index = None):
definition_changes_container = UM.Settings.InstanceContainer(global_container_stack.getName() + "_settings")
definition = global_container_stack.getBottom()
definition_changes_container.setDefinition(definition)
definition_changes_container.addMetaDataEntry("type", "definition_changes")
self._container_registry.addContainer(definition_changes_container)
# Insert definition_changes between the definition and the variant
global_container_stack.insertContainer(-1, definition_changes_container)
return definition_changes_container
containerIndexChanged = pyqtSignal()
@pyqtProperty(int, notify = containerIndexChanged)
def containerIndex(self):
return self._container_index
def _onContainerAdded(self, container):
# Add this action as a supported action to all machine definitions
@ -44,10 +65,6 @@ class MachineSettingsAction(MachineAction):
# Multiextruder printers are not currently supported
UM.Logger.log("d", "Not attaching MachineSettingsAction to %s; Multi-extrusion printers are not supported", container.getId())
return
if container.getMetaDataEntry("has_variants", False):
# Machines that use variants are not currently supported
UM.Logger.log("d", "Not attaching MachineSettingsAction to %s; Machines that use variants are not supported", container.getId())
return
UM.Application.getInstance().getMachineActionManager().addSupportedAction(container.getId(), self.getKey())
@ -78,7 +95,7 @@ class MachineSettingsAction(MachineAction):
# Set the material container to a sane default
if material_container.getId() == "empty_material":
search_criteria = { "type": "material", "definition": "fdmprinter", "id": "*pla*" }
containers = UM.Settings.ContainerRegistry.getInstance().findInstanceContainers(**search_criteria)
containers = self._container_registry.findInstanceContainers(**search_criteria)
if containers:
global_container_stack.replaceContainer(material_index, containers[0])
else:
@ -87,7 +104,7 @@ class MachineSettingsAction(MachineAction):
if "has_materials" in global_container_stack.getMetaData():
global_container_stack.removeMetaDataEntry("has_materials")
empty_material = UM.Settings.ContainerRegistry.getInstance().findInstanceContainers(id = "empty_material")[0]
empty_material = self._container_registry.findInstanceContainers(id = "empty_material")[0]
global_container_stack.replaceContainer(material_index, empty_material)
UM.Application.getInstance().globalContainerStackChanged.emit()

View file

@ -147,19 +147,40 @@ Cura.MachineAction
ComboBox
{
model: ["RepRap (Marlin/Sprinter)", "UltiGCode", "Repetier"]
model: ListModel
{
id: flavorModel
Component.onCompleted:
{
// Options come in as a string-representation of an OrderedDict
var options = machineGCodeFlavorProvider.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("', '")
flavorModel.append({text: option[1], value: option[0]});
}
}
}
}
currentIndex:
{
var index = model.indexOf(machineGCodeFlavorProvider.properties.value);
if(index == -1)
var currentValue = machineGCodeFlavorProvider.properties.value;
var index = 0;
for(var i = 0; i < flavorModel.count; i++)
{
index = 0;
if(flavorModel.get(i).value == currentValue) {
index = i;
break;
}
}
return index
}
onActivated:
{
machineGCodeFlavorProvider.setPropertyValue("value", model[index]);
machineGCodeFlavorProvider.setPropertyValue("value", flavorModel.get(index).value);
manager.updateHasMaterialsMetadata();
}
}
@ -273,17 +294,20 @@ Cura.MachineAction
Label
{
text: catalog.i18nc("@label", "Nozzle size")
visible: !Cura.MachineManager.hasVariants
}
TextField
{
id: nozzleSizeField
text: machineNozzleSizeProvider.properties.value
visible: !Cura.MachineManager.hasVariants
validator: RegExpValidator { regExp: /[0-9\.]{0,6}/ }
onEditingFinished: { machineNozzleSizeProvider.setPropertyValue("value", text) }
}
Label
{
text: catalog.i18nc("@label", "mm")
visible: !Cura.MachineManager.hasVariants
}
}
}
@ -308,6 +332,8 @@ Cura.MachineAction
id: machineStartGcodeField
width: parent.width
height: parent.height - y
font: UM.Theme.getFont("fixed")
wrapMode: TextEdit.NoWrap
text: machineStartGcodeProvider.properties.value
onActiveFocusChanged:
{
@ -330,6 +356,8 @@ Cura.MachineAction
id: machineEndGcodeField
width: parent.width
height: parent.height - y
font: UM.Theme.getFont("fixed")
wrapMode: TextEdit.NoWrap
text: machineEndGcodeProvider.properties.value
onActiveFocusChanged:
{
@ -377,7 +405,7 @@ Cura.MachineAction
containerStackId: Cura.MachineManager.activeMachineId
key: "machine_width"
watchedProperties: [ "value" ]
storeIndex: 4
storeIndex: manager.containerIndex
}
UM.SettingPropertyProvider
@ -387,7 +415,7 @@ Cura.MachineAction
containerStackId: Cura.MachineManager.activeMachineId
key: "machine_depth"
watchedProperties: [ "value" ]
storeIndex: 4
storeIndex: manager.containerIndex
}
UM.SettingPropertyProvider
@ -397,7 +425,7 @@ Cura.MachineAction
containerStackId: Cura.MachineManager.activeMachineId
key: "machine_height"
watchedProperties: [ "value" ]
storeIndex: 4
storeIndex: manager.containerIndex
}
UM.SettingPropertyProvider
@ -407,7 +435,7 @@ Cura.MachineAction
containerStackId: Cura.MachineManager.activeMachineId
key: "machine_heated_bed"
watchedProperties: [ "value" ]
storeIndex: 4
storeIndex: manager.containerIndex
}
UM.SettingPropertyProvider
@ -417,7 +445,7 @@ Cura.MachineAction
containerStackId: Cura.MachineManager.activeMachineId
key: "machine_center_is_zero"
watchedProperties: [ "value" ]
storeIndex: 4
storeIndex: manager.containerIndex
}
UM.SettingPropertyProvider
@ -426,8 +454,8 @@ Cura.MachineAction
containerStackId: Cura.MachineManager.activeMachineId
key: "machine_gcode_flavor"
watchedProperties: [ "value" ]
storeIndex: 4
watchedProperties: [ "value", "options" ]
storeIndex: manager.containerIndex
}
UM.SettingPropertyProvider
@ -437,7 +465,7 @@ Cura.MachineAction
containerStackId: Cura.MachineManager.activeMachineId
key: "machine_nozzle_size"
watchedProperties: [ "value" ]
storeIndex: 4
storeIndex: manager.containerIndex
}
UM.SettingPropertyProvider
@ -447,7 +475,7 @@ Cura.MachineAction
containerStackId: Cura.MachineManager.activeMachineId
key: "gantry_height"
watchedProperties: [ "value" ]
storeIndex: 4
storeIndex: manager.containerIndex
}
UM.SettingPropertyProvider
@ -457,7 +485,7 @@ Cura.MachineAction
containerStackId: Cura.MachineManager.activeMachineId
key: "machine_head_with_fans_polygon"
watchedProperties: [ "value" ]
storeIndex: 4
storeIndex: manager.containerIndex
}
@ -468,7 +496,7 @@ Cura.MachineAction
containerStackId: Cura.MachineManager.activeMachineId
key: "machine_start_gcode"
watchedProperties: [ "value" ]
storeIndex: 4
storeIndex: manager.containerIndex
}
UM.SettingPropertyProvider
@ -478,7 +506,7 @@ Cura.MachineAction
containerStackId: Cura.MachineManager.activeMachineId
key: "machine_end_gcode"
watchedProperties: [ "value" ]
storeIndex: 4
storeIndex: manager.containerIndex
}
}

View file

@ -6,7 +6,7 @@ import os.path
from UM.Application import Application
from UM.Logger import Logger
from UM.Message import Message
from UM.Mesh.WriteMeshJob import WriteMeshJob
from UM.FileHandler.WriteFileJob import WriteFileJob
from UM.Mesh.MeshWriter import MeshWriter
from UM.Scene.Iterator.BreadthFirstIterator import BreadthFirstIterator
from UM.OutputDevice.OutputDevice import OutputDevice
@ -37,13 +37,17 @@ class RemovableDriveOutputDevice(OutputDevice):
# meshes.
# \param limit_mimetypes Should we limit the available MIME types to the
# MIME types available to the currently active machine?
def requestWrite(self, nodes, file_name = None, filter_by_machine = False):
def requestWrite(self, nodes, file_name = None, filter_by_machine = False, file_handler = None):
filter_by_machine = True # This plugin is indended to be used by machine (regardless of what it was told to do)
if self._writing:
raise OutputDeviceError.DeviceBusyError()
# Formats supported by this application (File types that we can actually write)
file_formats = Application.getInstance().getMeshFileHandler().getSupportedFileTypesWrite()
if file_handler:
file_formats = file_handler.getSupportedFileTypesWrite()
else:
file_formats = Application.getInstance().getMeshFileHandler().getSupportedFileTypesWrite()
if filter_by_machine:
container = Application.getInstance().getGlobalContainerStack().findContainer({"file_formats": "*"})
@ -58,7 +62,11 @@ class RemovableDriveOutputDevice(OutputDevice):
raise OutputDeviceError.WriteRequestFailedError()
# Just take the first file format available.
writer = Application.getInstance().getMeshFileHandler().getWriterByMimeType(file_formats[0]["mime_type"])
if file_handler is not None:
writer = file_handler.getWriterByMimeType(file_formats[0]["mime_type"])
else:
writer = Application.getInstance().getMeshFileHandler().getWriterByMimeType(file_formats[0]["mime_type"])
extension = file_formats[0]["extension"]
if file_name is None:
@ -72,7 +80,7 @@ class RemovableDriveOutputDevice(OutputDevice):
Logger.log("d", "Writing to %s", file_name)
# Using buffering greatly reduces the write time for many lines of gcode
self._stream = open(file_name, "wt", buffering = 1, encoding = "utf-8")
job = WriteMeshJob(writer, self._stream, nodes, MeshWriter.OutputMode.TextMode)
job = WriteFileJob(writer, self._stream, nodes, MeshWriter.OutputMode.TextMode)
job.setFileName(file_name)
job.progress.connect(self._onProgress)
job.finished.connect(self._onFinished)

View file

@ -0,0 +1,148 @@
from cura.MachineAction import MachineAction
from UM.Application import Application
from UM.PluginRegistry import PluginRegistry
from UM.Logger import Logger
from PyQt5.QtCore import pyqtSignal, pyqtProperty, pyqtSlot, QUrl, QObject
from PyQt5.QtQml import QQmlComponent, QQmlContext
import os.path
import time
from UM.i18n import i18nCatalog
catalog = i18nCatalog("cura")
class DiscoverUM3Action(MachineAction):
def __init__(self):
super().__init__("DiscoverUM3Action", catalog.i18nc("@action","Connect via Network"))
self._qml_url = "DiscoverUM3Action.qml"
self._network_plugin = None
self.__additional_components_context = None
self.__additional_component = None
self.__additional_components_view = None
Application.getInstance().engineCreatedSignal.connect(self._createAdditionalComponentsView)
self._last_zeroconf_event_time = time.time()
self._zeroconf_change_grace_period = 0.25 # Time to wait after a zeroconf service change before allowing a zeroconf reset
printersChanged = pyqtSignal()
@pyqtSlot()
def startDiscovery(self):
if not self._network_plugin:
self._network_plugin = Application.getInstance().getOutputDeviceManager().getOutputDevicePlugin("UM3NetworkPrinting")
self._network_plugin.printerListChanged.connect(self._onPrinterDiscoveryChanged)
self.printersChanged.emit()
## Re-filters the list of printers.
@pyqtSlot()
def reset(self):
self.printersChanged.emit()
@pyqtSlot()
def restartDiscovery(self):
# Ensure that there is a bit of time after a printer has been discovered.
# This is a work around for an issue with Qt 5.5.1 up to Qt 5.7 which can segfault if we do this too often.
# It's most likely that the QML engine is still creating delegates, where the python side already deleted or
# garbage collected the data.
# Whatever the case, waiting a bit ensures that it doesn't crash.
if time.time() - self._last_zeroconf_event_time > self._zeroconf_change_grace_period:
if not self._network_plugin:
self.startDiscovery()
else:
self._network_plugin.startDiscovery()
@pyqtSlot(str, str)
def removeManualPrinter(self, key, address):
if not self._network_plugin:
return
self._network_plugin.removeManualPrinter(key, address)
@pyqtSlot(str, str)
def setManualPrinter(self, key, address):
if key != "":
# This manual printer replaces a current manual printer
self._network_plugin.removeManualPrinter(key)
if address != "":
self._network_plugin.addManualPrinter(address)
def _onPrinterDiscoveryChanged(self, *args):
self._last_zeroconf_event_time = time.time()
self.printersChanged.emit()
@pyqtProperty("QVariantList", notify = printersChanged)
def foundDevices(self):
if self._network_plugin:
if Application.getInstance().getGlobalContainerStack():
global_printer_type = Application.getInstance().getGlobalContainerStack().getBottom().getId()
else:
global_printer_type = "unknown"
printers = list(self._network_plugin.getPrinters().values())
# TODO; There are still some testing printers that don't have a correct printer type, so don't filter out unkown ones just yet.
printers = [printer for printer in printers if printer.printerType == global_printer_type or printer.printerType == "unknown"]
printers.sort(key = lambda k: k.name)
return printers
else:
return []
@pyqtSlot(str)
def setKey(self, key):
global_container_stack = Application.getInstance().getGlobalContainerStack()
if global_container_stack:
meta_data = global_container_stack.getMetaData()
if "um_network_key" in meta_data:
global_container_stack.setMetaDataEntry("um_network_key", key)
# Delete old authentication data.
global_container_stack.removeMetaDataEntry("network_authentication_id")
global_container_stack.removeMetaDataEntry("network_authentication_key")
else:
global_container_stack.addMetaDataEntry("um_network_key", key)
if self._network_plugin:
# Ensure that the connection states are refreshed.
self._network_plugin.reCheckConnections()
@pyqtSlot(result = str)
def getStoredKey(self):
global_container_stack = Application.getInstance().getGlobalContainerStack()
if global_container_stack:
meta_data = global_container_stack.getMetaData()
if "um_network_key" in meta_data:
return global_container_stack.getMetaDataEntry("um_network_key")
return ""
@pyqtSlot()
def loadConfigurationFromPrinter(self):
machine_manager = Application.getInstance().getMachineManager()
hotend_ids = machine_manager.printerOutputDevices[0].hotendIds
for index in range(len(hotend_ids)):
machine_manager.printerOutputDevices[0].hotendIdChanged.emit(index, hotend_ids[index])
material_ids = machine_manager.printerOutputDevices[0].materialIds
for index in range(len(material_ids)):
machine_manager.printerOutputDevices[0].materialIdChanged.emit(index, material_ids[index])
def _createAdditionalComponentsView(self):
Logger.log("d", "Creating additional ui components for UM3.")
path = QUrl.fromLocalFile(os.path.join(PluginRegistry.getInstance().getPluginPath("UM3NetworkPrinting"), "UM3InfoComponents.qml"))
self.__additional_component = QQmlComponent(Application.getInstance()._engine, path)
# We need access to engine (although technically we can't)
self.__additional_components_context = QQmlContext(Application.getInstance()._engine.rootContext())
self.__additional_components_context.setContextProperty("manager", self)
self.__additional_components_view = self.__additional_component.create(self.__additional_components_context)
if not self.__additional_components_view:
Logger.log("w", "Could not create ui components for UM3.")
return
Application.getInstance().addAdditionalComponent("monitorButtons", self.__additional_components_view.findChild(QObject, "networkPrinterConnectButton"))
Application.getInstance().addAdditionalComponent("machinesDetailPane", self.__additional_components_view.findChild(QObject, "networkPrinterConnectionInfo"))

View file

@ -0,0 +1,369 @@
import UM 1.2 as UM
import Cura 1.0 as Cura
import QtQuick 2.2
import QtQuick.Controls 1.1
import QtQuick.Layouts 1.1
import QtQuick.Window 2.1
Cura.MachineAction
{
id: base
anchors.fill: parent;
property var selectedPrinter: null
property bool completeProperties: true
property var connectingToPrinter: null
Connections
{
target: dialog ? dialog : null
ignoreUnknownSignals: true
onNextClicked:
{
// Connect to the printer if the MachineAction is currently shown
if(base.parent.wizard == dialog)
{
connectToPrinter();
}
}
}
function connectToPrinter()
{
if(base.selectedPrinter && base.completeProperties)
{
var printerKey = base.selectedPrinter.getKey()
if(connectingToPrinter != printerKey) {
// prevent an infinite loop
connectingToPrinter = printerKey;
manager.setKey(printerKey);
completed();
}
}
}
Column
{
anchors.fill: parent;
id: discoverUM3Action
spacing: UM.Theme.getSize("default_margin").height
SystemPalette { id: palette }
UM.I18nCatalog { id: catalog; name:"cura" }
Label
{
id: pageTitle
width: parent.width
text: catalog.i18nc("@title:window", "Connect to Networked Printer")
wrapMode: Text.WordWrap
font.pointSize: 18
}
Label
{
id: pageDescription
width: parent.width
wrapMode: Text.WordWrap
text: catalog.i18nc("@label", "To print directly to your printer over the network, please make sure your printer is connected to the network using a network cable or by connecting your printer to your WIFI network. If you don't connect Cura with your printer, you can still use a USB drive to transfer g-code files to your printer.\n\nSelect your printer from the list below:")
}
Row
{
spacing: UM.Theme.getSize("default_lining").width
Button
{
id: addButton
text: catalog.i18nc("@action:button", "Add");
onClicked:
{
manualPrinterDialog.showDialog("", "");
}
}
Button
{
id: editButton
text: catalog.i18nc("@action:button", "Edit")
enabled: base.selectedPrinter != null && base.selectedPrinter.getProperty("manual") == "true"
onClicked:
{
manualPrinterDialog.showDialog(base.selectedPrinter.getKey(), base.selectedPrinter.ipAddress);
}
}
Button
{
id: removeButton
text: catalog.i18nc("@action:button", "Remove")
enabled: base.selectedPrinter != null && base.selectedPrinter.getProperty("manual") == "true"
onClicked: manager.removeManualPrinter(base.selectedPrinter.getKey(), base.selectedPrinter.ipAddress)
}
Button
{
id: rediscoverButton
text: catalog.i18nc("@action:button", "Refresh")
onClicked: manager.restartDiscovery()
}
}
Row
{
id: contentRow
width: parent.width
spacing: UM.Theme.getSize("default_margin").width
Column
{
width: parent.width * 0.5
spacing: UM.Theme.getSize("default_margin").height
ScrollView
{
id: objectListContainer
frameVisible: true
width: parent.width
height: base.height - contentRow.y - discoveryTip.height
Rectangle
{
parent: viewport
anchors.fill: parent
color: palette.light
}
ListView
{
id: listview
model: manager.foundDevices
onModelChanged:
{
var selectedKey = manager.getStoredKey();
for(var i = 0; i < model.length; i++) {
if(model[i].getKey() == selectedKey)
{
currentIndex = i;
return
}
}
currentIndex = -1;
}
width: parent.width
currentIndex: -1
onCurrentIndexChanged:
{
base.selectedPrinter = listview.model[currentIndex];
// Only allow connecting if the printer has responded to API query since the last refresh
base.completeProperties = base.selectedPrinter != null && base.selectedPrinter.getProperty("incomplete") != "true";
}
Component.onCompleted: manager.startDiscovery()
delegate: Rectangle
{
height: childrenRect.height
color: ListView.isCurrentItem ? palette.highlight : index % 2 ? palette.base : palette.alternateBase
width: parent.width
Label
{
anchors.left: parent.left
anchors.leftMargin: UM.Theme.getSize("default_margin").width
anchors.right: parent.right
text: listview.model[index].name
color: parent.ListView.isCurrentItem ? palette.highlightedText : palette.text
elide: Text.ElideRight
}
MouseArea
{
anchors.fill: parent;
onClicked:
{
if(!parent.ListView.isCurrentItem)
{
parent.ListView.view.currentIndex = index;
}
}
}
}
}
}
Label
{
id: discoveryTip
anchors.left: parent.left
anchors.right: parent.right
wrapMode: Text.WordWrap
//: Tips label
//TODO: get actual link from webteam
text: catalog.i18nc("@label", "If your printer is not listed, read the <a href='%1'>network-printing troubleshooting guide</a>").arg("https://ultimaker.com/en/troubleshooting");
onLinkActivated: Qt.openUrlExternally(link)
}
}
Column
{
width: parent.width * 0.5
visible: base.selectedPrinter ? true : false
spacing: UM.Theme.getSize("default_margin").height
Label
{
width: parent.width
wrapMode: Text.WordWrap
text: base.selectedPrinter ? base.selectedPrinter.name : ""
font: UM.Theme.getFont("large")
elide: Text.ElideRight
}
Grid
{
visible: base.completeProperties
width: parent.width
columns: 2
Label
{
width: parent.width * 0.5
wrapMode: Text.WordWrap
text: catalog.i18nc("@label", "Type")
}
Label
{
width: parent.width * 0.5
wrapMode: Text.WordWrap
text:
{
if(base.selectedPrinter)
{
if(base.selectedPrinter.printerType == "ultimaker3")
{
return catalog.i18nc("@label", "Ultimaker 3")
} else if(base.selectedPrinter.printerType == "ultimaker3_extended")
{
return catalog.i18nc("@label", "Ultimaker 3 Extended")
} else
{
return catalog.i18nc("@label", "Unknown") // We have no idea what type it is. Should not happen 'in the field'
}
}
else
{
return ""
}
}
}
Label
{
width: parent.width * 0.5
wrapMode: Text.WordWrap
text: catalog.i18nc("@label", "Firmware version")
}
Label
{
width: parent.width * 0.5
wrapMode: Text.WordWrap
text: base.selectedPrinter ? base.selectedPrinter.firmwareVersion : ""
}
Label
{
width: parent.width * 0.5
wrapMode: Text.WordWrap
text: catalog.i18nc("@label", "Address")
}
Label
{
width: parent.width * 0.5
wrapMode: Text.WordWrap
text: base.selectedPrinter ? base.selectedPrinter.ipAddress : ""
}
}
Label
{
width: parent.width
wrapMode: Text.WordWrap
visible: base.selectedPrinter != null && !base.completeProperties
text: catalog.i18nc("@label", "The printer at this address has not yet responded." )
}
Button
{
text: catalog.i18nc("@action:button", "Connect")
enabled: (base.selectedPrinter && base.completeProperties) ? true : false
onClicked: connectToPrinter()
}
}
}
}
UM.Dialog
{
id: manualPrinterDialog
property string printerKey
property alias addressText: addressField.text
title: catalog.i18nc("@title:window", "Printer Address")
minimumWidth: 400 * Screen.devicePixelRatio
minimumHeight: 120 * Screen.devicePixelRatio
width: minimumWidth
height: minimumHeight
signal showDialog(string key, string address)
onShowDialog:
{
printerKey = key;
addressText = address;
addressField.selectAll();
addressField.focus = true;
manualPrinterDialog.show();
}
onAccepted:
{
manager.setManualPrinter(printerKey, addressText)
}
Column {
anchors.fill: parent
spacing: UM.Theme.getSize("default_margin").height
Label
{
text: catalog.i18nc("@alabel","Enter the IP address or hostname of your printer on the network.")
width: parent.width
wrapMode: Text.WordWrap
}
TextField
{
id: addressField
width: parent.width
maximumLength: 40
validator: RegExpValidator
{
regExp: /[a-zA-Z0-9\.\-\_]*/
}
}
}
rightButtons: [
Button {
text: catalog.i18nc("@action:button","Cancel")
onClicked:
{
manualPrinterDialog.reject()
manualPrinterDialog.hide()
}
},
Button {
text: catalog.i18nc("@action:button", "Ok")
onClicked:
{
manualPrinterDialog.accept()
manualPrinterDialog.hide()
}
enabled: manualPrinterDialog.addressText.trim() != ""
isDefault: true
}
]
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,205 @@
from UM.OutputDevice.OutputDevicePlugin import OutputDevicePlugin
from . import NetworkPrinterOutputDevice
from zeroconf import Zeroconf, ServiceBrowser, ServiceStateChange, ServiceInfo
from UM.Logger import Logger
from UM.Signal import Signal, signalemitter
from UM.Application import Application
from UM.Preferences import Preferences
from PyQt5.QtNetwork import QNetworkRequest, QNetworkAccessManager, QNetworkReply
from PyQt5.QtCore import QUrl
import time
import json
## This plugin handles the connection detection & creation of output device objects for the UM3 printer.
# Zero-Conf is used to detect printers, which are saved in a dict.
# If we discover a printer that has the same key as the active machine instance a connection is made.
@signalemitter
class NetworkPrinterOutputDevicePlugin(OutputDevicePlugin):
def __init__(self):
super().__init__()
self._zero_conf = None
self._browser = None
self._printers = {}
self._api_version = "1"
self._api_prefix = "/api/v" + self._api_version + "/"
self._network_manager = QNetworkAccessManager()
self._network_manager.finished.connect(self._onNetworkRequestFinished)
# List of old printer names. This is used to ensure that a refresh of zeroconf does not needlessly forces
# authentication requests.
self._old_printers = []
# Because the model needs to be created in the same thread as the QMLEngine, we use a signal.
self.addPrinterSignal.connect(self.addPrinter)
self.removePrinterSignal.connect(self.removePrinter)
Application.getInstance().globalContainerStackChanged.connect(self.reCheckConnections)
# Get list of manual printers from preferences
self._preferences = Preferences.getInstance()
self._preferences.addPreference("um3networkprinting/manual_instances", "") # A comma-separated list of ip adresses or hostnames
self._manual_instances = self._preferences.getValue("um3networkprinting/manual_instances").split(",")
addPrinterSignal = Signal()
removePrinterSignal = Signal()
printerListChanged = Signal()
## Start looking for devices on network.
def start(self):
self.startDiscovery()
def startDiscovery(self):
self.stop()
if self._browser:
self._browser.cancel()
self._browser = None
self._old_printers = [printer_name for printer_name in self._printers]
self._printers = {}
self.printerListChanged.emit()
# After network switching, one must make a new instance of Zeroconf
# On windows, the instance creation is very fast (unnoticable). Other platforms?
self._zero_conf = Zeroconf()
self._browser = ServiceBrowser(self._zero_conf, u'_ultimaker._tcp.local.', [self._onServiceChanged])
# Look for manual instances from preference
for address in self._manual_instances:
if address:
self.addManualPrinter(address)
def addManualPrinter(self, address):
if address not in self._manual_instances:
self._manual_instances.append(address)
self._preferences.setValue("um3networkprinting/manual_instances", ",".join(self._manual_instances))
name = address
instance_name = "manual:%s" % address
properties = { b"name": name.encode("utf-8"), b"manual": b"true", b"incomplete": b"true" }
if instance_name not in self._printers:
# Add a preliminary printer instance
self.addPrinter(instance_name, address, properties)
self.checkManualPrinter(address)
def removeManualPrinter(self, key, address = None):
if key in self._printers:
if not address:
address = self._printers[key].ipAddress
self.removePrinter(key)
if address in self._manual_instances:
self._manual_instances.remove(address)
self._preferences.setValue("um3networkprinting/manual_instances", ",".join(self._manual_instances))
def checkManualPrinter(self, address):
# Check if a printer exists at this address
# If a printer responds, it will replace the preliminary printer created above
url = QUrl("http://" + address + self._api_prefix + "system")
name_request = QNetworkRequest(url)
self._network_manager.get(name_request)
## Handler for all requests that have finished.
def _onNetworkRequestFinished(self, reply):
reply_url = reply.url().toString()
status_code = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute)
if reply.operation() == QNetworkAccessManager.GetOperation:
if "system" in reply_url: # Name returned from printer.
if status_code == 200:
system_info = json.loads(bytes(reply.readAll()).decode("utf-8"))
address = reply.url().host()
name = ("%s (%s)" % (system_info["name"], address))
instance_name = "manual:%s" % address
properties = { b"name": name.encode("utf-8"), b"firmware_version": system_info["firmware"].encode("utf-8"), b"manual": b"true" }
if instance_name in self._printers:
# Only replace the printer if it is still in the list of (manual) printers
self.removePrinter(instance_name)
self.addPrinter(instance_name, address, properties)
## Stop looking for devices on network.
def stop(self):
if self._zero_conf is not None:
self._zero_conf.close()
def getPrinters(self):
return self._printers
def reCheckConnections(self):
active_machine = Application.getInstance().getGlobalContainerStack()
if not active_machine:
return
for key in self._printers:
if key == active_machine.getMetaDataEntry("um_network_key"):
self._printers[key].connect()
self._printers[key].connectionStateChanged.connect(self._onPrinterConnectionStateChanged)
else:
if self._printers[key].isConnected():
self._printers[key].close()
## Because the model needs to be created in the same thread as the QMLEngine, we use a signal.
def addPrinter(self, name, address, properties):
printer = NetworkPrinterOutputDevice.NetworkPrinterOutputDevice(name, address, properties, self._api_prefix)
self._printers[printer.getKey()] = printer
global_container_stack = Application.getInstance().getGlobalContainerStack()
if global_container_stack and printer.getKey() == global_container_stack.getMetaDataEntry("um_network_key"):
if printer.getKey() not in self._old_printers: # Was the printer already connected, but a re-scan forced?
self._printers[printer.getKey()].connect()
printer.connectionStateChanged.connect(self._onPrinterConnectionStateChanged)
self.printerListChanged.emit()
def removePrinter(self, name):
printer = self._printers.pop(name, None)
if printer:
if printer.isConnected():
printer.connectionStateChanged.disconnect(self._onPrinterConnectionStateChanged)
printer.disconnect()
self.printerListChanged.emit()
## Handler for when the connection state of one of the detected printers changes
def _onPrinterConnectionStateChanged(self, key):
if key not in self._printers:
return
if self._printers[key].isConnected():
self.getOutputDeviceManager().addOutputDevice(self._printers[key])
else:
self.getOutputDeviceManager().removeOutputDevice(key)
## Handler for zeroConf detection
def _onServiceChanged(self, zeroconf, service_type, name, state_change):
if state_change == ServiceStateChange.Added:
Logger.log("d", "Bonjour service added: %s" % name)
# First try getting info from zeroconf cache
info = ServiceInfo(service_type, name, properties = {})
for record in zeroconf.cache.entries_with_name(name.lower()):
info.update_record(zeroconf, time.time(), record)
for record in zeroconf.cache.entries_with_name(info.server):
info.update_record(zeroconf, time.time(), record)
if info.address:
break
# Request more data if info is not complete
if not info.address:
Logger.log("d", "Trying to get address of %s", name)
info = zeroconf.get_service_info(service_type, name)
if info:
type_of_device = info.properties.get(b"type", None).decode("utf-8")
if type_of_device == "printer":
address = '.'.join(map(lambda n: str(n), info.address))
self.addPrinterSignal.emit(str(name), address, info.properties)
else:
Logger.log("w", "The type of the found device is '%s', not 'printer'! Ignoring.." %type_of_device )
else:
Logger.log("w", "Could not get information about %s" % name)
elif state_change == ServiceStateChange.Removed:
Logger.log("d", "Bonjour service removed: %s" % name)
self.removePrinterSignal.emit(str(name))

View file

@ -0,0 +1,124 @@
import UM 1.2 as UM
import Cura 1.0 as Cura
import QtQuick 2.2
import QtQuick.Controls 1.1
import QtQuick.Layouts 1.1
import QtQuick.Window 2.1
Item
{
id: base
property bool isUM3: Cura.MachineManager.activeQualityDefinitionId == "ultimaker3"
property bool printerConnected: Cura.MachineManager.printerOutputDevices.length != 0
property bool printerAcceptsCommands: printerConnected && Cura.MachineManager.printerOutputDevices[0].acceptsCommands
property bool authenticationRequested: printerConnected && Cura.MachineManager.printerOutputDevices[0].authenticationState == 2 // AuthState.AuthenticationRequested
Row
{
objectName: "networkPrinterConnectButton"
visible: isUM3
spacing: UM.Theme.getSize("default_margin").width
Button
{
height: UM.Theme.getSize("save_button_save_to_button").height
tooltip: catalog.i18nc("@info:tooltip", "Send access request to the printer")
text: catalog.i18nc("@action:button", "Request Access")
style: UM.Theme.styles.sidebar_action_button
onClicked: Cura.MachineManager.printerOutputDevices[0].requestAuthentication()
visible: printerConnected && !printerAcceptsCommands && !authenticationRequested
}
Button
{
height: UM.Theme.getSize("save_button_save_to_button").height
tooltip: catalog.i18nc("@info:tooltip", "Connect to a printer")
text: catalog.i18nc("@action:button", "Connect")
style: UM.Theme.styles.sidebar_action_button
onClicked: connectActionDialog.show()
visible: !printerConnected
}
}
UM.Dialog
{
id: connectActionDialog
Loader
{
anchors.fill: parent
source: "DiscoverUM3Action.qml"
}
rightButtons: Button
{
text: catalog.i18nc("@action:button", "Close")
iconName: "dialog-close"
onClicked: connectActionDialog.reject()
}
}
Column
{
objectName: "networkPrinterConnectionInfo"
visible: isUM3
spacing: UM.Theme.getSize("default_margin").width
anchors.fill: parent
Button
{
tooltip: catalog.i18nc("@info:tooltip", "Send access request to the printer")
text: catalog.i18nc("@action:button", "Request Access")
onClicked: Cura.MachineManager.printerOutputDevices[0].requestAuthentication()
visible: printerConnected && !printerAcceptsCommands && !authenticationRequested
}
Row
{
visible: printerConnected
spacing: UM.Theme.getSize("default_margin").width
anchors.left: parent.left
anchors.right: parent.right
height: childrenRect.height
Column
{
Repeater
{
model: Cura.ExtrudersModel { simpleNames: true }
Label { text: model.name }
}
}
Column
{
Repeater
{
id: nozzleColumn
model: printerConnected ? Cura.MachineManager.printerOutputDevices[0].hotendIds : null
Label { text: nozzleColumn.model[index] }
}
}
Column
{
Repeater
{
id: materialColumn
model: printerConnected ? Cura.MachineManager.printerOutputDevices[0].materialNames : null
Label { text: materialColumn.model[index] }
}
}
}
Button
{
tooltip: catalog.i18nc("@info:tooltip", "Load the configuration of the printer into Cura")
text: catalog.i18nc("@action:button", "Activate Configuration")
visible: printerConnected
onClicked: manager.loadConfigurationFromPrinter()
}
}
UM.I18nCatalog{id: catalog; name:"cura"}
}

View file

@ -0,0 +1,20 @@
# Copyright (c) 2015 Ultimaker B.V.
# Cura is released under the terms of the AGPLv3 or higher.
from . import NetworkPrinterOutputDevicePlugin
from . import DiscoverUM3Action
from UM.i18n import i18nCatalog
catalog = i18nCatalog("cura")
def getMetaData():
return {
"plugin": {
"name": "UM3 Network Connection",
"author": "Ultimaker",
"description": catalog.i18nc("@info:whatsthis", "Manages network connections to Ultimaker 3 printers"),
"version": "1.0",
"api": 3
}
}
def register(app):
return { "output_device": NetworkPrinterOutputDevicePlugin.NetworkPrinterOutputDevicePlugin(), "machine_action": DiscoverUM3Action.DiscoverUM3Action()}

View file

@ -432,7 +432,7 @@ class USBPrinterOutputDevice(PrinterOutputDevice):
# This is ignored.
# \param filter_by_machine Whether to filter MIME types by machine. This
# is ignored.
def requestWrite(self, nodes, file_name = None, filter_by_machine = False):
def requestWrite(self, nodes, file_name = None, filter_by_machine = False, file_handler = None):
Application.getInstance().showPrintMonitor.emit(True)
self.startPrint()

View file

@ -27,19 +27,23 @@ class UMOUpgradeSelection(MachineAction):
def setHeatedBed(self, heated_bed = True):
global_container_stack = Application.getInstance().getGlobalContainerStack()
if global_container_stack:
variant = global_container_stack.findContainer({"type": "variant"})
if variant:
if variant.getId() == "empty_variant":
variant_index = global_container_stack.getContainerIndex(variant)
variant = self._createVariant(global_container_stack, variant_index)
variant.setProperty("machine_heated_bed", "value", heated_bed)
self.heatedBedChanged.emit()
# Make sure there is a definition_changes container to store the machine settings
definition_changes_container = global_container_stack.findContainer({"type": "definition_changes"})
if not definition_changes_container:
definition_changes_container = self._createDefinitionChangesContainer(global_container_stack)
def _createVariant(self, global_container_stack, variant_index):
# Create and switch to a variant to store the settings in
new_variant = UM.Settings.InstanceContainer(global_container_stack.getName() + "_variant")
new_variant.addMetaDataEntry("type", "variant")
new_variant.setDefinition(global_container_stack.getBottom())
UM.Settings.ContainerRegistry.getInstance().addContainer(new_variant)
global_container_stack.replaceContainer(variant_index, new_variant)
return new_variant
definition_changes_container.setProperty("machine_heated_bed", "value", heated_bed)
self.heatedBedChanged.emit()
def _createDefinitionChangesContainer(self, global_container_stack):
# Create a definition_changes container to store the settings in and add it to the stack
definition_changes_container = UM.Settings.InstanceContainer(global_container_stack.getName() + "_settings")
definition = global_container_stack.getBottom()
definition_changes_container.setDefinition(definition)
definition_changes_container.addMetaDataEntry("type", "definition_changes")
UM.Settings.ContainerRegistry.getInstance().addContainer(definition_changes_container)
# Insert definition_changes between the definition and the variant
global_container_stack.insertContainer(-1, definition_changes_container)
return definition_changes_container

View file

@ -350,10 +350,22 @@ class XmlMaterialProfile(UM.Settings.InstanceContainer):
mapping[key] = element
first.append(element)
def clearData(self):
self._metadata = {}
self._name = ""
self._definition = None
self._instances = {}
self._read_only = False
self._dirty = False
self._path = ""
## Overridden from InstanceContainer
def deserialize(self, serialized):
data = ET.fromstring(serialized)
# Reset previous metadata
self.clearData() # Ensure any previous data is gone.
self.addMetaDataEntry("type", "material")
self.addMetaDataEntry("base_file", self.id)
@ -413,7 +425,7 @@ class XmlMaterialProfile(UM.Settings.InstanceContainer):
for entry in settings:
key = entry.get("key")
if key in self.__material_property_setting_map:
self.setProperty(self.__material_property_setting_map[key], "value", entry.text, self._definition)
self.setProperty(self.__material_property_setting_map[key], "value", entry.text)
global_setting_values[self.__material_property_setting_map[key]] = entry.text
elif key in self.__unmapped_settings:
if key == "hardware compatible":
@ -455,7 +467,16 @@ class XmlMaterialProfile(UM.Settings.InstanceContainer):
definition = definitions[0]
if machine_compatibility:
new_material = XmlMaterialProfile(self.id + "_" + machine_id)
new_material_id = self.id + "_" + machine_id
# It could be that we are overwriting, so check if the ID already exists.
materials = UM.Settings.ContainerRegistry.getInstance().findInstanceContainers(id=new_material_id)
if materials:
new_material = materials[0]
new_material.clearData()
else:
new_material = XmlMaterialProfile(new_material_id)
new_material.setName(self.getName())
new_material.setMetaData(copy.deepcopy(self.getMetaData()))
new_material.setDefinition(definition)
@ -463,15 +484,14 @@ class XmlMaterialProfile(UM.Settings.InstanceContainer):
new_material.getMetaData()["compatible"] = machine_compatibility
for key, value in global_setting_values.items():
new_material.setProperty(key, "value", value, definition)
new_material.setProperty(key, "value", value)
for key, value in machine_setting_values.items():
new_material.setProperty(key, "value", value, definition)
new_material.setProperty(key, "value", value)
new_material._dirty = False
UM.Settings.ContainerRegistry.getInstance().addContainer(new_material)
if not materials:
UM.Settings.ContainerRegistry.getInstance().addContainer(new_material)
hotends = machine.iterfind("./um:hotend", self.__namespaces)
for hotend in hotends:
@ -501,7 +521,15 @@ class XmlMaterialProfile(UM.Settings.InstanceContainer):
else:
Logger.log("d", "Unsupported material setting %s", key)
new_hotend_material = XmlMaterialProfile(self.id + "_" + machine_id + "_" + hotend_id.replace(" ", "_"))
# It could be that we are overwriting, so check if the ID already exists.
new_hotend_id = self.id + "_" + machine_id + "_" + hotend_id.replace(" ", "_")
materials = UM.Settings.ContainerRegistry.getInstance().findInstanceContainers(id=new_hotend_id)
if materials:
new_hotend_material = materials[0]
new_hotend_material.clearData()
else:
new_hotend_material = XmlMaterialProfile(new_hotend_id)
new_hotend_material.setName(self.getName())
new_hotend_material.setMetaData(copy.deepcopy(self.getMetaData()))
new_hotend_material.setDefinition(definition)
@ -510,16 +538,17 @@ class XmlMaterialProfile(UM.Settings.InstanceContainer):
new_hotend_material.getMetaData()["compatible"] = hotend_compatibility
for key, value in global_setting_values.items():
new_hotend_material.setProperty(key, "value", value, definition)
new_hotend_material.setProperty(key, "value", value)
for key, value in machine_setting_values.items():
new_hotend_material.setProperty(key, "value", value, definition)
new_hotend_material.setProperty(key, "value", value)
for key, value in hotend_setting_values.items():
new_hotend_material.setProperty(key, "value", value, definition)
new_hotend_material.setProperty(key, "value", value)
new_hotend_material._dirty = False
UM.Settings.ContainerRegistry.getInstance().addContainer(new_hotend_material)
if not materials: # It was not added yet, do so now.
UM.Settings.ContainerRegistry.getInstance().addContainer(new_hotend_material)
def _addSettingElement(self, builder, instance):
try:

View file

@ -1196,7 +1196,7 @@
"description": "The minimal temperature while heating up to the Printing Temperature at which printing can already start.",
"unit": "°C",
"type": "float",
"default_value": 190,
"value": "max(-273.15, material_print_temperature - 10)",
"minimum_value": "-273.15",
"minimum_value_warning": "material_standby_temperature",
"maximum_value_warning": "material_print_temperature",
@ -1210,7 +1210,7 @@
"description": "The temperature to which to already start cooling down just before the end of printing.",
"unit": "°C",
"type": "float",
"default_value": 180,
"value": "max(-273.15, material_print_temperature - 15)",
"minimum_value": "-273.15",
"minimum_value_warning": "material_standby_temperature",
"maximum_value_warning": "material_print_temperature",
@ -1696,6 +1696,7 @@
"unit": "mm/s",
"type": "float",
"default_value": 30,
"value": "speed_print * 30 / 60",
"minimum_value": "0.1",
"maximum_value": "math.sqrt(machine_max_feedrate_x ** 2 + machine_max_feedrate_y ** 2)",
"maximum_value_warning": "300",
@ -2296,12 +2297,12 @@
},
"default_value": "all",
"resolve": "'noskin' if 'noskin' in extruderValues('retraction_combing') else ('all' if 'all' in extruderValues('retraction_combing') else 'off')",
"settable_per_mesh": true,
"settable_per_mesh": false,
"settable_per_extruder": false
},
"travel_avoid_other_parts":
{
"label": "Avoid Printed Parts when Traveling",
"label": "Avoid Printed Parts When Traveling",
"description": "The nozzle avoids already printed parts when traveling. This option is only available when combing is enabled.",
"type": "bool",
"default_value": true,
@ -2326,8 +2327,8 @@
},
"start_layers_at_same_position":
{
"label": "Start Layers Near Same Point",
"description": "Start printing the objects in each layer near the same point, so that we don't start a new layer with printing the piece which the previous layer ended with. This makes for better overhangs and small parts, but increases printing time.",
"label": "Start Layers with the Same Part",
"description": "In each layer start with printing the object near the same point, so that we don't start a new layer with printing the piece which the previous layer ended with. This makes for better overhangs and small parts, but increases printing time.",
"type": "bool",
"default_value": false,
"settable_per_mesh": false,
@ -2337,7 +2338,7 @@
"layer_start_x":
{
"label": "Layer Start X",
"description": "The X coordinate of the position near where to start printing objects each layer.",
"description": "The X coordinate of the position near where to find the part to start printing each layer.",
"unit": "mm",
"type": "float",
"default_value": 0.0,
@ -2350,7 +2351,7 @@
"layer_start_y":
{
"label": "Layer Start Y",
"description": "The Y coordinate of the position near where to start printing objects each layer.",
"description": "The Y coordinate of the position near where to find the part to start printing each layer.",
"unit": "mm",
"type": "float",
"default_value": 0.0,
@ -2361,7 +2362,7 @@
"settable_per_meshgroup": true
},
"retraction_hop_enabled": {
"label": "Z Hop when Retracted",
"label": "Z Hop When Retracted",
"description": "Whenever a retraction is done, the build plate is lowered to create clearance between the nozzle and the print. It prevents the nozzle from hitting the print during travel moves, reducing the chance to knock the print from the build plate.",
"type": "bool",
"default_value": false,
@ -3637,7 +3638,6 @@
"unit": "mm",
"enabled": "resolveOrValue('prime_tower_enable')",
"default_value": 15,
"value": "15 if resolveOrValue('prime_tower_enable') else 0",
"resolve": "max(extruderValues('prime_tower_size'))",
"minimum_value": "0",
"maximum_value": "min(0.5 * machine_width, 0.5 * machine_depth)",
@ -3646,6 +3646,38 @@
"settable_per_mesh": false,
"settable_per_extruder": false
},
"prime_tower_min_volume":
{
"label": "Prime Tower Minimum Volume",
"description": "The minimum volume for each layer of the prime tower in order to purge enough material.",
"unit": "mm³",
"type": "float",
"default_value": 10,
"minimum_value": "0",
"maximum_value_warning": "resolveOrValue('prime_tower_size') ** 2 * resolveOrValue('layer_height')",
"enabled": "resolveOrValue('prime_tower_enable')",
"settable_per_mesh": false,
"settable_per_extruder": true,
"children":
{
"prime_tower_wall_thickness":
{
"label": "Prime Tower Thickness",
"description": "The thickness of the hollow prime tower. A thickness larger than half the Prime Tower Minimum Volume will result in a dense prime tower.",
"unit": "mm",
"type": "float",
"default_value": 2,
"value": "max(2 * min(extruderValues('prime_tower_line_width')), 0.5 * (resolveOrValue('prime_tower_size') - math.sqrt(resolveOrValue('prime_tower_size') ** 2 - max(extruderValues('prime_tower_min_volume')) / resolveOrValue('layer_height'))))",
"resolve": "max(extruderValues('prime_tower_wall_thickness'))",
"minimum_value": "0.001",
"minimum_value_warning": "2 * min(extruderValues('prime_tower_line_width'))",
"maximum_value_warning": "resolveOrValue('prime_tower_size') / 2",
"enabled": "resolveOrValue('prime_tower_enable')",
"settable_per_mesh": false,
"settable_per_extruder": false
}
}
},
"prime_tower_position_x":
{
"label": "Prime Tower X Position",
@ -3692,14 +3724,23 @@
},
"prime_tower_wipe_enabled":
{
"label": "Wipe Nozzle on Prime Tower",
"label": "Wipe Inactive Nozzle on Prime Tower",
"description": "After printing the prime tower with one nozzle, wipe the oozed material from the other nozzle off on the prime tower.",
"type": "bool",
"enabled": "resolveOrValue('prime_tower_enable')",
"default_value": true,
"resolve": "any(extruderValues('prime_tower_wipe_enabled'))",
"settable_per_mesh": false,
"settable_per_extruder": false
"settable_per_extruder": true
},
"dual_pre_wipe":
{
"label": "Wipe Nozzle After Switch",
"description": "After switching extruder, wipe the oozed material off of the nozzle on the first thing printed. This performs a safe slow wipe move at a place where the oozed material causes least harm to the surface quality of your print.",
"type": "bool",
"enabled": "resolveOrValue('prime_tower_enable')",
"default_value": true,
"settable_per_mesh": false,
"settable_per_extruder": true
},
"multiple_mesh_overlap":
{
@ -3763,7 +3804,7 @@
"meshfix_union_all":
{
"label": "Union Overlapping Volumes",
"description": "Ignore the internal geometry arising from overlapping volumes and print the volumes as one. This may cause internal cavities to disappear.",
"description": "Ignore the internal geometry arising from overlapping volumes within a mesh and print the volumes as one. This may cause unintended internal cavities to disappear.",
"type": "bool",
"default_value": true,
"settable_per_mesh": true
@ -3795,7 +3836,7 @@
"carve_multiple_volumes":
{
"label": "Remove Mesh Intersection",
"description": "Remove areas where multiple objects are overlapping with each other. This may be used if merged dual material objects overlap with each other.",
"description": "Remove areas where multiple meshes are overlapping with each other. This may be used if merged dual material objects overlap with each other.",
"type": "bool",
"default_value": true,
"value": "machine_extruder_count > 1",
@ -3806,7 +3847,7 @@
"alternate_carve_order":
{
"label": "Alternate Mesh Removal",
"description": "With every layer switch to which model the intersecting volumes will belong, so that the overlapping volumes become interwoven.",
"description": "Switch to which mesh intersecting volumes will belong with every layer, so that the overlapping meshes become interwoven. Turning this setting off will cause one of the meshes to obtain all of the volume in the overlap, while it is removed from the other meshes.",
"type": "bool",
"default_value": true,
"enabled": "carve_multiple_volumes",
@ -3865,6 +3906,28 @@
"settable_per_meshgroup": false,
"settable_globally": false
},
"support_mesh":
{
"label": "Support Mesh",
"description": "Use this mesh to specify support areas. This can be used to generate support structure.",
"type": "bool",
"default_value": false,
"settable_per_mesh": true,
"settable_per_extruder": false,
"settable_per_meshgroup": false,
"settable_globally": false
},
"anti_overhang_mesh":
{
"label": "Anti Overhang Mesh",
"description": "Use this mesh to specify where no part of the model should be detected as overhang. This can be used to remove unwanted support structure.",
"type": "bool",
"default_value": false,
"settable_per_mesh": true,
"settable_per_extruder": false,
"settable_per_meshgroup": false,
"settable_globally": false
},
"magic_mesh_surface_mode":
{
"label": "Surface Mode",

View file

@ -0,0 +1,35 @@
{
"id": "jellybox",
"version": 2,
"name": "JellyBOX",
"inherits": "fdmprinter",
"metadata": {
"visible": true,
"author": "IMADE3D",
"manufacturer": "IMADE3D",
"category": "Other",
"platform": "jellybox_platform.stl",
"platform_offset": [ 0, -0.3, 0],
"file_formats": "text/x-gcode",
"has_materials": true,
"has_machine_materials": true
},
"overrides": {
"machine_name": { "default_value": "IMADE3D JellyBOX" },
"machine_width": { "default_value": 170 },
"machine_height": { "default_value": 145 },
"machine_depth": { "default_value": 160 },
"machine_nozzle_size": { "default_value": 0.4 },
"material_diameter": { "default_value": 1.75 },
"machine_heated_bed": { "default_value": true },
"machine_center_is_zero": { "default_value": false },
"machine_gcode_flavor": { "default_value": "RepRap (Marlin/Sprinter)" },
"machine_start_gcode": {
"default_value": ";---------------------------------------\n; ; ; Jellybox Start Script Begin ; ; ;\n;_______________________________________\n; M92 E140 ;optionally adjust steps per mm for your filament\n\n; Print Settings Summary\n; (overwriting these values will NOT change your printer's behavior)\n; sliced for: {machine_name}\n; nozzle diameter: {machine_nozzle_size}\n; filament diameter: {material_diameter}\n; layer height: {layer_height}\n; 1st layer height: {layer_height_0}\n; line width: {line_width}\n; wall thickness: {wall_thickness}\n; infill density: {infill_sparse_density}\n; infill pattern: {infill_pattern}\n; print temperature: {material_print_temperature}\n; heated bed temperature: {material_bed_temperature}\n; regular fan speed: {cool_fan_speed_min}\n; max fan speed: {cool_fan_speed_max}\n; support? {support_enable}\n; spiralized? {magic_spiralize}\n\nM117 Preparing ;write Preparing\nM140 S{material_bed_temperature} ;set bed temperature and move on\nM104 S{material_print_temperature} ;set extruder temperature and move on\nM206 X10.0 Y0.0 ;set x homing offset for default bed leveling\nG21 ;metric values\nG90 ;absolute positioning\nM107 ;start with the fan off\nM82 ;set extruder to absolute mode\nG28 ;home all axes\nM203 Z5 ;slow Z speed down for greater accuracy when probing\nG29 ;auto bed leveling procedure\nM203 Z7 ;pick up z speed again for printing\nM190 S{material_bed_temperature} ;wait for the bed to reach desired temperature\nM109 S{material_print_temperature} ;wait for the extruder to reach desired temperature\nG92 E0 ;reset the extruder position\nG1 F200 E5 ;extrude 5mm of feed stock\nG92 E0 ;reset the extruder position again\nM117 Print starting ;write Print starting\n;---------------------------------------------\n; ; ; Jellybox Printer Start Script End ; ; ;\n;_____________________________________________"
},
"machine_end_gcode": {
"default_value": "\n;---------------------------------\n;;; Jellybox End Script Begin ;;;\n;_________________________________\nM117 Finishing Up ;write Finishing Up\n\nM104 S0 ;extruder heater off\nM140 S0 ;bed heater off (if you have it)\nG91 ;relative positioning\nG1 E-1 F300 ;retract the filament a bit before lifting the nozzle, to release some of the pressure\nG1 Z+0.5 E-5 X-20 Y-20 F9000 ;move Z up a bit and retract filament even more\nG90 ;absolute positioning\nG28 X ;home x, so the head is out of the way\nG1 Y100 ;move Y forward, so the print is more accessible\nM84 ;steppers off\n\nM117 Print finished ;write Print finished\n;---------------------------------------\n;;; Jellybox End Script End ;;;\n;_______________________________________"
}
}
}

View file

@ -70,6 +70,7 @@
"machine_end_gcode": { "default_value": "" },
"prime_tower_position_x": { "default_value": 175 },
"prime_tower_position_y": { "default_value": 179 },
"prime_tower_wipe_enabled": { "default_value": false },
"acceleration_enabled": { "value": "True" },
"acceleration_layer_0": { "value": "acceleration_topbottom" },
@ -108,15 +109,12 @@
"multiple_mesh_overlap": { "value": "0" },
"prime_tower_enable": { "value": "True" },
"raft_airgap": { "value": "0" },
"raft_base_speed": { "value": "20" },
"raft_base_thickness": { "value": "0.3" },
"raft_interface_line_spacing": { "value": "0.5" },
"raft_interface_line_width": { "value": "0.5" },
"raft_interface_speed": { "value": "20" },
"raft_interface_thickness": { "value": "0.2" },
"raft_jerk": { "value": "jerk_layer_0" },
"raft_margin": { "value": "10" },
"raft_speed": { "value": "25" },
"raft_surface_layers": { "value": "1" },
"retraction_amount": { "value": "2" },
"retraction_count_max": { "value": "10" },
@ -127,7 +125,7 @@
"retraction_min_travel": { "value": "5" },
"retraction_prime_speed": { "value": "15" },
"skin_overlap": { "value": "10" },
"speed_layer_0": { "value": "20" },
"speed_layer_0": { "value": "speed_print * 30 / 70" },
"speed_prime_tower": { "value": "speed_topbottom" },
"speed_print": { "value": "35" },
"speed_support": { "value": "speed_wall_0" },

View file

@ -73,10 +73,10 @@
"default_value": 2
},
"prime_tower_position_x": {
"default_value": 185
"default_value": 195
},
"prime_tower_position_y": {
"default_value": 160
"default_value": 149
}
}
}

View file

@ -1913,7 +1913,7 @@ msgstr "&Beenden"
#: /home/ruben/Projects/Cura/resources/qml/Actions.qml:97
msgctxt "@action:inmenu"
msgid "Configure Cura..."
msgstr "Cura wird konfiguriert..."
msgstr "Cura konfigurieren..."
#: /home/ruben/Projects/Cura/resources/qml/Actions.qml:104
msgctxt "@action:inmenu menubar:printer"

View file

@ -2629,3 +2629,8 @@ msgstr "De configuratie van de printer in Cura laden"
msgctxt "@action:button"
msgid "Activate Configuration"
msgstr "Configuratie Activeren"
#: /home/ruben/Projects/Cura/resources/qml/Preferences/MaterialView.qml:25
msgctxt "@title"
msgid "Information"
msgstr "Informatie"

Binary file not shown.

View file

@ -11,6 +11,7 @@ import Cura 1.0 as Cura
Item
{
property alias open: openAction;
property alias loadWorkspace: loadWorkspaceAction;
property alias quit: quitAction;
property alias undo: undoAction;
@ -122,7 +123,7 @@ Item
{
id: updateProfileAction;
enabled: !Cura.MachineManager.stacksHaveErrors && Cura.MachineManager.hasUserSettings && !Cura.MachineManager.isReadOnly(Cura.MachineManager.activeQualityId)
text: catalog.i18nc("@action:inmenu menubar:profile","&Update profile with current settings");
text: catalog.i18nc("@action:inmenu menubar:profile","&Update profile with current settings/overrides");
onTriggered: Cura.ContainerManager.updateQualityChanges();
}
@ -142,7 +143,7 @@ Item
{
id: addProfileAction;
enabled: !Cura.MachineManager.stacksHaveErrors && Cura.MachineManager.hasUserSettings
text: catalog.i18nc("@action:inmenu menubar:profile","&Create profile from current settings...");
text: catalog.i18nc("@action:inmenu menubar:profile","&Create profile from current settings/overrides...");
}
Action
@ -286,6 +287,12 @@ Item
shortcut: StandardKey.Open;
}
Action
{
id: loadWorkspaceAction
text: catalog.i18nc("@action:inmenu menubar:file","&Open Workspace...");
}
Action
{
id: showEngineLogAction;

View file

@ -7,7 +7,7 @@ import QtQuick.Controls.Styles 1.1
import QtQuick.Layouts 1.1
import QtQuick.Dialogs 1.1
import UM 1.2 as UM
import UM 1.3 as UM
import Cura 1.0 as Cura
import "Menus"
@ -67,9 +67,14 @@ UM.MainWindow
id: fileMenu
title: catalog.i18nc("@title:menu menubar:toplevel","&File");
MenuItem {
MenuItem
{
action: Cura.Actions.open;
}
MenuItem
{
action: Cura.Actions.loadWorkspace
}
RecentFilesMenu { }
@ -102,6 +107,12 @@ UM.MainWindow
onObjectRemoved: saveAllMenu.removeItem(object)
}
}
MenuItem
{
id: saveWorkspaceMenu
text: catalog.i18nc("@title:menu menubar:file","Save Workspace")
onTriggered: UM.OutputDeviceManager.requestWriteToDevice("local_file", PrintInformation.jobName, { "filter_by_machine": false, "file_type": "workspace" });
}
MenuItem { action: Cura.Actions.reloadAll; }
@ -723,6 +734,38 @@ UM.MainWindow
onTriggered: openDialog.open()
}
FileDialog
{
id: openWorkspaceDialog;
//: File open dialog title
title: catalog.i18nc("@title:window","Open workspace")
modality: UM.Application.platform == "linux" ? Qt.NonModal : Qt.WindowModal;
selectMultiple: false
nameFilters: UM.WorkspaceFileHandler.supportedReadFileTypes;
folder: CuraApplication.getDefaultPath("dialog_load_path")
onAccepted:
{
//Because several implementations of the file dialog only update the folder
//when it is explicitly set.
var f = folder;
folder = f;
CuraApplication.setDefaultPath("dialog_load_path", folder);
for(var i in fileUrls)
{
UM.WorkspaceFileHandler.readLocalFile(fileUrls[i])
}
}
}
Connections
{
target: Cura.Actions.loadWorkspace
onTriggered:openWorkspaceDialog.open()
}
EngineLog
{
id: engineLog;

View file

@ -162,7 +162,7 @@ UM.ManagementPage
Button
{
text: {
return catalog.i18nc("@action:button", "Update profile with current settings");
return catalog.i18nc("@action:button", "Update profile with current settings/overrides");
}
enabled: Cura.MachineManager.hasUserSettings && !Cura.MachineManager.isReadOnly(Cura.MachineManager.activeQualityId)
onClicked: Cura.ContainerManager.updateQualityChanges()
@ -187,7 +187,7 @@ UM.ManagementPage
Label {
id: defaultsMessage
visible: false
text: catalog.i18nc("@action:label", "This profile uses the defaults specified by the printer, so it has no settings in the list below.")
text: catalog.i18nc("@action:label", "This profile uses the defaults specified by the printer, so it has no settings/overrides in the list below.")
wrapMode: Text.WordWrap
width: parent.width
}

View file

@ -30,6 +30,7 @@ SettingItem
textRole: "name"
anchors.fill: parent
onCurrentIndexChanged: updateCurrentColor();
MouseArea
{
@ -115,12 +116,37 @@ SettingItem
propertyProvider.setPropertyValue("value", extruders_model.getItem(index).index);
control.color = extruders_model.getItem(index).color;
}
onModelChanged: updateCurrentIndex();
Connections
Binding
{
target: propertyProvider
onPropertiesChanged: control.updateCurrentIndex();
target: control
property: "currentIndex"
value:
{
for(var i = 0; i < extruders_model.rowCount(); ++i)
{
if(extruders_model.getItem(i).index == propertyProvider.properties.value)
{
return i;
}
}
return -1;
}
}
// In some cases we want to update the current color without updating the currentIndex, so it's a seperate function.
function updateCurrentColor()
{
for(var i = 0; i < extruders_model.rowCount(); ++i)
{
if(extruders_model.getItem(i).index == propertyProvider.properties.value)
{
control.color = extruders_model.getItem(i).color;
return;
}
}
}
function updateCurrentIndex()
@ -130,7 +156,6 @@ SettingItem
if(extruders_model.getItem(i).index == propertyProvider.properties.value)
{
control.currentIndex = i;
control.color = extruders_model.getItem(i).color;
return;
}
}

View file

@ -118,6 +118,7 @@ Item {
elide: Text.ElideMiddle;
color: UM.Theme.getColor("setting_control_text");
opacity: (definition.visible) ? 1 : 0.5
// emphasize the setting if it has a value in the user or quality profile
font: base.doQualityUserSettingEmphasis && base.stackLevel != undefined && base.stackLevel <= 1 ? UM.Theme.getFont("default_italic") : UM.Theme.getFont("default")
}
@ -209,14 +210,26 @@ Item {
// But this will cause the binding to be re-evaluated when the enabled property changes.
return false;
}
// There are no settings with any warning.
if(Cura.SettingInheritanceManager.settingsWithInheritanceWarning.length == 0)
{
return false;
}
// This setting has a resolve value, so an inheritance warning doesn't do anything.
if(resolve != "None")
{
return false
}
// If the setting does not have a limit_to_extruder property (or is -1), use the active stack.
if(globalPropertyProvider.properties.limit_to_extruder == null || globalPropertyProvider.properties.limit_to_extruder == -1)
{
return Cura.SettingInheritanceManager.settingsWithInheritanceWarning.indexOf(definition.key) >= 0;
}
// Setting does have a limit_to_extruder property, so use that one instead.
return Cura.SettingInheritanceManager.getOverridesForExtruder(definition.key, globalPropertyProvider.properties.limit_to_extruder).indexOf(definition.key) >= 0;
}
@ -227,7 +240,7 @@ Item {
focus = true;
// Get the most shallow function value (eg not a number) that we can find.
var last_entry = propertyProvider.stackLevels[propertyProvider.stackLevels.length]
var last_entry = propertyProvider.stackLevels[propertyProvider.stackLevels.length - 1]
for (var i = 1; i < base.stackLevels.length; i++)
{
var has_setting_function = typeof(propertyProvider.getPropertyValue("value", base.stackLevels[i])) == "object";

View file

@ -9,229 +9,405 @@ import QtQuick.Layouts 1.1
import UM 1.2 as UM
import Cura 1.0 as Cura
ScrollView
Item
{
id: base;
style: UM.Theme.styles.scrollview;
flickableItem.flickableDirection: Flickable.VerticalFlick;
property Action configureSettings;
property bool findingSettings;
signal showTooltip(Item item, point location, string text);
signal hideTooltip();
ListView
function toggleFilterField()
{
id: contents
spacing: UM.Theme.getSize("default_lining").height;
cacheBuffer: 1000000; // Set a large cache to effectively just cache every list item.
model: UM.SettingDefinitionsModel {
id: definitionsModel;
containerId: Cura.MachineManager.activeDefinitionId
visibilityHandler: UM.SettingPreferenceVisibilityHandler { }
exclude: ["machine_settings", "command_line_settings", "infill_mesh", "infill_mesh_order"] // TODO: infill_mesh settigns are excluded hardcoded, but should be based on the fact that settable_globally, settable_per_meshgroup and settable_per_extruder are false.
expanded: Printer.expandedCategories
onExpandedChanged: Printer.setExpandedCategories(expanded)
onVisibilityChanged: Cura.SettingInheritanceManager.forceUpdate()
}
delegate: Loader
filterContainer.visible = !filterContainer.visible
if(filterContainer.visible)
{
id: delegate
width: UM.Theme.getSize("sidebar").width;
height: provider.properties.enabled == "True" ? UM.Theme.getSize("section").height : - contents.spacing
Behavior on height { NumberAnimation { duration: 100 } }
opacity: provider.properties.enabled == "True" ? 1 : 0
Behavior on opacity { NumberAnimation { duration: 100 } }
enabled:
{
if(!ExtruderManager.activeExtruderStackId && ExtruderManager.extruderCount > 0)
{
// disable all controls on the global tab, except categories
return model.type == "category"
}
return provider.properties.enabled == "True"
}
property var definition: model
property var settingDefinitionsModel: definitionsModel
property var propertyProvider: provider
property var globalPropertyProvider: inheritStackProvider
//Qt5.4.2 and earlier has a bug where this causes a crash: https://bugreports.qt.io/browse/QTBUG-35989
//In addition, while it works for 5.5 and higher, the ordering of the actual combo box drop down changes,
//causing nasty issues when selecting different options. So disable asynchronous loading of enum type completely.
asynchronous: model.type != "enum" && model.type != "extruder"
active: model.type != undefined
source:
{
switch(model.type)
{
case "int":
return "SettingTextField.qml"
case "float":
return "SettingTextField.qml"
case "enum":
return "SettingComboBox.qml"
case "extruder":
return "SettingExtruder.qml"
case "bool":
return "SettingCheckBox.qml"
case "str":
return "SettingTextField.qml"
case "category":
return "SettingCategory.qml"
default:
return "SettingUnknown.qml"
}
}
// Binding to ensure that the right containerstack ID is set for the provider.
// This ensures that if a setting has a limit_to_extruder id (for instance; Support speed points to the
// extruder that actually prints the support, as that is the setting we need to use to calculate the value)
Binding
{
target: provider
property: "containerStackId"
when: model.settable_per_extruder || (inheritStackProvider.properties.limit_to_extruder != null && inheritStackProvider.properties.limit_to_extruder >= 0);
value:
{
if(!model.settable_per_extruder || machineExtruderCount.properties.value == 1)
{
//Not settable per extruder or there only is global, so we must pick global.
return Cura.MachineManager.activeMachineId;
}
if(inheritStackProvider.properties.limit_to_extruder != null && inheritStackProvider.properties.limit_to_extruder >= 0)
{
//We have limit_to_extruder, so pick that stack.
return ExtruderManager.extruderIds[String(inheritStackProvider.properties.limit_to_extruder)];
}
if(ExtruderManager.activeExtruderStackId)
{
//We're on an extruder tab. Pick the current extruder.
return ExtruderManager.activeExtruderStackId;
}
//No extruder tab is selected. Pick the global stack. Shouldn't happen any more since we removed the global tab.
return Cura.MachineManager.activeMachineId;
}
}
// Specialty provider that only watches global_inherits (we cant filter on what property changed we get events
// so we bypass that to make a dedicated provider).
UM.SettingPropertyProvider
{
id: inheritStackProvider
containerStackId: Cura.MachineManager.activeMachineId
key: model.key
watchedProperties: [ "limit_to_extruder" ]
}
UM.SettingPropertyProvider
{
id: provider
containerStackId: Cura.MachineManager.activeMachineId
key: model.key ? model.key : ""
watchedProperties: [ "value", "enabled", "state", "validationState", "settable_per_extruder", "resolve" ]
storeIndex: 0
// Due to the way setPropertyValue works, removeUnusedValue gives the correct output in case of resolve
removeUnusedValue: model.resolve == undefined
}
Connections
{
target: item
onContextMenuRequested:
{
contextMenu.key = model.key;
contextMenu.provider = provider
contextMenu.popup();
}
onShowTooltip: base.showTooltip(delegate, { x: 0, y: delegate.height / 2 }, text)
onHideTooltip: base.hideTooltip()
onShowAllHiddenInheritedSettings:
{
var children_with_override = Cura.SettingInheritanceManager.getChildrenKeysWithOverride(category_id)
for(var i = 0; i < children_with_override.length; i++)
{
definitionsModel.setVisible(children_with_override[i], true)
}
Cura.SettingInheritanceManager.manualRemoveOverride(category_id)
}
}
filter.forceActiveFocus();
}
UM.I18nCatalog { id: catalog; name: "uranium"; }
add: Transition {
SequentialAnimation {
NumberAnimation { properties: "height"; from: 0; duration: 100 }
NumberAnimation { properties: "opacity"; from: 0; duration: 100 }
}
}
remove: Transition {
SequentialAnimation {
NumberAnimation { properties: "opacity"; to: 0; duration: 100 }
NumberAnimation { properties: "height"; to: 0; duration: 100 }
}
}
addDisplaced: Transition {
NumberAnimation { properties: "x,y"; duration: 100 }
}
removeDisplaced: Transition {
SequentialAnimation {
PauseAnimation { duration: 100; }
NumberAnimation { properties: "x,y"; duration: 100 }
}
}
Menu
else
{
id: contextMenu
property string key
property var provider
MenuItem
{
//: Settings context menu action
text: catalog.i18nc("@action:menu", "Copy value to all extruders")
visible: machineExtruderCount.properties.value > 1
enabled: contextMenu.provider != undefined && contextMenu.provider.properties.settable_per_extruder != "False"
onTriggered: Cura.MachineManager.copyValueToExtruders(contextMenu.key)
}
MenuSeparator
{
visible: machineExtruderCount.properties.value > 1
}
MenuItem
{
//: Settings context menu action
text: catalog.i18nc("@action:menu", "Hide this setting");
onTriggered: definitionsModel.hide(contextMenu.key);
}
MenuItem
{
//: Settings context menu action
text: catalog.i18nc("@action:menu", "Configure setting visiblity...");
onTriggered: Cura.Actions.configureSettingVisibility.trigger(contextMenu);
}
}
UM.SettingPropertyProvider
{
id: machineExtruderCount
containerStackId: Cura.MachineManager.activeMachineId
key: "machine_extruder_count"
watchedProperties: [ "value" ]
storeIndex: 0
filter.text = "";
}
}
}
Rectangle
{
id: filterContainer
visible: false
border.width: UM.Theme.getSize("default_lining").width
border.color:
{
if(hoverMouseArea.containsMouse || clearFilterButton.containsMouse)
{
return UM.Theme.getColor("setting_control_border_highlight");
}
else
{
return UM.Theme.getColor("setting_control_border");
}
}
color: UM.Theme.getColor("setting_control")
anchors
{
top: parent.top
left: parent.left
leftMargin: UM.Theme.getSize("default_margin").width
right: parent.right
rightMargin: UM.Theme.getSize("default_margin").width
}
height: visible ? UM.Theme.getSize("setting_control").height : 0
Behavior on height { NumberAnimation { duration: 100 } }
TextField
{
id: filter;
anchors.left: parent.left
anchors.right: clearFilterButton.left
anchors.rightMargin: UM.Theme.getSize("default_margin").width
placeholderText: catalog.i18nc("@label:textbox", "Filter...")
style: TextFieldStyle
{
textColor: UM.Theme.getColor("setting_control_text");
font: UM.Theme.getFont("default");
background: Item {}
}
property var expandedCategories
property bool lastFindingSettings: false
onTextChanged:
{
definitionsModel.filter = {"label": "*" + text};
findingSettings = (text.length > 0);
if(findingSettings != lastFindingSettings)
{
if(findingSettings)
{
expandedCategories = definitionsModel.expanded.slice();
definitionsModel.expanded = ["*"];
definitionsModel.showAncestors = true;
definitionsModel.showAll = true;
}
else
{
definitionsModel.expanded = expandedCategories;
definitionsModel.showAncestors = false;
definitionsModel.showAll = false;
}
lastFindingSettings = findingSettings;
}
}
Keys.onEscapePressed:
{
filter.text = "";
}
}
MouseArea
{
id: hoverMouseArea
anchors.fill: parent
hoverEnabled: true
acceptedButtons: Qt.NoButton
cursorShape: Qt.IBeamCursor
}
UM.SimpleButton
{
id: clearFilterButton
iconSource: UM.Theme.getIcon("cross1")
visible: findingSettings
height: parent.height * 0.4
width: visible ? height : 0
anchors.verticalCenter: parent.verticalCenter
anchors.right: parent.right
anchors.rightMargin: UM.Theme.getSize("default_margin").width
color: UM.Theme.getColor("setting_control_button")
hoverColor: UM.Theme.getColor("setting_control_button_hover")
onClicked:
{
filter.text = "";
filter.forceActiveFocus();
}
}
}
ScrollView
{
anchors.top: filterContainer.bottom;
anchors.bottom: parent.bottom;
anchors.right: parent.right;
anchors.left: parent.left;
anchors.topMargin: filterContainer.visible ? UM.Theme.getSize("default_margin").width : 0
Behavior on anchors.topMargin { NumberAnimation { duration: 100 } }
style: UM.Theme.styles.scrollview;
flickableItem.flickableDirection: Flickable.VerticalFlick;
ListView
{
id: contents
spacing: UM.Theme.getSize("default_lining").height;
cacheBuffer: 1000000; // Set a large cache to effectively just cache every list item.
model: UM.SettingDefinitionsModel
{
id: definitionsModel;
containerId: Cura.MachineManager.activeDefinitionId
visibilityHandler: UM.SettingPreferenceVisibilityHandler { }
exclude: ["machine_settings", "command_line_settings", "infill_mesh", "infill_mesh_order"] // TODO: infill_mesh settigns are excluded hardcoded, but should be based on the fact that settable_globally, settable_per_meshgroup and settable_per_extruder are false.
expanded: Printer.expandedCategories
onExpandedChanged:
{
if(!findingSettings)
{
// Do not change expandedCategories preference while filtering settings
// because all categories are expanded while filtering
Printer.setExpandedCategories(expanded)
}
}
onVisibilityChanged: Cura.SettingInheritanceManager.forceUpdate()
}
delegate: Loader
{
id: delegate
width: UM.Theme.getSize("sidebar").width;
height: provider.properties.enabled == "True" ? UM.Theme.getSize("section").height : - contents.spacing
Behavior on height { NumberAnimation { duration: 100 } }
opacity: provider.properties.enabled == "True" ? 1 : 0
Behavior on opacity { NumberAnimation { duration: 100 } }
enabled:
{
if(!ExtruderManager.activeExtruderStackId && ExtruderManager.extruderCount > 0)
{
// disable all controls on the global tab, except categories
return model.type == "category"
}
return provider.properties.enabled == "True"
}
property var definition: model
property var settingDefinitionsModel: definitionsModel
property var propertyProvider: provider
property var globalPropertyProvider: inheritStackProvider
//Qt5.4.2 and earlier has a bug where this causes a crash: https://bugreports.qt.io/browse/QTBUG-35989
//In addition, while it works for 5.5 and higher, the ordering of the actual combo box drop down changes,
//causing nasty issues when selecting different options. So disable asynchronous loading of enum type completely.
asynchronous: model.type != "enum" && model.type != "extruder"
active: model.type != undefined
source:
{
switch(model.type)
{
case "int":
return "SettingTextField.qml"
case "float":
return "SettingTextField.qml"
case "enum":
return "SettingComboBox.qml"
case "extruder":
return "SettingExtruder.qml"
case "bool":
return "SettingCheckBox.qml"
case "str":
return "SettingTextField.qml"
case "category":
return "SettingCategory.qml"
default:
return "SettingUnknown.qml"
}
}
// Binding to ensure that the right containerstack ID is set for the provider.
// This ensures that if a setting has a limit_to_extruder id (for instance; Support speed points to the
// extruder that actually prints the support, as that is the setting we need to use to calculate the value)
Binding
{
target: provider
property: "containerStackId"
when: model.settable_per_extruder || (inheritStackProvider.properties.limit_to_extruder != null && inheritStackProvider.properties.limit_to_extruder >= 0);
value:
{
if(!model.settable_per_extruder || machineExtruderCount.properties.value == 1)
{
//Not settable per extruder or there only is global, so we must pick global.
return Cura.MachineManager.activeMachineId;
}
if(inheritStackProvider.properties.limit_to_extruder != null && inheritStackProvider.properties.limit_to_extruder >= 0)
{
//We have limit_to_extruder, so pick that stack.
return ExtruderManager.extruderIds[String(inheritStackProvider.properties.limit_to_extruder)];
}
if(ExtruderManager.activeExtruderStackId)
{
//We're on an extruder tab. Pick the current extruder.
return ExtruderManager.activeExtruderStackId;
}
//No extruder tab is selected. Pick the global stack. Shouldn't happen any more since we removed the global tab.
return Cura.MachineManager.activeMachineId;
}
}
// Specialty provider that only watches global_inherits (we cant filter on what property changed we get events
// so we bypass that to make a dedicated provider).
UM.SettingPropertyProvider
{
id: inheritStackProvider
containerStackId: Cura.MachineManager.activeMachineId
key: model.key
watchedProperties: [ "limit_to_extruder" ]
}
UM.SettingPropertyProvider
{
id: provider
containerStackId: Cura.MachineManager.activeMachineId
key: model.key ? model.key : ""
watchedProperties: [ "value", "enabled", "state", "validationState", "settable_per_extruder", "resolve" ]
storeIndex: 0
// Due to the way setPropertyValue works, removeUnusedValue gives the correct output in case of resolve
removeUnusedValue: model.resolve == undefined
}
Connections
{
target: item
onContextMenuRequested:
{
contextMenu.key = model.key;
contextMenu.settingVisible = model.visible;
contextMenu.provider = provider
contextMenu.popup();
}
onShowTooltip: base.showTooltip(delegate, { x: 0, y: delegate.height / 2 }, text)
onHideTooltip: base.hideTooltip()
onShowAllHiddenInheritedSettings:
{
var children_with_override = Cura.SettingInheritanceManager.getChildrenKeysWithOverride(category_id)
for(var i = 0; i < children_with_override.length; i++)
{
definitionsModel.setVisible(children_with_override[i], true)
}
Cura.SettingInheritanceManager.manualRemoveOverride(category_id)
}
}
}
UM.I18nCatalog { id: catalog; name: "uranium"; }
add: Transition {
SequentialAnimation {
NumberAnimation { properties: "height"; from: 0; duration: 100 }
NumberAnimation { properties: "opacity"; from: 0; duration: 100 }
}
}
remove: Transition {
SequentialAnimation {
NumberAnimation { properties: "opacity"; to: 0; duration: 100 }
NumberAnimation { properties: "height"; to: 0; duration: 100 }
}
}
addDisplaced: Transition {
NumberAnimation { properties: "x,y"; duration: 100 }
}
removeDisplaced: Transition {
SequentialAnimation {
PauseAnimation { duration: 100; }
NumberAnimation { properties: "x,y"; duration: 100 }
}
}
Menu
{
id: contextMenu
property string key
property var provider
property bool settingVisible
MenuItem
{
//: Settings context menu action
text: catalog.i18nc("@action:menu", "Copy value to all extruders")
visible: machineExtruderCount.properties.value > 1
enabled: contextMenu.provider != undefined && contextMenu.provider.properties.settable_per_extruder != "False"
onTriggered: Cura.MachineManager.copyValueToExtruders(contextMenu.key)
}
MenuSeparator
{
visible: machineExtruderCount.properties.value > 1
}
MenuItem
{
//: Settings context menu action
visible: !findingSettings;
text: catalog.i18nc("@action:menu", "Hide this setting");
onTriggered: definitionsModel.hide(contextMenu.key);
}
MenuItem
{
//: Settings context menu action
text:
{
if (contextMenu.settingVisible)
{
return catalog.i18nc("@action:menu", "Don't show this setting");
}
else
{
return catalog.i18nc("@action:menu", "Keep this setting visible");
}
}
visible: findingSettings;
onTriggered:
{
if (contextMenu.settingVisible)
{
definitionsModel.hide(contextMenu.key);
}
else
{
definitionsModel.show(contextMenu.key);
}
}
}
MenuItem
{
//: Settings context menu action
text: catalog.i18nc("@action:menu", "Configure setting visiblity...");
onTriggered: Cura.Actions.configureSettingVisibility.trigger(contextMenu);
}
}
UM.SettingPropertyProvider
{
id: machineExtruderCount
containerStackId: Cura.MachineManager.activeMachineId
key: "machine_extruder_count"
watchedProperties: [ "value" ]
storeIndex: 0
}
}
}
}

View file

@ -214,7 +214,7 @@ Rectangle
anchors.left: parent.left
anchors.leftMargin: model.index * (settingsModeSelection.width / 2)
anchors.verticalCenter: parent.verticalCenter
width: parent.width / 2
width: 0.5 * parent.width - (model.showFilterButton ? toggleFilterButton.width : 0)
text: model.text
exclusiveGroup: modeMenuGroup;
checkable: true;
@ -256,6 +256,44 @@ Rectangle
}
}
Button
{
id: toggleFilterButton
anchors.right: parent.right
anchors.rightMargin: UM.Theme.getSize("default_margin").width
anchors.top: headerSeparator.bottom
anchors.topMargin: UM.Theme.getSize("default_margin").height
height: settingsModeSelection.height
width: visible ? height : 0
visible: !monitoringPrint && modesListModel.get(base.currentModeIndex).showFilterButton
opacity: visible ? 1 : 0
onClicked: sidebarContents.currentItem.toggleFilterField()
style: ButtonStyle
{
background: Rectangle
{
border.width: UM.Theme.getSize("default_lining").width
border.color: UM.Theme.getColor("toggle_checked_border")
color: visible ? UM.Theme.getColor("toggle_checked") : UM.Theme.getColor("toggle_hovered")
Behavior on color { ColorAnimation { duration: 50; } }
}
label: UM.RecolorImage
{
anchors.verticalCenter: control.verticalCenter
anchors.right: parent.right
anchors.rightMargin: UM.Theme.getSize("default_margin").width / 2
source: UM.Theme.getIcon("search")
color: UM.Theme.getColor("toggle_checked_text")
}
}
}
Label {
id: monitorLabel
text: catalog.i18nc("@label","Printer Monitor");
@ -379,8 +417,8 @@ Rectangle
Component.onCompleted:
{
modesListModel.append({ text: catalog.i18nc("@title:tab", "Recommended"), item: sidebarSimple })
modesListModel.append({ text: catalog.i18nc("@title:tab", "Custom"), item: sidebarAdvanced })
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 });
}

View file

@ -329,7 +329,7 @@ Column
}
onEntered:
{
var content = catalog.i18nc("@tooltip","Some setting values are different from the values stored in the profile.\n\nClick to open the profile manager.")
var content = catalog.i18nc("@tooltip","Some setting/override values are different from the values stored in the profile.\n\nClick to open the profile manager.")
base.showTooltip(globalProfileRow, Qt.point(0, globalProfileRow.height / 2), content)
}
onExited: base.hideTooltip()

View file

@ -15,5 +15,6 @@ wall_thickness = 0.88
top_bottom_thickness = 0.72
infill_sparse_density = 22
speed_print = 30
speed_layer_0 = =round(speed_print * 30 / 30)
cool_min_layer_time = 5
cool_min_speed = 10

View file

@ -15,9 +15,9 @@ wall_thickness = 0.7
top_bottom_thickness = 0.75
infill_sparse_density = 18
speed_print = 60
speed_layer_0 = =round(speed_print * 30 / 60)
speed_wall = 50
speed_topbottom = 30
speed_travel = 150
speed_layer_0 = 30
cool_min_layer_time = 5
cool_min_speed = 10

View file

@ -15,6 +15,7 @@ wall_thickness = 1.05
top_bottom_thickness = 0.72
infill_sparse_density = 22
speed_print = 50
speed_layer_0 = =round(speed_print * 30 / 50)
speed_topbottom = 20
cool_min_layer_time = 5
cool_min_speed = 10

View file

@ -15,6 +15,7 @@ wall_thickness = 1.05
top_bottom_thickness = 0.8
infill_sparse_density = 20
speed_print = 50
speed_layer_0 = =round(speed_print * 30 / 50)
speed_topbottom = 20
cool_min_layer_time = 5
cool_min_speed = 10

View file

@ -15,6 +15,7 @@ wall_thickness = 1.59
top_bottom_thickness = 1.2
infill_sparse_density = 20
speed_print = 55
speed_layer_0 = =round(speed_print * 30 / 55)
speed_wall = 40
speed_wall_0 = 25
speed_topbottom = 20

View file

@ -15,6 +15,7 @@ wall_thickness = 2.1
top_bottom_thickness = 1.2
infill_sparse_density = 20
speed_print = 40
speed_layer_0 = =round(speed_print * 30 / 40)
speed_wall_0 = 25
cool_min_layer_time = 5
cool_min_speed = 10

View file

@ -15,6 +15,7 @@ wall_thickness = 0.88
top_bottom_thickness = 0.72
infill_sparse_density = 22
speed_print = 30
speed_layer_0 = =round(speed_print * 30 / 30)
cool_min_layer_time = 3
cool_fan_speed_min = 20
cool_min_speed = 10

View file

@ -15,10 +15,10 @@ wall_thickness = 0.7
top_bottom_thickness = 0.75
infill_sparse_density = 18
speed_print = 55
speed_layer_0 = =round(speed_print * 30 / 55)
speed_wall = 40
speed_topbottom = 30
speed_travel = 150
speed_layer_0 = 30
cool_min_layer_time = 3
cool_fan_speed_min = 20
cool_min_speed = 10

View file

@ -15,6 +15,7 @@ wall_thickness = 1.05
top_bottom_thickness = 0.72
infill_sparse_density = 22
speed_print = 45
speed_layer_0 = =round(speed_print * 30 / 45)
speed_wall = 30
cool_min_layer_time = 3
cool_fan_speed_min = 20

View file

@ -15,6 +15,7 @@ wall_thickness = 1.05
top_bottom_thickness = 0.8
infill_sparse_density = 20
speed_print = 45
speed_layer_0 = =round(speed_print * 30 / 45)
speed_wall = 30
cool_min_layer_time = 3
cool_fan_speed_min = 20

View file

@ -15,6 +15,7 @@ wall_thickness = 1.59
top_bottom_thickness = 1.2
infill_sparse_density = 20
speed_print = 40
speed_layer_0 = =round(speed_print * 30 / 40)
speed_infill = 55
cool_min_layer_time = 3
cool_fan_speed_min = 50

View file

@ -15,6 +15,7 @@ wall_thickness = 2.1
top_bottom_thickness = 1.2
infill_sparse_density = 20
speed_print = 40
speed_layer_0 = =round(speed_print * 30 / 40)
cool_min_layer_time = 3
cool_fan_speed_min = 50
cool_min_speed = 15

View file

@ -15,6 +15,7 @@ wall_thickness = 0.88
top_bottom_thickness = 0.72
infill_sparse_density = 22
speed_print = 30
speed_layer_0 = =round(speed_print * 30 / 30)
cool_min_layer_time = 2
cool_fan_speed_min = 20
cool_min_speed = 15

View file

@ -15,9 +15,9 @@ wall_thickness = 0.7
top_bottom_thickness = 0.75
infill_sparse_density = 18
speed_print = 45
speed_layer_0 = =round(speed_print * 30 / 45)
speed_wall = 40
speed_travel = 150
speed_layer_0 = 30
cool_min_layer_time = 3
cool_fan_speed_min = 80
cool_min_speed = 10

View file

@ -15,6 +15,7 @@ wall_thickness = 1.05
top_bottom_thickness = 0.72
infill_sparse_density = 22
speed_print = 45
speed_layer_0 = =round(speed_print * 30 / 45)
speed_wall = 30
cool_min_layer_time = 2
cool_fan_speed_min = 80

View file

@ -15,6 +15,7 @@ wall_thickness = 1.05
top_bottom_thickness = 0.8
infill_sparse_density = 20
speed_print = 45
speed_layer_0 = =round(speed_print * 30 / 45)
speed_wall = 30
cool_min_layer_time = 3
cool_fan_speed_min = 80

View file

@ -15,6 +15,7 @@ wall_thickness = 1.59
top_bottom_thickness = 1.2
infill_sparse_density = 20
speed_print = 40
speed_layer_0 = =round(speed_print * 30 / 40)
cool_min_layer_time = 5
cool_fan_speed_min = 80
cool_min_speed = 8

View file

@ -15,6 +15,7 @@ wall_thickness = 2.1
top_bottom_thickness = 1.2
infill_sparse_density = 20
speed_print = 40
speed_layer_0 = =round(speed_print * 30 / 40)
cool_min_layer_time = 3
cool_fan_speed_min = 80
cool_min_speed = 8

View file

@ -20,7 +20,6 @@ raft_surface_thickness = 0.2
raft_surface_line_width = 0.57
raft_interface_line_spacing = 1.4
raft_margin = 15
speed_layer_0 = 30
raft_airgap = 0.37
infill_overlap = 5
layer_height = 0.3
@ -40,6 +39,7 @@ line_width = 0.57
layer_0_z_overlap = 0.22
raft_base_line_width = 1.2
speed_print = 25
speed_layer_0 = =round(speed_print * 30 / 50)
support_line_distance = 2.85
support_angle = 45
cool_min_layer_time = 3

View file

@ -33,6 +33,7 @@ infill_sparse_density = 40
layer_0_z_overlap = 0.22
raft_base_line_width = 1.6
speed_print = 25
speed_layer_0 = =round(speed_print * 30 / 25)
speed_wall_0 = 20
support_angle = 45
cool_min_layer_time = 3

View file

@ -33,6 +33,7 @@ infill_sparse_density = 40
layer_0_z_overlap = 0.22
raft_base_line_width = 1.6
speed_print = 30
speed_layer_0 = =round(speed_print * 30 / 30)
speed_wall_0 = 20
support_angle = 45
cool_min_layer_time = 3

View file

@ -20,7 +20,6 @@ support_top_distance = 0.5
raft_surface_thickness = 0.2
wall_thickness = 2.4
raft_margin = 15
speed_layer_0 = 30
raft_airgap = 0.44
infill_overlap = 5
layer_height = 0.2
@ -41,6 +40,7 @@ infill_sparse_density = 40
layer_0_z_overlap = 0.25
raft_base_line_width = 1.6
speed_print = 55
speed_layer_0 = =round(speed_print * 30 / 55)
support_angle = 45
raft_interface_line_spacing = 1.8

View file

@ -31,6 +31,7 @@ infill_sparse_density = 25
layer_0_z_overlap = 0.22
cool_min_layer_time = 2
speed_print = 30
speed_layer_0 = =round(speed_print * 30 / 30)
raft_base_line_spacing = 1
raft_base_line_width = 0.5

View file

@ -31,6 +31,7 @@ infill_sparse_density = 25
layer_0_z_overlap = 0.22
cool_min_layer_time = 2
speed_print = 30
speed_layer_0 = =round(speed_print * 30 / 30)
raft_base_line_spacing = 1
raft_base_line_width = 0.5

View file

@ -31,6 +31,7 @@ infill_sparse_density = 30
layer_0_z_overlap = 0.22
cool_min_layer_time = 3
speed_print = 45
speed_layer_0 = =round(speed_print * 30 / 45)
support_angle = 45
raft_base_line_spacing = 1.6
raft_base_line_width = 0.8

View file

@ -31,6 +31,7 @@ infill_sparse_density = 30
layer_0_z_overlap = 0.22
cool_min_layer_time = 3
speed_print = 45
speed_layer_0 = =round(speed_print * 30 / 45)
support_angle = 45
raft_base_line_spacing = 1.6
raft_base_line_width = 0.8

View file

@ -32,6 +32,7 @@ infill_sparse_density = 40
layer_0_z_overlap = 0.22
raft_base_line_width = 1.6
speed_print = 40
speed_layer_0 = =round(speed_print * 30 / 40)
support_angle = 45
cool_min_layer_time = 3

View file

@ -32,6 +32,7 @@ infill_sparse_density = 40
layer_0_z_overlap = 0.22
raft_base_line_width = 1.6
speed_print = 40
speed_layer_0 = =round(speed_print * 30 / 40)
support_angle = 45
cool_min_layer_time = 3

View file

@ -16,6 +16,7 @@ material_print_temperature = 240
prime_tower_size = 16
skin_overlap = 20
speed_print = 60
speed_layer_0 = =round(speed_print * 30 / 60)
speed_topbottom = =math.ceil(speed_print * 35 / 60)
speed_wall = =math.ceil(speed_print * 45 / 60)
speed_wall_0 = =math.ceil(speed_wall * 35 / 45)

View file

@ -17,6 +17,7 @@ material_print_temperature = 235
material_standby_temperature = 100
prime_tower_size = 16
speed_print = 60
speed_layer_0 = =round(speed_print * 30 / 60)
speed_topbottom = =math.ceil(speed_print * 30 / 60)
speed_wall = =math.ceil(speed_print * 40 / 60)
speed_wall_0 = =math.ceil(speed_wall * 30 / 40)

View file

@ -16,6 +16,7 @@ machine_nozzle_heat_up_speed = 1.5
material_standby_temperature = 100
prime_tower_size = 16
speed_print = 50
speed_layer_0 = =round(speed_print * 30 / 50)
speed_topbottom = =math.ceil(speed_print * 30 / 50)
speed_wall = =math.ceil(speed_print * 30 / 50)

View file

@ -16,6 +16,7 @@ material_print_temperature = 230
material_standby_temperature = 100
prime_tower_size = 16
speed_print = 55
speed_layer_0 = =round(speed_print * 30 / 55)
speed_topbottom = =math.ceil(speed_print * 30 / 55)
speed_wall = =math.ceil(speed_print * 30 / 55)

View file

@ -15,6 +15,7 @@ material_standby_temperature = 100
prime_tower_size = 17
skin_overlap = 20
speed_print = 60
speed_layer_0 = =round(speed_print * 30 / 60)
speed_topbottom = =math.ceil(speed_print * 35 / 60)
speed_wall = =math.ceil(speed_print * 45 / 60)
speed_wall_0 = =math.ceil(speed_wall * 35 / 45)

View file

@ -15,6 +15,7 @@ material_print_temperature = 245
material_standby_temperature = 100
prime_tower_size = 17
speed_print = 60
speed_layer_0 = =round(speed_print * 30 / 60)
speed_topbottom = =math.ceil(speed_print * 30 / 60)
speed_wall = =math.ceil(speed_print * 40 / 60)
speed_wall_0 = =math.ceil(speed_wall * 30 / 40)

View file

@ -16,6 +16,7 @@ machine_nozzle_heat_up_speed = 1.5
material_standby_temperature = 100
prime_tower_size = 17
speed_print = 50
speed_layer_0 = =round(speed_print * 30 / 50)
speed_topbottom = =math.ceil(speed_print * 30 / 50)
speed_wall = =math.ceil(speed_print * 30 / 50)

View file

@ -16,6 +16,7 @@ material_print_temperature = 240
material_standby_temperature = 100
prime_tower_size = 17
speed_print = 55
speed_layer_0 = =round(speed_print * 30 / 55)
speed_topbottom = =math.ceil(speed_print * 30 / 55)
speed_wall = =math.ceil(speed_print * 30 / 55)

View file

@ -17,6 +17,7 @@ machine_nozzle_heat_up_speed = 1.6
material_standby_temperature = 100
prime_tower_enable = False
speed_print = 80
speed_layer_0 = =round(speed_print * 30 / 80)
speed_topbottom = =math.ceil(speed_print * 30 / 80)
speed_wall = =math.ceil(speed_print * 40 / 80)
speed_wall_0 = =math.ceil(speed_wall * 30 / 40)

View file

@ -19,6 +19,7 @@ material_print_temperature = 195
material_standby_temperature = 100
skin_overlap = 10
speed_print = 60
speed_layer_0 = =round(speed_print * 30 / 60)
speed_topbottom = =math.ceil(speed_print * 30 / 60)
speed_wall = =math.ceil(speed_print * 30 / 60)
top_bottom_thickness = 1

View file

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 30 30">
<path
d="m 20.769231,12.692308 q 0,-3.3353368 -2.370794,-5.7061301 -2.370793,-2.3707933 -5.706129,-2.3707933 -3.335337,0 -5.70613,2.3707933 -2.370793,2.3707933 -2.370793,5.7061301 0,3.335336 2.370793,5.706129 2.370793,2.370794 5.70613,2.370794 3.335336,0 5.706129,-2.370794 2.370794,-2.370793 2.370794,-5.706129 z m 9.230769,15 q 0,0.9375 -0.685096,1.622596 Q 28.629808,30 27.692308,30 26.71875,30 26.069712,29.314904 l -6.183895,-6.165866 q -3.227163,2.235577 -7.193509,2.235577 -2.578125,0 -4.93089,-1.000601 Q 5.408654,23.383413 3.704928,21.679687 2.001202,19.975962 1.000601,17.623197 0,15.270433 0,12.692308 0,10.114183 1.000601,7.7614183 2.001202,5.4086538 3.704928,3.7049279 5.408654,2.0012019 7.761418,1.000601 10.114183,0 12.692308,0 q 2.578125,0 4.930889,1.000601 2.352765,1.0006009 4.05649,2.7043269 1.703726,1.7037259 2.704327,4.0564904 1.000601,2.3527647 1.000601,4.9308897 0,3.966346 -2.235577,7.193509 l 6.183895,6.183895 Q 30,26.736779 30,27.692308 Z" />
</svg>

After

Width:  |  Height:  |  Size: 1 KiB

View file

@ -12,15 +12,12 @@ brim_width = 7
machine_nozzle_cool_down_speed = 0.9
raft_acceleration = =acceleration_print
raft_airgap = 0.3
raft_base_speed = =0.75 * raft_speed
raft_base_thickness = =resolveOrValue('layer_height_0') * 1.2
raft_interface_line_spacing = =raft_interface_line_width + 0.2
raft_interface_line_width = =line_width * 2
raft_interface_speed = =raft_speed * 0.75
raft_interface_thickness = =layer_height * 1.5
raft_jerk = =jerk_print
raft_margin = 15
raft_speed = =speed_print / 60 * 30
raft_surface_layers = 2
retraction_amount = 6.5
retraction_count_max = 25

View file

@ -12,9 +12,12 @@ cool_fan_speed_max = =cool_fan_speed
machine_nozzle_heat_up_speed = 1.5
material_print_temperature = 215
retraction_extrusion_window = =retraction_amount
speed_layer_0 = 20
speed_wall_0 = =math.ceil(speed_wall * 25 / 30)
support_bottom_height = =layer_height * 2
support_bottom_stair_step_height = =layer_height
raft_interface_speed = 20
raft_base_speed = 20
support_infill_rate = 25
support_interface_enable = True
support_join_distance = 3
@ -22,4 +25,5 @@ support_line_width = =round(line_width * 0.4 / 0.35, 2)
support_offset = 3
support_xy_distance = =wall_line_width_0 * 3
support_xy_distance_overhang = =wall_line_width_0 / 2
raft_speed = 25

View file

@ -27,6 +27,7 @@ retraction_min_travel = 1.5
retraction_prime_speed = 25
skin_overlap = 15
speed_print = 70
speed_layer_0 = =round(speed_print * 30 / 70)
speed_topbottom = =math.ceil(speed_print * 30 / 70)
speed_wall = =math.ceil(speed_print * 30 / 70)
support_angle = 60