Merge branch 'master' into feature_xmlmaterials_cura_settings

# Conflicts:
#	plugins/XmlMaterialProfile/XmlMaterialProfile.py
This commit is contained in:
fieldOfView 2017-12-18 12:47:57 +01:00
commit 266be74569
312 changed files with 46856 additions and 17766 deletions

View file

@ -107,24 +107,17 @@ class ThreeMFReader(MeshReader):
um_node.addDecorator(SettingOverrideDecorator())
global_container_stack = Application.getInstance().getGlobalContainerStack()
# Ensure the correct next container for the SettingOverride decorator is set.
if global_container_stack:
multi_extrusion = global_container_stack.getProperty("machine_extruder_count", "value") > 1
default_stack = ExtruderManager.getInstance().getExtruderStack(0)
# Ensure that all extruder data is reset
if not multi_extrusion:
default_stack_id = global_container_stack.getId()
else:
default_stack = ExtruderManager.getInstance().getExtruderStack(0)
if default_stack:
default_stack_id = default_stack.getId()
else:
default_stack_id = global_container_stack.getId()
um_node.callDecoration("setActiveExtruder", default_stack_id)
if default_stack:
um_node.callDecoration("setActiveExtruder", default_stack.getId())
# Get the definition & set it
definition = QualityManager.getInstance().getParentMachineDefinition(global_container_stack.getBottom())
um_node.callDecoration("getStack").getTop().setDefinition(definition)
um_node.callDecoration("getStack").getTop().setDefinition(definition.getId())
setting_container = um_node.callDecoration("getStack").getTop()
@ -139,7 +132,7 @@ class ThreeMFReader(MeshReader):
else:
Logger.log("w", "Unable to find extruder in position %s", setting_value)
continue
setting_container.setProperty(key,"value", setting_value)
setting_container.setProperty(key, "value", setting_value)
if len(um_node.getChildren()) > 0:
group_decorator = GroupDecorator()

View file

@ -13,6 +13,7 @@ from UM.Settings.ContainerRegistry import ContainerRegistry
from UM.MimeTypeDatabase import MimeTypeDatabase
from UM.Job import Job
from UM.Preferences import Preferences
from UM.Util import parseBool
from .WorkspaceDialog import WorkspaceDialog
import xml.etree.ElementTree as ET
@ -121,7 +122,6 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
Logger.log("w", "Could not find reader that was able to read the scene data for 3MF workspace")
return WorkspaceReader.PreReadResult.failed
machine_name = ""
machine_type = ""
variant_type_name = i18n_catalog.i18nc("@label", "Nozzle")
@ -132,9 +132,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
# A few lists of containers in this project files.
# When loading the global stack file, it may be associated with those containers, which may or may not be
# in Cura already, so we need to provide them as alternative search lists.
definition_container_list = []
instance_container_list = []
material_container_list = []
resolve_strategy_keys = ["machine", "material", "quality_changes"]
self._resolve_strategies = {k: None for k in resolve_strategy_keys}
@ -148,20 +146,20 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
definition_container_files = [name for name in cura_file_names if name.endswith(self._definition_container_suffix)]
for each_definition_container_file in definition_container_files:
container_id = self._stripFileToId(each_definition_container_file)
definitions = self._container_registry.findDefinitionContainers(id=container_id)
definitions = self._container_registry.findDefinitionContainersMetadata(id = container_id)
if not definitions:
definition_container = DefinitionContainer(container_id)
definition_container.deserialize(archive.open(each_definition_container_file).read().decode("utf-8"))
definition_container.deserialize(archive.open(each_definition_container_file).read().decode("utf-8"), file_name = each_definition_container_file)
definition_container = definition_container.getMetaData()
else:
definition_container = definitions[0]
definition_container_list.append(definition_container)
definition_container_type = definition_container.getMetaDataEntry("type")
definition_container_type = definition_container.get("type")
if definition_container_type == "machine":
machine_type = definition_container.getName()
variant_type_name = definition_container.getMetaDataEntry("variants_name", variant_type_name)
machine_type = definition_container["name"]
variant_type_name = definition_container.get("variants_name", variant_type_name)
machine_definition_container_count += 1
elif definition_container_type == "extruder":
@ -185,11 +183,10 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
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)
material_labels.append(self._getMaterialLabelFromSerialized(archive.open(material_container_file).read().decode("utf-8")))
if materials:
if self._container_registry.findContainersMetadata(id = container_id): #This material already exists.
containers_found_dict["material"] = True
if not materials[0].isReadOnly(): # Only non readonly materials can be in conflict
if not self._container_registry.isReadOnly(container_id): # Only non readonly materials can be in conflict
material_conflict = True
Job.yieldThread()
@ -208,7 +205,8 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
instance_container = InstanceContainer(container_id)
# Deserialize InstanceContainer by converting read data from bytes to string
instance_container.deserialize(archive.open(each_instance_container_file).read().decode("utf-8"))
instance_container.deserialize(archive.open(each_instance_container_file).read().decode("utf-8"),
file_name = each_instance_container_file)
instance_container_list.append(instance_container)
container_type = instance_container.getMetaDataEntry("type")
@ -378,7 +376,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
return WorkspaceReader.PreReadResult.accepted
## Overrides an ExtruderStack in the given GlobalStack and returns the new ExtruderStack.
def _overrideExtruderStack(self, global_stack, extruder_file_content):
def _overrideExtruderStack(self, global_stack, extruder_file_content, extruder_stack_file):
# Get extruder position first
extruder_config = configparser.ConfigParser()
extruder_config.read_string(extruder_file_content)
@ -394,7 +392,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
return None
# Override the given extruder stack
extruder_stack.deserialize(extruder_file_content)
extruder_stack.deserialize(extruder_file_content, file_name = extruder_stack_file)
# return the new ExtruderStack
return extruder_stack
@ -446,6 +444,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
extruder_stacks = []
extruder_stacks_added = []
container_stacks_added = []
machine_extruder_count = None
containers_added = []
@ -458,7 +457,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
extruder_stack_id_map = {} # new and old ExtruderStack IDs map
if self._resolve_strategies["machine"] == "new":
# We need a new id if the id already exists
if self._container_registry.findContainerStacks(id = global_stack_id_original):
if self._container_registry.findContainerStacksMetadata(id = global_stack_id_original):
global_stack_id_new = self.getNewId(global_stack_id_original)
global_stack_need_rename = True
@ -467,7 +466,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
for each_extruder_stack_file in extruder_stack_files:
old_container_id = self._stripFileToId(each_extruder_stack_file)
new_container_id = old_container_id
if self._container_registry.findContainerStacks(id = old_container_id):
if self._container_registry.findContainerStacksMetadata(id = old_container_id):
# get a new name for this extruder
new_container_id = self.getNewId(old_container_id)
@ -481,10 +480,11 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
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)
definitions = self._container_registry.findDefinitionContainersMetadata(id = container_id)
if not definitions:
definition_container = DefinitionContainer(container_id)
definition_container.deserialize(archive.open(definition_container_file).read().decode("utf-8"))
definition_container.deserialize(archive.open(definition_container_file).read().decode("utf-8"),
file_name = definition_container_file)
self._container_registry.addContainer(definition_container)
Job.yieldThread()
@ -502,18 +502,21 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
if not materials:
material_container = xml_material_profile(container_id)
material_container.deserialize(archive.open(material_container_file).read().decode("utf-8"))
material_container.deserialize(archive.open(material_container_file).read().decode("utf-8"),
file_name = material_container_file)
containers_to_add.append(material_container)
else:
material_container = materials[0]
if not material_container.isReadOnly(): # Only create new materials if they are not read only.
if not self._container_registry.isReadOnly(container_id): # Only create new materials if they are not read only.
if self._resolve_strategies["material"] == "override":
material_container.deserialize(archive.open(material_container_file).read().decode("utf-8"))
material_container.deserialize(archive.open(material_container_file).read().decode("utf-8"),
file_name = material_container_file)
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"))
material_container.deserialize(archive.open(material_container_file).read().decode("utf-8"),
file_name = material_container_file)
containers_to_add.append(material_container)
material_containers.append(material_container)
@ -540,7 +543,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
instance_container = InstanceContainer(container_id)
# Deserialize InstanceContainer by converting read data from bytes to string
instance_container.deserialize(serialized)
instance_container.deserialize(serialized, file_name = instance_container_file)
container_type = instance_container.getMetaDataEntry("type")
Job.yieldThread()
@ -562,7 +565,8 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
else:
if self._resolve_strategies["machine"] == "override" or self._resolve_strategies["machine"] is None:
instance_container = user_containers[0]
instance_container.deserialize(archive.open(instance_container_file).read().decode("utf-8"))
instance_container.deserialize(archive.open(instance_container_file).read().decode("utf-8"),
file_name = instance_container_file)
instance_container.setDirty(True)
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.
@ -570,7 +574,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
if old_extruder_id:
new_extruder_id = extruder_stack_id_map[old_extruder_id]
new_id = new_extruder_id + "_current_settings"
instance_container._id = new_id
instance_container.setMetaDataEntry("id", new_id)
instance_container.setName(new_id)
instance_container.setMetaDataEntry("extruder", new_extruder_id)
containers_to_add.append(instance_container)
@ -579,7 +583,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
if machine_id:
new_machine_id = self.getNewId(machine_id)
new_id = new_machine_id + "_current_settings"
instance_container._id = new_id
instance_container.setMetadataEntry("id", new_id)
instance_container.setName(new_id)
instance_container.setMetaDataEntry("machine", new_machine_id)
containers_to_add.append(instance_container)
@ -595,7 +599,8 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
# selected strategy.
if self._resolve_strategies[container_type] == "override":
instance_container = changes_containers[0]
instance_container.deserialize(archive.open(instance_container_file).read().decode("utf-8"))
instance_container.deserialize(archive.open(instance_container_file).read().decode("utf-8"),
file_name = instance_container_file)
instance_container.setDirty(True)
elif self._resolve_strategies[container_type] == "new":
@ -627,8 +632,14 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
# The ID already exists, but nothing in the values changed, so do nothing.
pass
quality_and_definition_changes_instance_containers.append(instance_container)
if container_type == "definition_changes":
definition_changes_extruder_count = instance_container.getProperty("machine_extruder_count", "value")
if definition_changes_extruder_count is not None:
machine_extruder_count = definition_changes_extruder_count
else:
existing_container = self._container_registry.findInstanceContainers(id = container_id)
existing_container = self._container_registry.findInstanceContainersMetadata(id = container_id)
if not existing_container:
containers_to_add.append(instance_container)
if global_stack_need_rename:
@ -644,28 +655,30 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
# Get the stack(s) saved in the workspace.
Logger.log("d", "Workspace loading is checking stacks containers...")
# --
# load global stack file
try:
stack = None
if self._resolve_strategies["machine"] == "override":
container_stacks = self._container_registry.findContainerStacks(id = global_stack_id_original)
stack = container_stacks[0]
# HACK
# There is a machine, check if it has authentication data. If so, keep that data.
network_authentication_id = container_stacks[0].getMetaDataEntry("network_authentication_id")
network_authentication_key = container_stacks[0].getMetaDataEntry("network_authentication_key")
container_stacks[0].deserialize(archive.open(global_stack_file).read().decode("utf-8"))
network_authentication_id = stack.getMetaDataEntry("network_authentication_id")
network_authentication_key = stack.getMetaDataEntry("network_authentication_key")
stack.deserialize(archive.open(global_stack_file).read().decode("utf-8"), file_name = global_stack_file)
if network_authentication_id:
container_stacks[0].addMetaDataEntry("network_authentication_id", network_authentication_id)
stack.addMetaDataEntry("network_authentication_id", network_authentication_id)
if network_authentication_key:
container_stacks[0].addMetaDataEntry("network_authentication_key", network_authentication_key)
stack.addMetaDataEntry("network_authentication_key", network_authentication_key)
elif self._resolve_strategies["machine"] == "new":
# create a new global stack
stack = GlobalStack(global_stack_id_new)
# Deserialize stack by converting read data from bytes to string
stack.deserialize(archive.open(global_stack_file).read().decode("utf-8"))
stack.deserialize(archive.open(global_stack_file).read().decode("utf-8"),
file_name = global_stack_file)
# Ensure a unique ID and name
stack._id = global_stack_id_new
@ -682,12 +695,11 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
self._container_registry.addContainer(stack)
containers_added.append(stack)
else:
Logger.log("e", "Resolve strategy of %s for machine is not supported",
self._resolve_strategies["machine"])
Logger.log("e", "Resolve strategy of %s for machine is not supported", self._resolve_strategies["machine"])
# Create a new definition_changes container if it was empty
if stack.definitionChanges == self._container_registry.getEmptyInstanceContainer():
stack.setDefinitionChanges(CuraStackBuilder.createDefinitionChangesContainer(stack, stack._id + "_settings"))
stack.setDefinitionChanges(CuraStackBuilder.createDefinitionChangesContainer(stack, stack.getId() + "_settings"))
global_stack = stack
Job.yieldThread()
except:
@ -697,16 +709,6 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
self._container_registry.removeContainer(container.getId())
return
#
# Use the number of extruders from the global stack instead of the number of extruder stacks this project file
# contains. The Custom FDM Printer can have multiple extruders, but the actual number of extruders in used is
# defined in the global stack.
# Because for single-extrusion machines, there won't be an extruder stack, so relying on the the extruder count
# in the global stack can avoid problems in those cases.
#
extruder_count_from_global_stack = global_stack.getProperty("machine_extruder_count", "value")
# --
# load extruder stack files
try:
for extruder_stack_file in extruder_stack_files:
@ -714,11 +716,10 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
extruder_file_content = archive.open(extruder_stack_file, "r").read().decode("utf-8")
if self._resolve_strategies["machine"] == "override":
if global_stack.getProperty("machine_extruder_count", "value") > 1:
# deserialize new extruder stack over the current ones (if any)
stack = self._overrideExtruderStack(global_stack, extruder_file_content)
if stack is None:
continue
# deserialize new extruder stack over the current ones (if any)
stack = self._overrideExtruderStack(global_stack, extruder_file_content, extruder_stack_file)
if stack is None:
continue
elif self._resolve_strategies["machine"] == "new":
new_id = extruder_stack_id_map[container_id]
@ -736,7 +737,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
extruder_config.write(tmp_string_io)
extruder_file_content = tmp_string_io.getvalue()
stack.deserialize(extruder_file_content)
stack.deserialize(extruder_file_content, file_name = extruder_stack_file)
# Ensure a unique ID and name
stack._id = new_id
@ -749,9 +750,26 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
# Create a new definition_changes container if it was empty
if stack.definitionChanges == self._container_registry.getEmptyInstanceContainer():
stack.setDefinitionChanges(CuraStackBuilder.createDefinitionChangesContainer(stack, stack._id + "_settings"))
if global_stack.getProperty("machine_extruder_count", "value") > 1:
stack.setDefinitionChanges(CuraStackBuilder.createDefinitionChangesContainer(stack, stack.getId() + "_settings"))
if stack.getMetaDataEntry("type") == "extruder_train":
extruder_stacks.append(stack)
# If not extruder stacks were saved in the project file (pre 3.1) create one manually
# We re-use the container registry's addExtruderStackForSingleExtrusionMachine method for this
if not extruder_stacks:
stack = self._container_registry.addExtruderStackForSingleExtrusionMachine(global_stack, "fdmextruder")
if stack:
if self._resolve_strategies["machine"] == "override":
# in case the extruder is newly created (for a single-extrusion machine), we need to override
# the existing extruder stack.
existing_extruder_stack = global_stack.extruders[stack.getMetaDataEntry("position")]
for idx in range(len(_ContainerIndexes.IndexTypeMap)):
existing_extruder_stack.replaceContainer(idx, stack._containers[idx], postpone_emit = True)
extruder_stacks.append(existing_extruder_stack)
else:
extruder_stacks.append(stack)
except:
Logger.logException("w", "We failed to serialize the stack. Trying to clean up.")
# Something went really wrong. Try to remove any data that we added.
@ -759,6 +777,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
self._container_registry.removeContainer(container.getId())
return
# Check quality profiles to make sure that if one stack has the "not supported" quality profile,
# all others should have the same.
#
@ -773,18 +792,102 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
#
has_not_supported = False
for stack in [global_stack] + extruder_stacks:
if stack.quality.getId() == "empty_quality":
if stack.quality.getId() in ("empty", "empty_quality"):
has_not_supported = True
break
# We filter out extruder stacks that are not actually used, for example the UM3 and custom FDM printer extruder count setting.
extruder_stacks_in_use = extruder_stacks
if machine_extruder_count is not None:
extruder_stacks_in_use = extruder_stacks[:machine_extruder_count]
available_quality = QualityManager.getInstance().findAllUsableQualitiesForMachineAndExtruders(global_stack,
extruder_stacks_in_use)
if not has_not_supported:
available_quality = QualityManager.getInstance().findAllUsableQualitiesForMachineAndExtruders(global_stack, extruder_stacks)
has_not_supported = not available_quality
quality_has_been_changed = False
if has_not_supported:
empty_quality_container = self._container_registry.findInstanceContainers(id = "empty_quality")[0]
for stack in [global_stack] + extruder_stacks:
for stack in [global_stack] + extruder_stacks_in_use:
stack.replaceContainer(_ContainerIndexes.Quality, empty_quality_container)
empty_quality_changes_container = self._container_registry.findInstanceContainers(id = "empty_quality_changes")[0]
for stack in [global_stack] + extruder_stacks_in_use:
stack.replaceContainer(_ContainerIndexes.QualityChanges, empty_quality_changes_container)
quality_has_been_changed = True
else:
empty_quality_changes_container = self._container_registry.findInstanceContainers(id="empty_quality_changes")[0]
# The machine in the project has non-empty quality and there are usable qualities for this machine.
# We need to check if the current quality_type is still usable for this machine, if not, then the quality
# will be reset to the "preferred quality" if present, otherwise "normal".
available_quality_types = [q.getMetaDataEntry("quality_type") for q in available_quality]
if global_stack.quality.getMetaDataEntry("quality_type") not in available_quality_types:
# We are here because the quality_type specified in the project is not supported any more,
# so we need to switch it to the "preferred quality" if present, otherwise "normal".
quality_has_been_changed = True
# find the preferred quality
preferred_quality_id = global_stack.getMetaDataEntry("preferred_quality", None)
if preferred_quality_id is not None:
definition_id = global_stack.definition.getId()
definition_id = global_stack.definition.getMetaDataEntry("quality_definition", definition_id)
if not parseBool(global_stack.getMetaDataEntry("has_machine_quality", "False")):
definition_id = "fdmprinter"
containers = self._container_registry.findInstanceContainers(id = preferred_quality_id,
type = "quality",
definition = definition_id)
containers = [c for c in containers if not c.getMetaDataEntry("material", "")]
if containers:
global_stack.quality = containers[0]
global_stack.qualityChanges = empty_quality_changes_container
# also find the quality containers for the extruders
for extruder_stack in extruder_stacks_in_use:
search_criteria = {"id": preferred_quality_id,
"type": "quality",
"definition": definition_id}
if global_stack.getMetaDataEntry("has_machine_materials") and extruder_stack.material.getId() not in ("empty", "empty_material"):
search_criteria["material"] = extruder_stack.material.getId()
containers = self._container_registry.findInstanceContainers(**search_criteria)
if containers:
extruder_stack.quality = containers[0]
extruder_stack.qualityChanges = empty_quality_changes_container
else:
Logger.log("e", "Cannot find preferred quality for extruder [%s].", extruder_stack.getId())
else:
# we cannot find the preferred quality. THIS SHOULD NOT HAPPEN
Logger.log("e", "Cannot find the preferred quality for machine [%s]", global_stack.getId())
else:
# The quality_type specified in the project file is usable, but the quality container itself may not
# be correct. For example, for UM2, the quality container can be "draft" while it should be "um2_draft"
# instead.
# In this code branch, we try to fix those incorrect quality containers.
#
# ***IMPORTANT***: We only do this fix for single-extrusion machines.
# We will first find the correct quality profile for the extruder, then apply the same
# quality profile for the global stack.
#
if len(extruder_stacks) == 1:
extruder_stack = extruder_stacks[0]
search_criteria = {"type": "quality", "quality_type": global_stack.quality.getMetaDataEntry("quality_type")}
search_criteria["definition"] = global_stack.definition.getId()
if not parseBool(global_stack.getMetaDataEntry("has_machine_quality", "False")):
search_criteria["definition"] = "fdmprinter"
if global_stack.getMetaDataEntry("has_machine_materials") and extruder_stack.material.getId() not in ("empty", "empty_material"):
search_criteria["material"] = extruder_stack.material.getId()
containers = self._container_registry.findInstanceContainers(**search_criteria)
if containers:
new_quality_container = containers[0]
extruder_stack.quality = new_quality_container
global_stack.quality = new_quality_container
#
# Replacing the old containers if resolve is "new".
# When resolve is "new", some containers will get renamed, so all the other containers that reference to those
# MUST get updated too.
@ -807,7 +910,11 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
global_stack.userChanges = container
continue
for changes_container_type in ("quality_changes", "definition_changes"):
changes_container_types = ("quality_changes", "definition_changes")
if quality_has_been_changed:
# DO NOT replace quality_changes if the current quality_type is not supported
changes_container_types = ("definition_changes",)
for changes_container_type in changes_container_types:
if self._resolve_strategies[changes_container_type] == "new":
# Quality changes needs to get a new ID, added to registry and to the right stacks
for each_changes_container in quality_and_definition_changes_instance_containers:
@ -907,7 +1014,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
base_file_name = os.path.basename(file_name)
if base_file_name.endswith(".curaproject.3mf"):
base_file_name = base_file_name[:base_file_name.rfind(".curaproject.3mf")]
Application.getInstance().projectFileLoaded.emit(base_file_name)
self.setWorkspaceName(base_file_name)
return nodes
## HACK: Replaces the material container in the given stack with a newly created material container.

View file

@ -1,12 +1,10 @@
# Copyright (c) 2016 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from PyQt5.QtCore import QUrl, pyqtSignal, QObject, pyqtProperty, QCoreApplication
from PyQt5.QtCore import pyqtSignal, QObject, pyqtProperty, QCoreApplication
from UM.FlameProfiler import pyqtSlot
from PyQt5.QtQml import QQmlComponent, QQmlContext
from UM.PluginRegistry import PluginRegistry
from UM.Application import Application
from UM.Logger import Logger
from UM.i18n import i18nCatalog
from UM.Settings.ContainerRegistry import ContainerRegistry
@ -256,14 +254,8 @@ class WorkspaceDialog(QObject):
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())
path = os.path.join(PluginRegistry.getInstance().getPluginPath("3MFReader"), self._qml_url)
self._view = Application.getInstance().createQmlComponent(path, {"manager": self})
def show(self):
# Emit signal so the right thread actually shows the view.

View file

@ -59,7 +59,9 @@ class ThreeMFWorkspaceWriter(WorkspaceWriter):
version_file = zipfile.ZipInfo("Cura/version.ini")
version_config_parser = configparser.ConfigParser()
version_config_parser.add_section("versions")
version_config_parser.set("versions", "cura_version", Application.getStaticVersion())
version_config_parser.set("versions", "cura_version", Application.getInstance().getVersion())
version_config_parser.set("versions", "build_type", Application.getInstance().getBuildType())
version_config_parser.set("versions", "is_debug_mode", str(Application.getInstance().getIsDebugMode()))
version_file_string = StringIO()
version_config_parser.write(version_file_string)
@ -95,7 +97,7 @@ class ThreeMFWorkspaceWriter(WorkspaceWriter):
file_in_archive.compress_type = zipfile.ZIP_DEFLATED
# Do not include the network authentication keys
ignore_keys = ["network_authentication_id", "network_authentication_key"]
ignore_keys = {"network_authentication_id", "network_authentication_key"}
serialized_data = container.serialize(ignored_metadata_keys = ignore_keys)
archive.writestr(file_in_archive, serialized_data)

View file

@ -87,7 +87,7 @@ class ThreeMFWriter(MeshWriter):
if stack is not None:
changed_setting_keys = set(stack.getTop().getAllKeys())
# Ensure that we save the extruder used for this object.
# Ensure that we save the extruder used for this object in a multi-extrusion setup
if stack.getProperty("machine_extruder_count", "value") > 1:
changed_setting_keys.add("extruder_nr")

View file

@ -8,8 +8,7 @@ from UM.Application import Application
from UM.PluginRegistry import PluginRegistry
from UM.Version import Version
from PyQt5.QtQml import QQmlComponent, QQmlContext
from PyQt5.QtCore import QUrl, pyqtSlot, QObject
from PyQt5.QtCore import pyqtSlot, QObject
import os.path
import collections
@ -106,9 +105,5 @@ class ChangeLog(Extension, QObject,):
self._changelog_window.hide()
def createChangelogWindow(self):
path = QUrl.fromLocalFile(os.path.join(PluginRegistry.getInstance().getPluginPath(self.getPluginId()), "ChangeLog.qml"))
component = QQmlComponent(Application.getInstance()._engine, path)
self._changelog_context = QQmlContext(Application.getInstance()._engine.rootContext())
self._changelog_context.setContextProperty("manager", self)
self._changelog_window = component.create(self._changelog_context)
path = os.path.join(PluginRegistry.getInstance().getPluginPath(self.getPluginId()), "ChangeLog.qml")
self._changelog_window = Application.getInstance().createQmlComponent(path, {"manager": self})

View file

@ -1,3 +1,72 @@
[3.1.0]
*Profile added for 0.25 mm print core
This new print core gives extra fine line widths which gives prints extra definition and surface quality.
*Profile added for Breakaway material
New material profile for Breakaway material, a new dry post processing support material, which can be used for models with flat surface area overhangs.
*Layer view
The existing Layer View has been updated in order to see a live simulation of all the paths within a layer.
*Quick camera controls
New buttons have been added to the interface that can quickly reposition the camera view of the buildplate.
*Lock model on platform
The move tool has a new option to lock a selected model to the platform.
*Faster profile switching speed
Duplicating and removing a profile could take Ultimaker Cura quite some time, it now happens instantly.
*Faster printer selection
Removing a printer from the library is now instant. No more unresponsive screens.
*Faster processing speed
A 5 - 10 % speed increase when calculating normals, loading models, and slicing.
*Feedrate visualization
Feedrate visualization has been added to the Layer view. Using this gives the user an idea of the print speeds per model part, allowing for better control over prints.
*Jogging
It allows the printhead to be moved with on-screen controls. Contributed by fieldOfView.
*Large model loading
A new feature has been added which unloads the layer view when switching to solid mode, speeding Ultimaker Cura back up without losing your G-code/layer view information.
*Scripts folder
A scripts folder is now available in the Ultimaker Cura configuration folder. This folder can be loaded with post processing plugins scripts, which will automatically show in Ultimaker Cura. Contributed by fieldOfView.
*Optimized workflow for crash reporting
Crash reports are automatically generated and allow the user, in case of a crash, to easily send their report with a description to developers.
*Floating models enabled
In previous releases, models were dropped to the build plate when support was disabled. Models now float when the setting is enabled (even if creates an impossible-to-print situation). This can be used to stack separate models on top of each other.
*Slicing tolerance
A new setting that affects the intersect point to influence the dimensional accuracy for diagonal surfaces. The user can select the behaviour: Inclusive makes gaps narrower, Exclusive makes gaps wider, and Middle is the fastest to process. This can be used to create better tolerances for printed screw holes. Contributed by BagelOrb.
*Optimized zig zag patterns
Zig zag patterns now print more consistently. Lines now have a 5 micron tolerance in which they are printed any way, resulting in longer connected lines. Contributed by smartavionics.
*Aligned z-seam inner wall moves
Inner wall travel moves are aligned with the z-seam. This reduces the number of travel moves and reduces the chance of more unwanted seams.
*Relative positioning of infill patterns
Infill patterns are now positioned relative to the center of loaded models and an offset can be applied to control the infill more precisely and adjust it to preference or strength. Contributed by smartavionics.
*Line resolution
Enables the user to specify the minimum allowed distance value between two points in G-code to create lower or higher resolution polygons.
*Custom mode changes
If profile settings have been modified in recommended mode under custom mode, a reset icon will appear to notify the user. Click the icon to show the changes that have been made, and revert back to the default profile settings.
*Bugfixes
- Fix for layer numbers being displayed incorrectly when switching between solid and layer mode
- Fix for Ultimaker Cura engine crashes on certain models
- Fix for Uninstalling previous versions of Cura on Windows platforms
- Fix for displaying visible settings
- Fix for importing legacy .ini files
- Prevent skipping user agreement dialog by pressing escape
[3.0.4]
*Bug fixes
- Fixed OpenGL issue that prevents Cura from starting.
@ -40,7 +109,7 @@ The build plate now shows graduations of 10 mm and 1 mm for easy model positioni
Extruder tabs have become buttons and icons have been updated.
*Add an "Export to Cura" button in SOLIDWORKS
SOLIDWORKS plugin can now be installed using an automatic installer.
A macro can be added to your SOLIDWORKS installation that loads your model into Ultimaker Cura.
*Siemens NX macro
When a user updates models in Siemens NX and clicks the button, the updated models replace the models opened in Ultimaker Cura.
@ -571,10 +640,10 @@ The new GUI allows custom profiles to load easily and intuitively, directly from
*3MF File Loading Support
Were happy to report we now support loading 3MF files. This is a new file format similar to AMF, but freely available.
*Intuitive Cut-Off Object Bottom
*Intuitive Cut-Off Object Bottom
Weve added a feature that allows you to move objects below the build plate. You can either correct a model with a rough bottom, or print only a part of an object. Please note that the implementation differs greatly from the old one when it was just a setting.
*64-bit Windows Builds
*64-bit Windows Builds
An optimized 64-bit Windows Cura version is now available. This allows you to load larger model files.
*Automatic calculations
@ -639,3 +708,165 @@ Prints the outer walls with a jittering motion to give your object a diffuse fin
*Wire Printing
The object is printed with a mid-air / net-like structure, following the mesh surface. The build plate will move up and down during diagonal segments. Though not visible in layer view, you can view the result in other software, such as Repetier Host or http://chilipeppr.com/tinyg.
[15.06 Beta]
Cura 15.06 is a new release built from the ground up on a completely new
framework called Uranium. This framework has been designed to make it easier to
extend Cura with additional functionality as well as provide a cleaner UI.
[15.05.95]
* Fixed: Selection ghost remains visible after deleting an object
* Fixed: Window does not show up immediately after starting application on OSX
* Fixed: Added display of rotation angle during rotation
* Fixed: Object changes position while rotating/scaling
* Fixed: Loading improvements in the layer view
* Fixed: Added application icons
* Fixed: Improved feedback when loading models
* Fixed: Eject device on MacOSX now provides proper feedback
* Fixed: Make it possible to show retraction settings for UM2
* Fixed: Opening the machine preferences page will switch to the first available machine
* Fixed: Improved tool handle hit area size
* Fixed: Render lines with a thickness based on screen DPI
[15.05.94]
* Added Russian translations
* Fixed: Infill not displayed in layer view
* Fixed: Cannot select/scale/rotate when first activating the tool and then trying to select a model.
* Fixed: Improved font rendering on Windows
* Fixed: Help > Show Documentation crashes Cura on Windows
* Fixed: "There is no disk in the drive" repeating messages on Windows
* Fixed: Retraction settings not visible for Ultimaker2
* Fixed: Display rotation angle when rotating an object
* Fixed: Time/Quality slider values are properly rounded
* Fixed: Improved clarity of buttons and text
* Fixed: No indication that anything is happening when loading a model
* Fixed: Eject device now works on Windows
[15.05.93]
* Fixed: No shortcuts for moving up/down layers in layer view.
* Fixed: Last view layers could not be scrolled through in layer view.
* Fixed: Files provided on command line would not actually show up on the build
platform.
* Fixed: Render a ghost of the selection in Layer view to make the actual object
position clear.
* Fixed: Showing a menu would clear the selection.
* Fixed: Size and scaling factor display for scale tool.
* Fixed: Missing background for additional tool controls.
* Fixed: Loading message times out when loading large files.
* Fixed: Show recent files in the file menu.
* Fixed: Windows installer will now install MSVC 2010 redistributable, to
prevent issues with missing DLL's.
* Fixed: Collapsed/expanded state of setting categories not stored.
[15.05.91]
* There is now a working MacOSX version. Currently it supports OSX 10.7 and
higher.
* Fixed: Need to deselect before selecting a different object.
* Fixed: Object can be moved on Z axis.
* Fixed: Error values should be considered invalid values and will not trigger a
slice.
* Fixed: Text fields used a locale-aware validator while the underlying code did
not.
* Fixed: Text fields will trigger a slice on text change, not only after focus
change/enter press.
* Fixed: Rotate Tool snaps to incorrect value.
* Fixed: Object Collision would only moved objects to the right.
* Fixed: Object Collision would move the selected object when it should not.
* Fixed: Camera panning now works correctly instead of doing nothing.
* Fixed: Camera would flip around center point at maximum rotation.
* Fixed: Build platform grid blocked view from below objects.
* Fixed: Viewport on MacOSX with high-DPI screens was only taking 1/4th of the
window
[15.05.90]
* Fixed: Additional UI elements for tools and views not loading.
* Fixed: Double click needed to change setting dialog page.
* Fixed: Context menu entries (reload, center object, etc.) not working.
* Fixed: "Open With" or passing files from command line not working.
* Fixed: "Reload All" would not reload files.
In addition, a lot of work has gone into getting a usable Mac OSX version.
New Features
------------
* Plugin based system
The Uranium framework provides us with a plugin-based system
that provides additional flexibility when extending Cura. Think
of new views, tools, file formats, etc. This is probably the
biggest new feature.
* Improved UI
The UI has received a complete overhaul.
* Time-Quality Slider
The 4 static quick print profiles have been replaced with
a slider that should make it easier to find the right spot
between print time and print quality.
* More Settings
The Advanced mode is now configurable and can show many
additional settings that were previously not available, while at
the same time not overwhelming new users with too many settings.
Custom set of visible settings can be created by the user.
* Support for high-DPI screens
The refreshed UI has been designed with high-DPI screens in
mind which should improve the experience of Cura on such
devices.
* Improved language support
(Not yet available for the Beta release.)
* Improved support structure generation
The new version of the CuraEngine now features improved
support generation algorithms and additional options for support
structure generation.
* Experimental Feature: Wire Printing
Wire Printing has been added as an experimental new feature. It
will print objects as a structure of lines. It can be enabled by
from Advanced Mode -> Fixes -> Wire Printing.
* Undo/Redo
It is now possible to undo and redo most scene operations, like
moving or rotating objects.
Features from earlier versions not (yet) in this release
--------------------------------------------------------
* The All-at-once/One-at-a-time toggle is not available.
We are working on an improved implementation of this mechanism
but it will not be available for this release.
* No dual extrusion features are available yet.
We are working on a completely new workflow for this but this
needs additional time.
* “Lay Flat” has been removed.
The existing implementation was unfortunately not salvageable.
We will be looking into an improved implementation for this
feature.
* "Split Object Into Parts" has been removed.
Due to the same reason as Lay Flat.
* Support for AMF and DAE file formats has been removed.
Both of these will be implemented as plugins in the future.
* Support for directly loading a GCode file is not yet available.
This will be implemented as a plugin in the future.
* Support for PNG, JPG and other image formats has been removed.
These can be supported by a plugin with an improved UI.
* Support for loading Minecraft levels has been removed.
This can be implemented as a plugin.
* Windows XP support has been dropped.
Microsoft is no longer supporting xp, so they no longer back
port certain features that we require.
* X-Ray view is missing.
Will be implemented as a (you might have guessed it) plugin.
* Fixes: Follow Mesh Surface
Has been removed from the engine, the same result can be
achieved using no infill or top/bottom layers.
Known Issues
------------
* Some OBJ files are rendered as black objects due to missing
normals.
* Disabling plugins does not work correctly yet.
* Unicorn occasionally still requires feeding. Do not feed it
after midnight.

View file

@ -61,6 +61,8 @@ message Polygon {
Type type = 1; // Type of move
bytes points = 2; // The points of the polygon, or two points if only a line segment (Currently only line segments are used)
float line_width = 3; // The width of the line being laid down
float line_thickness = 4; // The thickness of the line being laid down
float line_feedrate = 5; // The feedrate of the line being laid down
}
message LayerOptimized {
@ -82,6 +84,8 @@ message PathSegment {
bytes points = 3; // The points defining the line segments, bytes of float[2/3] array of length N+1
bytes line_type = 4; // Type of line segment as an unsigned char array of length 1 or N, where N is the number of line segments in this path
bytes line_width = 5; // The widths of the line segments as bytes of a float array of length 1 or N
bytes line_thickness = 6; // The thickness of the line segments as bytes of a float array of length 1 or N
bytes line_feedrate = 7; // The feedrate of the line segments as bytes of a float array of length 1 or N
}

View file

@ -86,6 +86,7 @@ class CuraEngineBackend(QObject, Backend):
#
self._global_container_stack = None
Application.getInstance().globalContainerStackChanged.connect(self._onGlobalStackChanged)
Application.getInstance().getExtruderManager().activeExtruderChanged.connect(self._onGlobalStackChanged)
self._onGlobalStackChanged()
Application.getInstance().stacksValidationFinished.connect(self._onStackErrorCheckFinished)
@ -301,6 +302,26 @@ class CuraEngineBackend(QObject, Backend):
self.backendStateChange.emit(BackendState.NotStarted)
return
elif job.getResult() == StartSliceJob.StartJobResult.ObjectSettingError:
errors = {}
for node in DepthFirstIterator(Application.getInstance().getController().getScene().getRoot()):
stack = node.callDecoration("getStack")
if not stack:
continue
for key in stack.getErrorKeys():
definition = self._global_container_stack.getBottom().findDefinitions(key = key)
if not definition:
Logger.log("e", "When checking settings for errors, unable to find definition for key {key} in per-object stack.".format(key = key))
continue
definition = definition[0]
errors[key] = definition.label
error_labels = ", ".join(errors.values())
self._error_message = Message(catalog.i18nc("@info:status", "Unable to slice due to some per-model settings. The following settings have errors on one or more models: {error_labels}").format(error_labels = error_labels),
title = catalog.i18nc("@info:title", "Unable to slice"))
self._error_message.show()
self.backendStateChange.emit(BackendState.Error)
return
if job.getResult() == StartSliceJob.StartJobResult.BuildPlateError:
if Application.getInstance().platformActivity:
self._error_message = Message(catalog.i18nc("@info:status", "Unable to slice because the prime tower or prime position(s) are invalid."),
@ -588,7 +609,7 @@ class CuraEngineBackend(QObject, Backend):
def _onActiveViewChanged(self):
if Application.getInstance().getController().getActiveView():
view = Application.getInstance().getController().getActiveView()
if view.getPluginId() == "LayerView": # If switching to layer view, we should process the layers if that hasn't been done yet.
if view.getPluginId() == "SimulationView": # If switching to layer view, we should process the layers if that hasn't been done yet.
self._layer_view_active = True
# There is data and we're not slicing at the moment
# if we are slicing, there is no need to re-calculate the data as it will be invalid in a moment.

View file

@ -61,7 +61,9 @@ class ProcessSlicedLayersJob(Job):
def run(self):
start_time = time()
if Application.getInstance().getController().getActiveView().getPluginId() == "LayerView":
view = Application.getInstance().getController().getActiveView()
if view.getPluginId() == "SimulationView":
view.resetLayerData()
self._progress_message.show()
Job.yieldThread()
if self._abort_requested:
@ -95,20 +97,27 @@ class ProcessSlicedLayersJob(Job):
# Find the minimum layer number
# When using a raft, the raft layers are sent as layers < 0. Instead of allowing layers < 0, we
# instead simply offset all other layers so the lowest layer is always 0.
# instead simply offset all other layers so the lowest layer is always 0. It could happens that
# the first raft layer has value -8 but there are just 4 raft (negative) layers.
min_layer_number = 0
negative_layers = 0
for layer in self._layers:
if layer.id < min_layer_number:
min_layer_number = layer.id
if layer.id < 0:
negative_layers += 1
current_layer = 0
for layer in self._layers:
abs_layer_number = layer.id + abs(min_layer_number)
# Negative layers are offset by the minimum layer number, but the positive layers are just
# offset by the number of negative layers so there is no layer gap between raft and model
abs_layer_number = layer.id + abs(min_layer_number) if layer.id < 0 else layer.id + negative_layers
layer_data.addLayer(abs_layer_number)
this_layer = layer_data.getLayer(abs_layer_number)
layer_data.setLayerHeight(abs_layer_number, layer.height)
layer_data.setLayerThickness(abs_layer_number, layer.thickness)
for p in range(layer.repeatedMessageCount("path_segment")):
polygon = layer.getRepeatedMessage("path_segment", p)
@ -127,10 +136,11 @@ class ProcessSlicedLayersJob(Job):
line_widths = numpy.fromstring(polygon.line_width, dtype="f4") # Convert bytearray to numpy array
line_widths = line_widths.reshape((-1,1)) # We get a linear list of pairs that make up the points, so make numpy interpret them correctly.
# In the future, line_thicknesses should be given by CuraEngine as well.
# Currently the infill layer thickness also translates to line width
line_thicknesses = numpy.zeros(line_widths.shape, dtype="f4")
line_thicknesses[:] = layer.thickness / 1000 # from micrometer to millimeter
line_thicknesses = numpy.fromstring(polygon.line_thickness, dtype="f4") # Convert bytearray to numpy array
line_thicknesses = line_thicknesses.reshape((-1,1)) # We get a linear list of pairs that make up the points, so make numpy interpret them correctly.
line_feedrates = numpy.fromstring(polygon.line_feedrate, dtype="f4") # Convert bytearray to numpy array
line_feedrates = line_feedrates.reshape((-1,1)) # We get a linear list of pairs that make up the points, so make numpy interpret them correctly.
# Create a new 3D-array, copy the 2D points over and insert the right height.
# This uses manual array creation + copy rather than numpy.insert since this is
@ -145,7 +155,7 @@ class ProcessSlicedLayersJob(Job):
new_points[:, 1] = points[:, 2]
new_points[:, 2] = -points[:, 1]
this_poly = LayerPolygon.LayerPolygon(extruder, line_types, new_points, line_widths, line_thicknesses)
this_poly = LayerPolygon.LayerPolygon(extruder, line_types, new_points, line_widths, line_thicknesses, line_feedrates)
this_poly.buildCache()
this_layer.polygons.append(this_poly)
@ -218,10 +228,6 @@ class ProcessSlicedLayersJob(Job):
if self._progress_message:
self._progress_message.setProgress(100)
view = Application.getInstance().getController().getActiveView()
if view.getPluginId() == "LayerView":
view.resetLayerData()
if self._progress_message:
self._progress_message.hide()
@ -232,7 +238,7 @@ class ProcessSlicedLayersJob(Job):
def _onActiveViewChanged(self):
if self.isRunning():
if Application.getInstance().getController().getActiveView().getPluginId() == "LayerView":
if Application.getInstance().getController().getActiveView().getPluginId() == "SimulationView":
if not self._progress_message:
self._progress_message = Message(catalog.i18nc("@info:status", "Processing Layers"), 0, False, 0, catalog.i18nc("@info:title", "Information"))
if self._progress_message.getProgress() != 100:

View file

@ -26,6 +26,7 @@ class StartJobResult(IntEnum):
NothingToSlice = 4
MaterialIncompatible = 5
BuildPlateError = 6
ObjectSettingError = 7 #When an error occurs in per-object settings.
## Formatter class that handles token expansion in start/end gcod
@ -44,14 +45,6 @@ class GcodeStartEndFormatter(Formatter):
## Job class that builds up the message of scene data to send to CuraEngine.
class StartSliceJob(Job):
## Meshes that are sent to the engine regardless of being outside of the
# build volume.
#
# If these settings are True for any mesh, the build volume is ignored.
# Note that Support Mesh is not in here because it actually generates
# g-code in the volume of the mesh.
_not_printed_mesh_settings = {"anti_overhang_mesh", "infill_mesh", "cutting_mesh"}
def __init__(self, slice_message):
super().__init__()
@ -105,7 +98,7 @@ class StartSliceJob(Job):
continue
if self._checkStackForErrors(node.callDecoration("getStack")):
self.setResult(StartJobResult.SettingError)
self.setResult(StartJobResult.ObjectSettingError)
return
with self._scene.getSceneLock():
@ -138,13 +131,21 @@ class StartSliceJob(Job):
Logger.log("w", "No objects suitable for one at a time found, or no correct order found")
else:
temp_list = []
has_printing_mesh = False
for node in DepthFirstIterator(self._scene.getRoot()):
if type(node) is SceneNode and node.getMeshData() and node.getMeshData().getVertices() is not None:
if not getattr(node, "_outside_buildarea", False)\
or (node.callDecoration("getStack") and any(node.callDecoration("getStack").getProperty(setting, "value") for setting in self._not_printed_mesh_settings)):
_non_printing_mesh = getattr(node, "_non_printing_mesh", False)
if not getattr(node, "_outside_buildarea", False) or _non_printing_mesh:
temp_list.append(node)
if not _non_printing_mesh:
has_printing_mesh = True
Job.yieldThread()
#If the list doesn't have any model with suitable settings then clean the list
# otherwise CuraEngine will crash
if not has_printing_mesh:
temp_list.clear()
if temp_list:
object_groups.append(temp_list)
@ -158,13 +159,9 @@ class StartSliceJob(Job):
self._buildGlobalSettingsMessage(stack)
self._buildGlobalInheritsStackMessage(stack)
# Only add extruder stacks if there are multiple extruders
# Single extruder machines only use the global stack to store setting values
if stack.getProperty("machine_extruder_count", "value") > 1:
for extruder_stack in ExtruderManager.getInstance().getMachineExtruders(stack.getId()):
self._buildExtruderMessage(extruder_stack)
else:
self._buildExtruderMessageFromGlobalStack(stack)
# Build messages for extruder stacks
for extruder_stack in ExtruderManager.getInstance().getMachineExtruders(stack.getId()):
self._buildExtruderMessage(extruder_stack)
for group in object_groups:
group_message = self._slice_message.addRepeatedMessage("object_lists")
@ -208,32 +205,53 @@ class StartSliceJob(Job):
def isCancelled(self):
return self._is_cancelled
def _expandGcodeTokens(self, key, value, settings):
## Creates a dictionary of tokens to replace in g-code pieces.
#
# This indicates what should be replaced in the start and end g-codes.
# \param stack The stack to get the settings from to replace the tokens
# with.
# \return A dictionary of replacement tokens to the values they should be
# replaced with.
def _buildReplacementTokens(self, stack) -> dict:
result = {}
for key in stack.getAllKeys():
result[key] = stack.getProperty(key, "value")
Job.yieldThread()
result["print_bed_temperature"] = result["material_bed_temperature"] # Renamed settings.
result["print_temperature"] = result["material_print_temperature"]
result["time"] = time.strftime("%H:%M:%S") #Some extra settings.
result["date"] = time.strftime("%d-%m-%Y")
result["day"] = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"][int(time.strftime("%w"))]
return result
## Replace setting tokens in a piece of g-code.
# \param value A piece of g-code to replace tokens in.
# \param settings A dictionary of tokens to replace and their respective
# replacement strings.
def _expandGcodeTokens(self, value: str, settings: dict):
try:
# any setting can be used as a token
fmt = GcodeStartEndFormatter()
return str(fmt.format(value, **settings)).encode("utf-8")
return str(fmt.format(value, **settings))
except:
Logger.logException("w", "Unable to do token replacement on start/end gcode")
return str(value).encode("utf-8")
return str(value)
## Create extruder message from stack
def _buildExtruderMessage(self, stack):
message = self._slice_message.addRepeatedMessage("extruders")
message.id = int(stack.getMetaDataEntry("position"))
material_instance_container = stack.findContainer({"type": "material"})
settings = self._buildReplacementTokens(stack)
settings = {}
for key in stack.getAllKeys():
settings[key] = stack.getProperty(key, "value")
Job.yieldThread()
# Also send the material GUID. This is a setting in fdmprinter, but we have no interface for it.
settings["material_guid"] = stack.material.getMetaDataEntry("GUID", "")
settings["print_bed_temperature"] = settings["material_bed_temperature"] #Renamed settings.
settings["print_temperature"] = settings["material_print_temperature"]
settings["time"] = time.strftime("%H:%M:%S") #Some extra settings.
settings["date"] = time.strftime("%d-%m-%Y")
settings["day"] = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"][int(time.strftime("%w"))]
# Replace the setting tokens in start and end g-code.
settings["machine_extruder_start_code"] = self._expandGcodeTokens(settings["machine_extruder_start_code"], settings)
settings["machine_extruder_end_code"] = self._expandGcodeTokens(settings["machine_extruder_end_code"], settings)
for key, value in settings.items():
# Do not send settings that are not settable_per_extruder.
@ -241,26 +259,7 @@ class StartSliceJob(Job):
continue
setting = message.getMessage("settings").addRepeatedMessage("settings")
setting.name = key
if key == "material_guid" and material_instance_container:
# Also send the material GUID. This is a setting in fdmprinter, but we have no interface for it.
setting.value = str(material_instance_container.getMetaDataEntry("GUID", "")).encode("utf-8")
elif key == "machine_extruder_start_code" or key == "machine_extruder_end_code":
setting.value = self._expandGcodeTokens(key, value, settings)
else:
setting.value = str(stack.getProperty(key, "value")).encode("utf-8")
Job.yieldThread()
## Create extruder message from global stack
def _buildExtruderMessageFromGlobalStack(self, stack):
message = self._slice_message.addRepeatedMessage("extruders")
for key in stack.getAllKeys():
# Do not send settings that are not settable_per_extruder.
if not stack.getProperty(key, "settable_per_extruder"):
continue
setting = message.getMessage("settings").addRepeatedMessage("settings")
setting.name = key
setting.value = str(stack.getProperty(key, "value")).encode("utf-8")
setting.value = str(value).encode("utf-8")
Job.yieldThread()
## Sends all global settings to the engine.
@ -268,33 +267,28 @@ class StartSliceJob(Job):
# The settings are taken from the global stack. This does not include any
# per-extruder settings or per-object settings.
def _buildGlobalSettingsMessage(self, stack):
keys = stack.getAllKeys()
settings = {}
for key in keys:
settings[key] = stack.getProperty(key, "value")
Job.yieldThread()
settings = self._buildReplacementTokens(stack)
# Pre-compute material material_bed_temp_prepend and material_print_temp_prepend
start_gcode = settings["machine_start_gcode"]
#Pre-compute material material_bed_temp_prepend and material_print_temp_prepend
bed_temperature_settings = {"material_bed_temperature", "material_bed_temperature_layer_0"}
settings["material_bed_temp_prepend"] = all(("{" + setting + "}" not in start_gcode for setting in bed_temperature_settings))
print_temperature_settings = {"material_print_temperature", "material_print_temperature_layer_0", "default_material_print_temperature", "material_initial_print_temperature", "material_final_print_temperature", "material_standby_temperature"}
settings["material_print_temp_prepend"] = all(("{" + setting + "}" not in start_gcode for setting in print_temperature_settings))
settings["print_bed_temperature"] = settings["material_bed_temperature"]
settings["print_temperature"] = settings["material_print_temperature"]
# Find the correct temperatures from the first used extruder
extruder_stack = Application.getInstance().getExtruderManager().getUsedExtruderStacks()[0]
extruder_0_settings = self._buildReplacementTokens(extruder_stack)
settings["time"] = time.strftime('%H:%M:%S')
settings["date"] = time.strftime('%d-%m-%Y')
settings["day"] = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'][int(time.strftime('%w'))]
# Replace the setting tokens in start and end g-code.
settings["machine_start_gcode"] = self._expandGcodeTokens(settings["machine_start_gcode"], extruder_0_settings)
settings["machine_end_gcode"] = self._expandGcodeTokens(settings["machine_end_gcode"], extruder_0_settings)
for key, value in settings.items(): #Add all submessages for each individual setting.
# Add all sub-messages for each individual setting.
for key, value in settings.items():
setting_message = self._slice_message.getMessage("global_settings").addRepeatedMessage("settings")
setting_message.name = key
if key == "machine_start_gcode" or key == "machine_end_gcode": #If it's a g-code message, use special formatting.
setting_message.value = self._expandGcodeTokens(key, value, settings)
else:
setting_message.value = str(value).encode("utf-8")
setting_message.value = str(value).encode("utf-8")
Job.yieldThread()
## Sends for some settings which extruder they should fallback to if not
@ -364,3 +358,4 @@ class StartSliceJob(Job):
relations_set.add(relation.target.key)
self._addRelations(relations_set, relation.target.relations)

View file

@ -0,0 +1,462 @@
# Copyright (c) 2017 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from UM.Application import Application
from UM.Backend import Backend
from UM.Job import Job
from UM.Logger import Logger
from UM.Math.AxisAlignedBox import AxisAlignedBox
from UM.Math.Vector import Vector
from UM.Message import Message
from UM.Scene.SceneNode import SceneNode
from UM.i18n import i18nCatalog
from UM.Preferences import Preferences
catalog = i18nCatalog("cura")
from cura import LayerDataBuilder
from cura import LayerDataDecorator
from cura.LayerPolygon import LayerPolygon
from cura.GCodeListDecorator import GCodeListDecorator
from cura.Settings.ExtruderManager import ExtruderManager
import numpy
import math
import re
from collections import namedtuple
# This parser is intented for interpret the common firmware codes among all the different flavors
class FlavorParser:
def __init__(self):
Application.getInstance().hideMessageSignal.connect(self._onHideMessage)
self._cancelled = False
self._message = None
self._layer_number = 0
self._extruder_number = 0
self._clearValues()
self._scene_node = None
# X, Y, Z position, F feedrate and E extruder values are stored
self._position = namedtuple('Position', ['x', 'y', 'z', 'f', 'e'])
self._is_layers_in_file = False # Does the Gcode have the layers comment?
self._extruder_offsets = {} # Offsets for multi extruders. key is index, value is [x-offset, y-offset]
self._current_layer_thickness = 0.2 # default
Preferences.getInstance().addPreference("gcodereader/show_caution", True)
def _clearValues(self):
self._filament_diameter = 2.85
self._extruder_number = 0
self._extrusion_length_offset = [0]
self._layer_type = LayerPolygon.Inset0Type
self._layer_number = 0
self._previous_z = 0
self._layer_data_builder = LayerDataBuilder.LayerDataBuilder()
self._center_is_zero = False
self._is_absolute_positioning = True # It can be absolute (G90) or relative (G91)
self._is_absolute_extrusion = True # It can become absolute (M82, default) or relative (M83)
@staticmethod
def _getValue(line, code):
n = line.find(code)
if n < 0:
return None
n += len(code)
pattern = re.compile("[;\s]")
match = pattern.search(line, n)
m = match.start() if match is not None else -1
try:
if m < 0:
return line[n:]
return line[n:m]
except:
return None
def _getInt(self, line, code):
value = self._getValue(line, code)
try:
return int(value)
except:
return None
def _getFloat(self, line, code):
value = self._getValue(line, code)
try:
return float(value)
except:
return None
def _onHideMessage(self, message):
if message == self._message:
self._cancelled = True
@staticmethod
def _getNullBoundingBox():
return AxisAlignedBox(minimum=Vector(0, 0, 0), maximum=Vector(10, 10, 10))
def _createPolygon(self, layer_thickness, path, extruder_offsets):
countvalid = 0
for point in path:
if point[5] > 0:
countvalid += 1
if countvalid >= 2:
# we know what to do now, no need to count further
continue
if countvalid < 2:
return False
try:
self._layer_data_builder.addLayer(self._layer_number)
self._layer_data_builder.setLayerHeight(self._layer_number, path[0][2])
self._layer_data_builder.setLayerThickness(self._layer_number, layer_thickness)
this_layer = self._layer_data_builder.getLayer(self._layer_number)
except ValueError:
return False
count = len(path)
line_types = numpy.empty((count - 1, 1), numpy.int32)
line_widths = numpy.empty((count - 1, 1), numpy.float32)
line_thicknesses = numpy.empty((count - 1, 1), numpy.float32)
line_feedrates = numpy.empty((count - 1, 1), numpy.float32)
line_widths[:, 0] = 0.35 # Just a guess
line_thicknesses[:, 0] = layer_thickness
points = numpy.empty((count, 3), numpy.float32)
extrusion_values = numpy.empty((count, 1), numpy.float32)
i = 0
for point in path:
points[i, :] = [point[0] + extruder_offsets[0], point[2], -point[1] - extruder_offsets[1]]
extrusion_values[i] = point[4]
if i > 0:
line_feedrates[i - 1] = point[3]
line_types[i - 1] = point[5]
if point[5] in [LayerPolygon.MoveCombingType, LayerPolygon.MoveRetractionType]:
line_widths[i - 1] = 0.1
line_thicknesses[i - 1] = 0.0 # Travels are set as zero thickness lines
else:
line_widths[i - 1] = self._calculateLineWidth(points[i], points[i-1], extrusion_values[i], extrusion_values[i-1], layer_thickness)
i += 1
this_poly = LayerPolygon(self._extruder_number, line_types, points, line_widths, line_thicknesses, line_feedrates)
this_poly.buildCache()
this_layer.polygons.append(this_poly)
return True
def _createEmptyLayer(self, layer_number):
self._layer_data_builder.addLayer(layer_number)
self._layer_data_builder.setLayerHeight(layer_number, 0)
self._layer_data_builder.setLayerThickness(layer_number, 0)
def _calculateLineWidth(self, current_point, previous_point, current_extrusion, previous_extrusion, layer_thickness):
# Area of the filament
Af = (self._filament_diameter / 2) ** 2 * numpy.pi
# Length of the extruded filament
de = current_extrusion - previous_extrusion
# Volumne of the extruded filament
dVe = de * Af
# Length of the printed line
dX = numpy.sqrt((current_point[0] - previous_point[0])**2 + (current_point[2] - previous_point[2])**2)
# When the extruder recovers from a retraction, we get zero distance
if dX == 0:
return 0.1
# Area of the printed line. This area is a rectangle
Ae = dVe / dX
# This area is a rectangle with area equal to layer_thickness * layer_width
line_width = Ae / layer_thickness
# A threshold is set to avoid weird paths in the GCode
if line_width > 1.2:
return 0.35
return line_width
def _gCode0(self, position, params, path):
x, y, z, f, e = position
if self._is_absolute_positioning:
x = params.x if params.x is not None else x
y = params.y if params.y is not None else y
z = params.z if params.z is not None else z
else:
x += params.x if params.x is not None else 0
y += params.y if params.y is not None else 0
z += params.z if params.z is not None else 0
f = params.f if params.f is not None else f
if params.e is not None:
new_extrusion_value = params.e if self._is_absolute_extrusion else e[self._extruder_number] + params.e
if new_extrusion_value > e[self._extruder_number]:
path.append([x, y, z, f, new_extrusion_value + self._extrusion_length_offset[self._extruder_number], self._layer_type]) # extrusion
else:
path.append([x, y, z, f, new_extrusion_value + self._extrusion_length_offset[self._extruder_number], LayerPolygon.MoveRetractionType]) # retraction
e[self._extruder_number] = new_extrusion_value
# Only when extruding we can determine the latest known "layer height" which is the difference in height between extrusions
# Also, 1.5 is a heuristic for any priming or whatsoever, we skip those.
if z > self._previous_z and (z - self._previous_z < 1.5):
self._current_layer_thickness = z - self._previous_z # allow a tiny overlap
self._previous_z = z
else:
path.append([x, y, z, f, e[self._extruder_number] + self._extrusion_length_offset[self._extruder_number], LayerPolygon.MoveCombingType])
return self._position(x, y, z, f, e)
# G0 and G1 should be handled exactly the same.
_gCode1 = _gCode0
## Home the head.
def _gCode28(self, position, params, path):
return self._position(
params.x if params.x is not None else position.x,
params.y if params.y is not None else position.y,
params.z if params.z is not None else position.z,
position.f,
position.e)
## Set the absolute positioning
def _gCode90(self, position, params, path):
self._is_absolute_positioning = True
self._is_absolute_extrusion = True
return position
## Set the relative positioning
def _gCode91(self, position, params, path):
self._is_absolute_positioning = False
self._is_absolute_extrusion = False
return position
## Reset the current position to the values specified.
# For example: G92 X10 will set the X to 10 without any physical motion.
def _gCode92(self, position, params, path):
if params.e is not None:
# Sometimes a G92 E0 is introduced in the middle of the GCode so we need to keep those offsets for calculate the line_width
self._extrusion_length_offset[self._extruder_number] += position.e[self._extruder_number] - params.e
position.e[self._extruder_number] = params.e
return self._position(
params.x if params.x is not None else position.x,
params.y if params.y is not None else position.y,
params.z if params.z is not None else position.z,
params.f if params.f is not None else position.f,
position.e)
def processGCode(self, G, line, position, path):
func = getattr(self, "_gCode%s" % G, None)
line = line.split(";", 1)[0] # Remove comments (if any)
if func is not None:
s = line.upper().split(" ")
x, y, z, f, e = None, None, None, None, None
for item in s[1:]:
if len(item) <= 1:
continue
if item.startswith(";"):
continue
if item[0] == "X":
x = float(item[1:])
if item[0] == "Y":
y = float(item[1:])
if item[0] == "Z":
z = float(item[1:])
if item[0] == "F":
f = float(item[1:]) / 60
if item[0] == "E":
e = float(item[1:])
if self._is_absolute_positioning and ((x is not None and x < 0) or (y is not None and y < 0)):
self._center_is_zero = True
params = self._position(x, y, z, f, e)
return func(position, params, path)
return position
def processTCode(self, T, line, position, path):
self._extruder_number = T
if self._extruder_number + 1 > len(position.e):
self._extrusion_length_offset.extend([0] * (self._extruder_number - len(position.e) + 1))
position.e.extend([0] * (self._extruder_number - len(position.e) + 1))
return position
def processMCode(self, M, line, position, path):
pass
_type_keyword = ";TYPE:"
_layer_keyword = ";LAYER:"
## For showing correct x, y offsets for each extruder
def _extruderOffsets(self):
result = {}
for extruder in ExtruderManager.getInstance().getExtruderStacks():
result[int(extruder.getMetaData().get("position", "0"))] = [
extruder.getProperty("machine_nozzle_offset_x", "value"),
extruder.getProperty("machine_nozzle_offset_y", "value")]
return result
def processGCodeFile(self, file_name):
Logger.log("d", "Preparing to load %s" % file_name)
self._cancelled = False
# We obtain the filament diameter from the selected printer to calculate line widths
self._filament_diameter = Application.getInstance().getGlobalContainerStack().getProperty("material_diameter", "value")
scene_node = SceneNode()
# Override getBoundingBox function of the sceneNode, as this node should return a bounding box, but there is no
# real data to calculate it from.
scene_node.getBoundingBox = self._getNullBoundingBox
gcode_list = []
self._is_layers_in_file = False
Logger.log("d", "Opening file %s" % file_name)
self._extruder_offsets = self._extruderOffsets() # dict with index the extruder number. can be empty
with open(file_name, "r") as file:
file_lines = 0
current_line = 0
for line in file:
file_lines += 1
gcode_list.append(line)
if not self._is_layers_in_file and line[:len(self._layer_keyword)] == self._layer_keyword:
self._is_layers_in_file = True
file.seek(0)
file_step = max(math.floor(file_lines / 100), 1)
self._clearValues()
self._message = Message(catalog.i18nc("@info:status", "Parsing G-code"),
lifetime=0,
title = catalog.i18nc("@info:title", "G-code Details"))
self._message.setProgress(0)
self._message.show()
Logger.log("d", "Parsing %s..." % file_name)
current_position = self._position(0, 0, 0, 0, [0])
current_path = []
min_layer_number = 0
negative_layers = 0
previous_layer = 0
for line in file:
if self._cancelled:
Logger.log("d", "Parsing %s cancelled" % file_name)
return None
current_line += 1
if current_line % file_step == 0:
self._message.setProgress(math.floor(current_line / file_lines * 100))
Job.yieldThread()
if len(line) == 0:
continue
if line.find(self._type_keyword) == 0:
type = line[len(self._type_keyword):].strip()
if type == "WALL-INNER":
self._layer_type = LayerPolygon.InsetXType
elif type == "WALL-OUTER":
self._layer_type = LayerPolygon.Inset0Type
elif type == "SKIN":
self._layer_type = LayerPolygon.SkinType
elif type == "SKIRT":
self._layer_type = LayerPolygon.SkirtType
elif type == "SUPPORT":
self._layer_type = LayerPolygon.SupportType
elif type == "FILL":
self._layer_type = LayerPolygon.InfillType
else:
Logger.log("w", "Encountered a unknown type (%s) while parsing g-code.", type)
# When the layer change is reached, the polygon is computed so we have just one layer per layer per extruder
if self._is_layers_in_file and line[:len(self._layer_keyword)] == self._layer_keyword:
try:
layer_number = int(line[len(self._layer_keyword):])
self._createPolygon(self._current_layer_thickness, current_path, self._extruder_offsets.get(self._extruder_number, [0, 0]))
current_path.clear()
# When using a raft, the raft layers are stored as layers < 0, it mimics the same behavior
# as in ProcessSlicedLayersJob
if layer_number < min_layer_number:
min_layer_number = layer_number
if layer_number < 0:
layer_number += abs(min_layer_number)
negative_layers += 1
else:
layer_number += negative_layers
# In case there is a gap in the layer count, empty layers are created
for empty_layer in range(previous_layer + 1, layer_number):
self._createEmptyLayer(empty_layer)
self._layer_number = layer_number
previous_layer = layer_number
except:
pass
# This line is a comment. Ignore it (except for the layer_keyword)
if line.startswith(";"):
continue
G = self._getInt(line, "G")
if G is not None:
# When find a movement, the new posistion is calculated and added to the current_path, but
# don't need to create a polygon until the end of the layer
current_position = self.processGCode(G, line, current_position, current_path)
continue
# When changing the extruder, the polygon with the stored paths is computed
if line.startswith("T"):
T = self._getInt(line, "T")
if T is not None:
self._createPolygon(self._current_layer_thickness, current_path, self._extruder_offsets.get(self._extruder_number, [0, 0]))
current_path.clear()
current_position = self.processTCode(T, line, current_position, current_path)
if line.startswith("M"):
M = self._getInt(line, "M")
self.processMCode(M, line, current_position, current_path)
# "Flush" leftovers. Last layer paths are still stored
if len(current_path) > 1:
if self._createPolygon(self._current_layer_thickness, current_path, self._extruder_offsets.get(self._extruder_number, [0, 0])):
self._layer_number += 1
current_path.clear()
material_color_map = numpy.zeros((10, 4), dtype = numpy.float32)
material_color_map[0, :] = [0.0, 0.7, 0.9, 1.0]
material_color_map[1, :] = [0.7, 0.9, 0.0, 1.0]
layer_mesh = self._layer_data_builder.build(material_color_map)
decorator = LayerDataDecorator.LayerDataDecorator()
decorator.setLayerData(layer_mesh)
scene_node.addDecorator(decorator)
gcode_list_decorator = GCodeListDecorator()
gcode_list_decorator.setGCodeList(gcode_list)
scene_node.addDecorator(gcode_list_decorator)
Application.getInstance().getController().getScene().gcode_list = gcode_list
Logger.log("d", "Finished parsing %s" % file_name)
self._message.hide()
if self._layer_number == 0:
Logger.log("w", "File %s doesn't contain any valid layers" % file_name)
settings = Application.getInstance().getGlobalContainerStack()
machine_width = settings.getProperty("machine_width", "value")
machine_depth = settings.getProperty("machine_depth", "value")
if not self._center_is_zero:
scene_node.setPosition(Vector(-machine_width / 2, 0, machine_depth / 2))
Logger.log("d", "Loaded %s" % file_name)
if Preferences.getInstance().getValue("gcodereader/show_caution"):
caution_message = Message(catalog.i18nc(
"@info:generic",
"Make sure the g-code is suitable for your printer and printer configuration before sending the file to it. The g-code representation may not be accurate."),
lifetime=0,
title = catalog.i18nc("@info:title", "G-code Details"))
caution_message.show()
# The "save/print" button's state is bound to the backend state.
backend = Application.getInstance().getBackend()
backend.backendStateChange.emit(Backend.BackendState.Disabled)
return scene_node

View file

@ -1,377 +1,44 @@
# Copyright (c) 2016 Aleph Objects, Inc.
# Copyright (c) 2017 Aleph Objects, Inc.
# Cura is released under the terms of the LGPLv3 or higher.
from UM.Application import Application
from UM.Backend import Backend
from UM.Job import Job
from UM.Logger import Logger
from UM.Math.AxisAlignedBox import AxisAlignedBox
from UM.Math.Vector import Vector
from UM.FileHandler.FileReader import FileReader
from UM.Mesh.MeshReader import MeshReader
from UM.Message import Message
from UM.Scene.SceneNode import SceneNode
from UM.i18n import i18nCatalog
from UM.Preferences import Preferences
catalog = i18nCatalog("cura")
from cura import LayerDataBuilder
from cura import LayerDataDecorator
from cura.LayerPolygon import LayerPolygon
from cura.GCodeListDecorator import GCodeListDecorator
from cura.Settings.ExtruderManager import ExtruderManager
import numpy
import math
import re
from collections import namedtuple
from . import MarlinFlavorParser, RepRapFlavorParser
# Class for loading and parsing G-code files
class GCodeReader(MeshReader):
_flavor_default = "Marlin"
_flavor_keyword = ";FLAVOR:"
_flavor_readers_dict = {"RepRap" : RepRapFlavorParser.RepRapFlavorParser(),
"Marlin" : MarlinFlavorParser.MarlinFlavorParser()}
def __init__(self):
super(GCodeReader, self).__init__()
self._supported_extensions = [".gcode", ".g"]
Application.getInstance().hideMessageSignal.connect(self._onHideMessage)
self._cancelled = False
self._message = None
self._layer_number = 0
self._extruder_number = 0
self._clearValues()
self._scene_node = None
self._position = namedtuple('Position', ['x', 'y', 'z', 'e'])
self._is_layers_in_file = False # Does the Gcode have the layers comment?
self._extruder_offsets = {} # Offsets for multi extruders. key is index, value is [x-offset, y-offset]
self._current_layer_thickness = 0.2 # default
self._flavor_reader = None
Preferences.getInstance().addPreference("gcodereader/show_caution", True)
def _clearValues(self):
self._extruder_number = 0
self._layer_type = LayerPolygon.Inset0Type
self._layer_number = 0
self._previous_z = 0
self._layer_data_builder = LayerDataBuilder.LayerDataBuilder()
self._center_is_zero = False
# PreRead is used to get the correct flavor. If not, Marlin is set by default
def preRead(self, file_name, *args, **kwargs):
with open(file_name, "r") as file:
for line in file:
if line[:len(self._flavor_keyword)] == self._flavor_keyword:
try:
self._flavor_reader = self._flavor_readers_dict[line[len(self._flavor_keyword):].rstrip()]
return FileReader.PreReadResult.accepted
except:
# If there is no entry in the dictionary for this flavor, just skip and select the by-default flavor
break
@staticmethod
def _getValue(line, code):
n = line.find(code)
if n < 0:
return None
n += len(code)
pattern = re.compile("[;\s]")
match = pattern.search(line, n)
m = match.start() if match is not None else -1
try:
if m < 0:
return line[n:]
return line[n:m]
except:
return None
def _getInt(self, line, code):
value = self._getValue(line, code)
try:
return int(value)
except:
return None
def _getFloat(self, line, code):
value = self._getValue(line, code)
try:
return float(value)
except:
return None
def _onHideMessage(self, message):
if message == self._message:
self._cancelled = True
@staticmethod
def _getNullBoundingBox():
return AxisAlignedBox(minimum=Vector(0, 0, 0), maximum=Vector(10, 10, 10))
def _createPolygon(self, layer_thickness, path, extruder_offsets):
countvalid = 0
for point in path:
if point[3] > 0:
countvalid += 1
if countvalid >= 2:
# we know what to do now, no need to count further
continue
if countvalid < 2:
return False
try:
self._layer_data_builder.addLayer(self._layer_number)
self._layer_data_builder.setLayerHeight(self._layer_number, path[0][2])
self._layer_data_builder.setLayerThickness(self._layer_number, layer_thickness)
this_layer = self._layer_data_builder.getLayer(self._layer_number)
except ValueError:
return False
count = len(path)
line_types = numpy.empty((count - 1, 1), numpy.int32)
line_widths = numpy.empty((count - 1, 1), numpy.float32)
line_thicknesses = numpy.empty((count - 1, 1), numpy.float32)
# TODO: need to calculate actual line width based on E values
line_widths[:, 0] = 0.35 # Just a guess
line_thicknesses[:, 0] = layer_thickness
points = numpy.empty((count, 3), numpy.float32)
i = 0
for point in path:
points[i, :] = [point[0] + extruder_offsets[0], point[2], -point[1] - extruder_offsets[1]]
if i > 0:
line_types[i - 1] = point[3]
if point[3] in [LayerPolygon.MoveCombingType, LayerPolygon.MoveRetractionType]:
line_widths[i - 1] = 0.1
i += 1
this_poly = LayerPolygon(self._extruder_number, line_types, points, line_widths, line_thicknesses)
this_poly.buildCache()
this_layer.polygons.append(this_poly)
return True
def _gCode0(self, position, params, path):
x, y, z, e = position
x = params.x if params.x is not None else x
y = params.y if params.y is not None else y
z = params.z if params.z is not None else position.z
if params.e is not None:
if params.e > e[self._extruder_number]:
path.append([x, y, z, self._layer_type]) # extrusion
else:
path.append([x, y, z, LayerPolygon.MoveRetractionType]) # retraction
e[self._extruder_number] = params.e
# Only when extruding we can determine the latest known "layer height" which is the difference in height between extrusions
# Also, 1.5 is a heuristic for any priming or whatsoever, we skip those.
if z > self._previous_z and (z - self._previous_z < 1.5):
self._current_layer_thickness = z - self._previous_z + 0.05 # allow a tiny overlap
self._previous_z = z
else:
path.append([x, y, z, LayerPolygon.MoveCombingType])
return self._position(x, y, z, e)
# G0 and G1 should be handled exactly the same.
_gCode1 = _gCode0
## Home the head.
def _gCode28(self, position, params, path):
return self._position(
params.x if params.x is not None else position.x,
params.y if params.y is not None else position.y,
0,
position.e)
## Reset the current position to the values specified.
# For example: G92 X10 will set the X to 10 without any physical motion.
def _gCode92(self, position, params, path):
if params.e is not None:
position.e[self._extruder_number] = params.e
return self._position(
params.x if params.x is not None else position.x,
params.y if params.y is not None else position.y,
params.z if params.z is not None else position.z,
position.e)
def _processGCode(self, G, line, position, path):
func = getattr(self, "_gCode%s" % G, None)
line = line.split(";", 1)[0] # Remove comments (if any)
if func is not None:
s = line.upper().split(" ")
x, y, z, e = None, None, None, None
for item in s[1:]:
if len(item) <= 1:
continue
if item.startswith(";"):
continue
if item[0] == "X":
x = float(item[1:])
if item[0] == "Y":
y = float(item[1:])
if item[0] == "Z":
z = float(item[1:])
if item[0] == "E":
e = float(item[1:])
if (x is not None and x < 0) or (y is not None and y < 0):
self._center_is_zero = True
params = self._position(x, y, z, e)
return func(position, params, path)
return position
def _processTCode(self, T, line, position, path):
self._extruder_number = T
if self._extruder_number + 1 > len(position.e):
position.e.extend([0] * (self._extruder_number - len(position.e) + 1))
return position
_type_keyword = ";TYPE:"
_layer_keyword = ";LAYER:"
## For showing correct x, y offsets for each extruder
def _extruderOffsets(self):
result = {}
for extruder in ExtruderManager.getInstance().getExtruderStacks():
result[int(extruder.getMetaData().get("position", "0"))] = [
extruder.getProperty("machine_nozzle_offset_x", "value"),
extruder.getProperty("machine_nozzle_offset_y", "value")]
return result
# If no flavor is found in the GCode, then we use the by-default
self._flavor_reader = self._flavor_readers_dict[self._flavor_default]
return FileReader.PreReadResult.accepted
def read(self, file_name):
Logger.log("d", "Preparing to load %s" % file_name)
self._cancelled = False
scene_node = SceneNode()
# Override getBoundingBox function of the sceneNode, as this node should return a bounding box, but there is no
# real data to calculate it from.
scene_node.getBoundingBox = self._getNullBoundingBox
gcode_list = []
self._is_layers_in_file = False
Logger.log("d", "Opening file %s" % file_name)
self._extruder_offsets = self._extruderOffsets() # dict with index the extruder number. can be empty
last_z = 0
with open(file_name, "r") as file:
file_lines = 0
current_line = 0
for line in file:
file_lines += 1
gcode_list.append(line)
if not self._is_layers_in_file and line[:len(self._layer_keyword)] == self._layer_keyword:
self._is_layers_in_file = True
file.seek(0)
file_step = max(math.floor(file_lines / 100), 1)
self._clearValues()
self._message = Message(catalog.i18nc("@info:status", "Parsing G-code"),
lifetime=0,
title = catalog.i18nc("@info:title", "G-code Details"))
self._message.setProgress(0)
self._message.show()
Logger.log("d", "Parsing %s..." % file_name)
current_position = self._position(0, 0, 0, [0])
current_path = []
for line in file:
if self._cancelled:
Logger.log("d", "Parsing %s cancelled" % file_name)
return None
current_line += 1
last_z = current_position.z
if current_line % file_step == 0:
self._message.setProgress(math.floor(current_line / file_lines * 100))
Job.yieldThread()
if len(line) == 0:
continue
if line.find(self._type_keyword) == 0:
type = line[len(self._type_keyword):].strip()
if type == "WALL-INNER":
self._layer_type = LayerPolygon.InsetXType
elif type == "WALL-OUTER":
self._layer_type = LayerPolygon.Inset0Type
elif type == "SKIN":
self._layer_type = LayerPolygon.SkinType
elif type == "SKIRT":
self._layer_type = LayerPolygon.SkirtType
elif type == "SUPPORT":
self._layer_type = LayerPolygon.SupportType
elif type == "FILL":
self._layer_type = LayerPolygon.InfillType
else:
Logger.log("w", "Encountered a unknown type (%s) while parsing g-code.", type)
if self._is_layers_in_file and line[:len(self._layer_keyword)] == self._layer_keyword:
try:
layer_number = int(line[len(self._layer_keyword):])
self._createPolygon(self._current_layer_thickness, current_path, self._extruder_offsets.get(self._extruder_number, [0, 0]))
current_path.clear()
self._layer_number = layer_number
except:
pass
# This line is a comment. Ignore it (except for the layer_keyword)
if line.startswith(";"):
continue
G = self._getInt(line, "G")
if G is not None:
current_position = self._processGCode(G, line, current_position, current_path)
# < 2 is a heuristic for a movement only, that should not be counted as a layer
if current_position.z > last_z and abs(current_position.z - last_z) < 2:
if self._createPolygon(self._current_layer_thickness, current_path, self._extruder_offsets.get(self._extruder_number, [0, 0])):
current_path.clear()
if not self._is_layers_in_file:
self._layer_number += 1
continue
if line.startswith("T"):
T = self._getInt(line, "T")
if T is not None:
self._createPolygon(self._current_layer_thickness, current_path, self._extruder_offsets.get(self._extruder_number, [0, 0]))
current_path.clear()
current_position = self._processTCode(T, line, current_position, current_path)
# "Flush" leftovers
if not self._is_layers_in_file and len(current_path) > 1:
if self._createPolygon(self._current_layer_thickness, current_path, self._extruder_offsets.get(self._extruder_number, [0, 0])):
self._layer_number += 1
current_path.clear()
material_color_map = numpy.zeros((10, 4), dtype = numpy.float32)
material_color_map[0, :] = [0.0, 0.7, 0.9, 1.0]
material_color_map[1, :] = [0.7, 0.9, 0.0, 1.0]
layer_mesh = self._layer_data_builder.build(material_color_map)
decorator = LayerDataDecorator.LayerDataDecorator()
decorator.setLayerData(layer_mesh)
scene_node.addDecorator(decorator)
gcode_list_decorator = GCodeListDecorator()
gcode_list_decorator.setGCodeList(gcode_list)
scene_node.addDecorator(gcode_list_decorator)
Application.getInstance().getController().getScene().gcode_list = gcode_list
Logger.log("d", "Finished parsing %s" % file_name)
self._message.hide()
if self._layer_number == 0:
Logger.log("w", "File %s doesn't contain any valid layers" % file_name)
settings = Application.getInstance().getGlobalContainerStack()
machine_width = settings.getProperty("machine_width", "value")
machine_depth = settings.getProperty("machine_depth", "value")
if not self._center_is_zero:
scene_node.setPosition(Vector(-machine_width / 2, 0, machine_depth / 2))
Logger.log("d", "Loaded %s" % file_name)
if Preferences.getInstance().getValue("gcodereader/show_caution"):
caution_message = Message(catalog.i18nc(
"@info:generic",
"Make sure the g-code is suitable for your printer and printer configuration before sending the file to it. The g-code representation may not be accurate."),
lifetime=0,
title = catalog.i18nc("@info:title", "G-code Details"))
caution_message.show()
# The "save/print" button's state is bound to the backend state.
backend = Application.getInstance().getBackend()
backend.backendStateChange.emit(Backend.BackendState.Disabled)
return scene_node
return self._flavor_reader.processGCodeFile(file_name)

View file

@ -0,0 +1,10 @@
# Copyright (c) 2017 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from . import FlavorParser
# This parser is intented for interpret the Marlin/Sprinter Firmware flavor
class MarlinFlavorParser(FlavorParser.FlavorParser):
def __init__(self):
super().__init__()

View file

@ -0,0 +1,32 @@
# Copyright (c) 2017 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from . import FlavorParser
# This parser is intented for interpret the RepRap Firmware flavor
class RepRapFlavorParser(FlavorParser.FlavorParser):
def __init__(self):
super().__init__()
def processMCode(self, M, line, position, path):
if M == 82:
# Set absolute extrusion mode
self._is_absolute_extrusion = True
elif M == 83:
# Set relative extrusion mode
self._is_absolute_extrusion = False
## Set the absolute positioning
# RepRapFlavor code G90 sets position of X, Y, Z, and E to absolute
def _gCode90(self, position, params, path):
self._is_absolute_positioning = True
self._is_absolute_extrusion = True
return position
## Set the relative positioning
# RepRapFlavor code G91 sets position of X, Y, Z to relative
# For relative E, M83 is used
def _gCode91(self, position, params, path):
self._is_absolute_positioning = False
return position

View file

@ -74,12 +74,13 @@ class GCodeWriter(MeshWriter):
## Create a new container with container 2 as base and container 1 written over it.
def _createFlattenedContainerInstance(self, instance_container1, instance_container2):
flat_container = InstanceContainer(instance_container2.getName())
if instance_container1.getDefinition():
flat_container.setDefinition(instance_container1.getDefinition())
else:
flat_container.setDefinition(instance_container2.getDefinition())
# The metadata includes id, name and definition
flat_container.setMetaData(copy.deepcopy(instance_container2.getMetaData()))
if instance_container1.getDefinition():
flat_container.setDefinition(instance_container1.getDefinition().getId())
for key in instance_container2.getAllKeys():
flat_container.setProperty(key, "value", instance_container2.getProperty(key, "value"))

View file

@ -43,7 +43,7 @@ UM.Dialog
TextField {
id: peak_height
objectName: "Peak_Height"
validator: DoubleValidator {notation: DoubleValidator.StandardNotation; bottom: -500; top: 500;}
validator: RegExpValidator {regExp: /^-?\d{1,3}([\,|\.]\d*)?$/}
width: 180 * screenScaleFactor
onTextChanged: { manager.onPeakHeightChanged(text) }
}
@ -66,7 +66,7 @@ UM.Dialog
TextField {
id: base_height
objectName: "Base_Height"
validator: DoubleValidator {notation: DoubleValidator.StandardNotation; bottom: 0; top: 500;}
validator: RegExpValidator {regExp: /^\d{1,3}([\,|\.]\d*)?$/}
width: 180 * screenScaleFactor
onTextChanged: { manager.onBaseHeightChanged(text) }
}
@ -90,7 +90,7 @@ UM.Dialog
id: width
objectName: "Width"
focus: true
validator: DoubleValidator {notation: DoubleValidator.StandardNotation; bottom: 1; top: 500;}
validator: RegExpValidator {regExp: /^[1-9]\d{0,2}([\,|\.]\d*)?$/}
width: 180 * screenScaleFactor
onTextChanged: { manager.onWidthChanged(text) }
}
@ -113,7 +113,7 @@ UM.Dialog
id: depth
objectName: "Depth"
focus: true
validator: DoubleValidator {notation: DoubleValidator.StandardNotation; bottom: 1; top: 500;}
validator: RegExpValidator {regExp: /^[1-9]\d{0,2}([\,|\.]\d*)?$/}
width: 180 * screenScaleFactor
onTextChanged: { manager.onDepthChanged(text) }
}

View file

@ -4,8 +4,7 @@
import os
import threading
from PyQt5.QtCore import Qt, QUrl, pyqtSignal, QObject
from PyQt5.QtQml import QQmlComponent, QQmlContext
from PyQt5.QtCore import Qt, pyqtSignal, QObject
from UM.FlameProfiler import pyqtSlot
from UM.Application import Application
from UM.PluginRegistry import PluginRegistry
@ -81,14 +80,9 @@ class ImageReaderUI(QObject):
def _createConfigUI(self):
if self._ui_view is None:
Logger.log("d", "Creating ImageReader config UI")
path = QUrl.fromLocalFile(os.path.join(PluginRegistry.getInstance().getPluginPath("ImageReader"), "ConfigUI.qml"))
component = QQmlComponent(Application.getInstance()._engine, path)
self._ui_context = QQmlContext(Application.getInstance()._engine.rootContext())
self._ui_context.setContextProperty("manager", self)
self._ui_view = component.create(self._ui_context)
path = os.path.join(PluginRegistry.getInstance().getPluginPath("ImageReader"), "ConfigUI.qml")
self._ui_view = Application.getInstance().createQmlComponent(path, {"manager": self})
self._ui_view.setFlags(self._ui_view.flags() & ~Qt.WindowCloseButtonHint & ~Qt.WindowMinimizeButtonHint & ~Qt.WindowMaximizeButtonHint);
self._disable_size_callbacks = False
@pyqtSlot()
@ -107,7 +101,7 @@ class ImageReaderUI(QObject):
def onWidthChanged(self, value):
if self._ui_view and not self._disable_size_callbacks:
if len(value) > 0:
self._width = float(value)
self._width = float(value.replace(",", "."))
else:
self._width = 0
@ -120,7 +114,7 @@ class ImageReaderUI(QObject):
def onDepthChanged(self, value):
if self._ui_view and not self._disable_size_callbacks:
if len(value) > 0:
self._depth = float(value)
self._depth = float(value.replace(",", "."))
else:
self._depth = 0
@ -132,14 +126,14 @@ class ImageReaderUI(QObject):
@pyqtSlot(str)
def onBaseHeightChanged(self, value):
if (len(value) > 0):
self.base_height = float(value)
self.base_height = float(value.replace(",", "."))
else:
self.base_height = 0
@pyqtSlot(str)
def onPeakHeightChanged(self, value):
if (len(value) > 0):
self.peak_height = float(value)
self.peak_height = float(value.replace(",", "."))
else:
self.peak_height = 0
@ -149,7 +143,4 @@ class ImageReaderUI(QObject):
@pyqtSlot(int)
def onImageColorInvertChanged(self, value):
if (value == 1):
self.image_color_invert = True
else:
self.image_color_invert = False
self.image_color_invert = (value == 1)

View file

@ -1,113 +0,0 @@
# Copyright (c) 2016 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
from UM.Resources import Resources
from UM.Scene.SceneNode import SceneNode
from UM.Scene.ToolHandle import ToolHandle
from UM.Application import Application
from UM.PluginRegistry import PluginRegistry
from UM.View.RenderPass import RenderPass
from UM.View.RenderBatch import RenderBatch
from UM.View.GL.OpenGL import OpenGL
from cura.Settings.ExtruderManager import ExtruderManager
import os.path
## RenderPass used to display g-code paths.
class LayerPass(RenderPass):
def __init__(self, width, height):
super().__init__("layerview", width, height)
self._layer_shader = None
self._tool_handle_shader = None
self._gl = OpenGL.getInstance().getBindingsObject()
self._scene = Application.getInstance().getController().getScene()
self._extruder_manager = ExtruderManager.getInstance()
self._layer_view = None
self._compatibility_mode = None
def setLayerView(self, layerview):
self._layer_view = layerview
self._compatibility_mode = layerview.getCompatibilityMode()
def render(self):
if not self._layer_shader:
if self._compatibility_mode:
shader_filename = "layers.shader"
else:
shader_filename = "layers3d.shader"
self._layer_shader = OpenGL.getInstance().createShaderProgram(os.path.join(PluginRegistry.getInstance().getPluginPath("LayerView"), shader_filename))
# Use extruder 0 if the extruder manager reports extruder index -1 (for single extrusion printers)
self._layer_shader.setUniformValue("u_active_extruder", float(max(0, self._extruder_manager.activeExtruderIndex)))
if self._layer_view:
self._layer_shader.setUniformValue("u_layer_view_type", self._layer_view.getLayerViewType())
self._layer_shader.setUniformValue("u_extruder_opacity", self._layer_view.getExtruderOpacities())
self._layer_shader.setUniformValue("u_show_travel_moves", self._layer_view.getShowTravelMoves())
self._layer_shader.setUniformValue("u_show_helpers", self._layer_view.getShowHelpers())
self._layer_shader.setUniformValue("u_show_skin", self._layer_view.getShowSkin())
self._layer_shader.setUniformValue("u_show_infill", self._layer_view.getShowInfill())
else:
#defaults
self._layer_shader.setUniformValue("u_layer_view_type", 1)
self._layer_shader.setUniformValue("u_extruder_opacity", [1, 1, 1, 1])
self._layer_shader.setUniformValue("u_show_travel_moves", 0)
self._layer_shader.setUniformValue("u_show_helpers", 1)
self._layer_shader.setUniformValue("u_show_skin", 1)
self._layer_shader.setUniformValue("u_show_infill", 1)
if not self._tool_handle_shader:
self._tool_handle_shader = OpenGL.getInstance().createShaderProgram(Resources.getPath(Resources.Shaders, "toolhandle.shader"))
self.bind()
tool_handle_batch = RenderBatch(self._tool_handle_shader, type = RenderBatch.RenderType.Overlay)
for node in DepthFirstIterator(self._scene.getRoot()):
if isinstance(node, ToolHandle):
tool_handle_batch.addItem(node.getWorldTransformation(), mesh = node.getSolidMesh())
elif isinstance(node, SceneNode) and (node.getMeshData() or node.callDecoration("isBlockSlicing")) and node.isVisible():
layer_data = node.callDecoration("getLayerData")
if not layer_data:
continue
# Render all layers below a certain number as line mesh instead of vertices.
if self._layer_view._current_layer_num > -1 and ((not self._layer_view._only_show_top_layers) or (not self._layer_view.getCompatibilityMode())):
start = 0
end = 0
element_counts = layer_data.getElementCounts()
for layer in sorted(element_counts.keys()):
if layer > self._layer_view._current_layer_num:
break
if self._layer_view._minimum_layer_num > layer:
start += element_counts[layer]
end += element_counts[layer]
# This uses glDrawRangeElements internally to only draw a certain range of lines.
batch = RenderBatch(self._layer_shader, type = RenderBatch.RenderType.Solid, mode = RenderBatch.RenderMode.Lines, range = (start, end))
batch.addItem(node.getWorldTransformation(), layer_data)
batch.render(self._scene.getActiveCamera())
# Create a new batch that is not range-limited
batch = RenderBatch(self._layer_shader, type = RenderBatch.RenderType.Solid)
if self._layer_view.getCurrentLayerMesh():
batch.addItem(node.getWorldTransformation(), self._layer_view.getCurrentLayerMesh())
if self._layer_view.getCurrentLayerJumps():
batch.addItem(node.getWorldTransformation(), self._layer_view.getCurrentLayerJumps())
if len(batch.items) > 0:
batch.render(self._scene.getActiveCamera())
# Render toolhandles on top of the layerview
if len(tool_handle_batch.items) > 0:
tool_handle_batch.render(self._scene.getActiveCamera())
self.release()

View file

@ -1,388 +0,0 @@
// Copyright (c) 2017 Ultimaker B.V.
// Cura is released under the terms of the LGPLv3 or higher.
import QtQuick 2.4
import QtQuick.Controls 1.2
import QtQuick.Layouts 1.1
import QtQuick.Controls.Styles 1.1
import UM 1.0 as UM
import Cura 1.0 as Cura
Item
{
id: base
width: {
if (UM.LayerView.compatibilityMode) {
return UM.Theme.getSize("layerview_menu_size_compatibility").width;
} else {
return UM.Theme.getSize("layerview_menu_size").width;
}
}
height: {
if (UM.LayerView.compatibilityMode) {
return UM.Theme.getSize("layerview_menu_size_compatibility").height;
} else if (UM.Preferences.getValue("layerview/layer_view_type") == 0) {
return UM.Theme.getSize("layerview_menu_size_material_color_mode").height + UM.LayerView.extruderCount * (UM.Theme.getSize("layerview_row").height + UM.Theme.getSize("layerview_row_spacing").height)
} else {
return UM.Theme.getSize("layerview_menu_size").height + UM.LayerView.extruderCount * (UM.Theme.getSize("layerview_row").height + UM.Theme.getSize("layerview_row_spacing").height)
}
}
property var buttonTarget: {
if(parent != null)
{
var force_binding = parent.y; // ensure this gets reevaluated when the panel moves
return base.mapFromItem(parent.parent, parent.buttonTarget.x, parent.buttonTarget.y)
}
return Qt.point(0,0)
}
visible: parent != null ? !parent.parent.monitoringPrint: true
UM.PointingRectangle {
id: layerViewMenu
anchors.right: parent.right
anchors.top: parent.top
width: parent.width
height: parent.height
z: slider.z - 1
color: UM.Theme.getColor("tool_panel_background")
borderWidth: UM.Theme.getSize("default_lining").width
borderColor: UM.Theme.getColor("lining")
arrowSize: 0 // hide arrow until weird issue with first time rendering is fixed
ColumnLayout {
id: view_settings
property var extruder_opacities: UM.Preferences.getValue("layerview/extruder_opacities").split("|")
property bool show_travel_moves: UM.Preferences.getValue("layerview/show_travel_moves")
property bool show_helpers: UM.Preferences.getValue("layerview/show_helpers")
property bool show_skin: UM.Preferences.getValue("layerview/show_skin")
property bool show_infill: UM.Preferences.getValue("layerview/show_infill")
// if we are in compatibility mode, we only show the "line type"
property bool show_legend: UM.LayerView.compatibilityMode ? 1 : UM.Preferences.getValue("layerview/layer_view_type") == 1
property bool only_show_top_layers: UM.Preferences.getValue("view/only_show_top_layers")
property int top_layer_count: UM.Preferences.getValue("view/top_layer_count")
anchors.top: parent.top
anchors.topMargin: UM.Theme.getSize("default_margin").height
anchors.left: parent.left
anchors.leftMargin: UM.Theme.getSize("default_margin").width
spacing: UM.Theme.getSize("layerview_row_spacing").height
anchors.right: parent.right
anchors.rightMargin: UM.Theme.getSize("default_margin").width
Label
{
id: layerViewTypesLabel
anchors.left: parent.left
text: catalog.i18nc("@label","Color scheme")
font: UM.Theme.getFont("default");
visible: !UM.LayerView.compatibilityMode
Layout.fillWidth: true
color: UM.Theme.getColor("setting_control_text")
}
ListModel // matches LayerView.py
{
id: layerViewTypes
}
Component.onCompleted:
{
layerViewTypes.append({
text: catalog.i18nc("@label:listbox", "Material Color"),
type_id: 0
})
layerViewTypes.append({
text: catalog.i18nc("@label:listbox", "Line Type"),
type_id: 1 // these ids match the switching in the shader
})
}
ComboBox
{
id: layerTypeCombobox
anchors.left: parent.left
Layout.fillWidth: true
Layout.preferredWidth: UM.Theme.getSize("layerview_row").width
model: layerViewTypes
visible: !UM.LayerView.compatibilityMode
style: UM.Theme.styles.combobox
anchors.right: parent.right
anchors.rightMargin: 10 * screenScaleFactor
onActivated:
{
UM.Preferences.setValue("layerview/layer_view_type", index);
}
Component.onCompleted:
{
currentIndex = UM.LayerView.compatibilityMode ? 1 : UM.Preferences.getValue("layerview/layer_view_type");
updateLegends(currentIndex);
}
function updateLegends(type_id)
{
// update visibility of legends
view_settings.show_legend = UM.LayerView.compatibilityMode || (type_id == 1);
}
}
Label
{
id: compatibilityModeLabel
anchors.left: parent.left
text: catalog.i18nc("@label","Compatibility Mode")
font: UM.Theme.getFont("default")
color: UM.Theme.getColor("text")
visible: UM.LayerView.compatibilityMode
Layout.fillWidth: true
Layout.preferredHeight: UM.Theme.getSize("layerview_row").height
Layout.preferredWidth: UM.Theme.getSize("layerview_row").width
}
Label
{
id: space2Label
anchors.left: parent.left
text: " "
font.pointSize: 0.5
}
Connections {
target: UM.Preferences
onPreferenceChanged:
{
layerTypeCombobox.currentIndex = UM.LayerView.compatibilityMode ? 1 : UM.Preferences.getValue("layerview/layer_view_type");
layerTypeCombobox.updateLegends(layerTypeCombobox.currentIndex);
view_settings.extruder_opacities = UM.Preferences.getValue("layerview/extruder_opacities").split("|");
view_settings.show_travel_moves = UM.Preferences.getValue("layerview/show_travel_moves");
view_settings.show_helpers = UM.Preferences.getValue("layerview/show_helpers");
view_settings.show_skin = UM.Preferences.getValue("layerview/show_skin");
view_settings.show_infill = UM.Preferences.getValue("layerview/show_infill");
view_settings.only_show_top_layers = UM.Preferences.getValue("view/only_show_top_layers");
view_settings.top_layer_count = UM.Preferences.getValue("view/top_layer_count");
}
}
Repeater {
model: Cura.ExtrudersModel{}
CheckBox {
id: extrudersModelCheckBox
checked: view_settings.extruder_opacities[index] > 0.5 || view_settings.extruder_opacities[index] == undefined || view_settings.extruder_opacities[index] == ""
onClicked: {
view_settings.extruder_opacities[index] = checked ? 1.0 : 0.0
UM.Preferences.setValue("layerview/extruder_opacities", view_settings.extruder_opacities.join("|"));
}
visible: !UM.LayerView.compatibilityMode
enabled: index + 1 <= 4
Rectangle {
anchors.verticalCenter: parent.verticalCenter
anchors.right: extrudersModelCheckBox.right
anchors.rightMargin: UM.Theme.getSize("default_margin").width
width: UM.Theme.getSize("layerview_legend_size").width
height: UM.Theme.getSize("layerview_legend_size").height
color: model.color
radius: width / 2
border.width: UM.Theme.getSize("default_lining").width
border.color: UM.Theme.getColor("lining")
visible: !view_settings.show_legend
}
Layout.fillWidth: true
Layout.preferredHeight: UM.Theme.getSize("layerview_row").height + UM.Theme.getSize("default_lining").height
Layout.preferredWidth: UM.Theme.getSize("layerview_row").width
style: UM.Theme.styles.checkbox
Label
{
text: model.name
elide: Text.ElideRight
color: UM.Theme.getColor("setting_control_text")
font: UM.Theme.getFont("default")
anchors.verticalCenter: parent.verticalCenter
anchors.left: extrudersModelCheckBox.left;
anchors.right: extrudersModelCheckBox.right;
anchors.leftMargin: UM.Theme.getSize("checkbox").width + UM.Theme.getSize("default_margin").width /2
anchors.rightMargin: UM.Theme.getSize("default_margin").width * 2
}
}
}
Repeater {
model: ListModel {
id: typesLegenModel
Component.onCompleted:
{
typesLegenModel.append({
label: catalog.i18nc("@label", "Show Travels"),
initialValue: view_settings.show_travel_moves,
preference: "layerview/show_travel_moves",
colorId: "layerview_move_combing"
});
typesLegenModel.append({
label: catalog.i18nc("@label", "Show Helpers"),
initialValue: view_settings.show_helpers,
preference: "layerview/show_helpers",
colorId: "layerview_support"
});
typesLegenModel.append({
label: catalog.i18nc("@label", "Show Shell"),
initialValue: view_settings.show_skin,
preference: "layerview/show_skin",
colorId: "layerview_inset_0"
});
typesLegenModel.append({
label: catalog.i18nc("@label", "Show Infill"),
initialValue: view_settings.show_infill,
preference: "layerview/show_infill",
colorId: "layerview_infill"
});
}
}
CheckBox {
id: legendModelCheckBox
checked: model.initialValue
onClicked: {
UM.Preferences.setValue(model.preference, checked);
}
Rectangle {
anchors.verticalCenter: parent.verticalCenter
anchors.right: legendModelCheckBox.right
anchors.rightMargin: UM.Theme.getSize("default_margin").width
width: UM.Theme.getSize("layerview_legend_size").width
height: UM.Theme.getSize("layerview_legend_size").height
color: UM.Theme.getColor(model.colorId)
border.width: UM.Theme.getSize("default_lining").width
border.color: UM.Theme.getColor("lining")
visible: view_settings.show_legend
}
Layout.fillWidth: true
Layout.preferredHeight: UM.Theme.getSize("layerview_row").height + UM.Theme.getSize("default_lining").height
Layout.preferredWidth: UM.Theme.getSize("layerview_row").width
style: UM.Theme.styles.checkbox
Label
{
text: label
font: UM.Theme.getFont("default")
elide: Text.ElideRight
color: UM.Theme.getColor("setting_control_text")
anchors.verticalCenter: parent.verticalCenter
anchors.left: legendModelCheckBox.left;
anchors.right: legendModelCheckBox.right;
anchors.leftMargin: UM.Theme.getSize("checkbox").width + UM.Theme.getSize("default_margin").width /2
anchors.rightMargin: UM.Theme.getSize("default_margin").width * 2
}
}
}
CheckBox {
checked: view_settings.only_show_top_layers
onClicked: {
UM.Preferences.setValue("view/only_show_top_layers", checked ? 1.0 : 0.0);
}
text: catalog.i18nc("@label", "Only Show Top Layers")
visible: UM.LayerView.compatibilityMode
style: UM.Theme.styles.checkbox
}
CheckBox {
checked: view_settings.top_layer_count == 5
onClicked: {
UM.Preferences.setValue("view/top_layer_count", checked ? 5 : 1);
}
text: catalog.i18nc("@label", "Show 5 Detailed Layers On Top")
visible: UM.LayerView.compatibilityMode
style: UM.Theme.styles.checkbox
}
Repeater {
model: ListModel {
id: typesLegenModelNoCheck
Component.onCompleted:
{
typesLegenModelNoCheck.append({
label: catalog.i18nc("@label", "Top / Bottom"),
colorId: "layerview_skin",
});
typesLegenModelNoCheck.append({
label: catalog.i18nc("@label", "Inner Wall"),
colorId: "layerview_inset_x",
});
}
}
Label {
text: label
visible: view_settings.show_legend
id: typesLegendModelLabel
Rectangle {
anchors.verticalCenter: parent.verticalCenter
anchors.right: typesLegendModelLabel.right
anchors.rightMargin: UM.Theme.getSize("default_margin").width
width: UM.Theme.getSize("layerview_legend_size").width
height: UM.Theme.getSize("layerview_legend_size").height
color: UM.Theme.getColor(model.colorId)
border.width: UM.Theme.getSize("default_lining").width
border.color: UM.Theme.getColor("lining")
visible: view_settings.show_legend
}
Layout.fillWidth: true
Layout.preferredHeight: UM.Theme.getSize("layerview_row").height + UM.Theme.getSize("default_lining").height
Layout.preferredWidth: UM.Theme.getSize("layerview_row").width
color: UM.Theme.getColor("setting_control_text")
font: UM.Theme.getFont("default")
}
}
}
LayerSlider {
id: slider
width: UM.Theme.getSize("slider_handle").width
height: UM.Theme.getSize("layerview_menu_size").height
anchors {
top: parent.bottom
topMargin: UM.Theme.getSize("slider_layerview_margin").height
right: layerViewMenu.right
rightMargin: UM.Theme.getSize("slider_layerview_margin").width
}
// custom properties
upperValue: UM.LayerView.currentLayer
lowerValue: UM.LayerView.minimumLayer
maximumValue: UM.LayerView.numLayers
handleSize: UM.Theme.getSize("slider_handle").width
trackThickness: UM.Theme.getSize("slider_groove").width
trackColor: UM.Theme.getColor("slider_groove")
trackBorderColor: UM.Theme.getColor("slider_groove_border")
upperHandleColor: UM.Theme.getColor("slider_handle")
lowerHandleColor: UM.Theme.getColor("slider_handle")
rangeHandleColor: UM.Theme.getColor("slider_groove_fill")
handleLabelWidth: UM.Theme.getSize("slider_layerview_background").width
layersVisible: UM.LayerView.layerActivity && CuraApplication.platformActivity ? true : false
// update values when layer data changes
Connections {
target: UM.LayerView
onMaxLayersChanged: slider.setUpperValue(UM.LayerView.currentLayer)
onMinimumLayerChanged: slider.setLowerValue(UM.LayerView.minimumLayer)
onCurrentLayerChanged: slider.setUpperValue(UM.LayerView.currentLayer)
}
// make sure the slider handlers show the correct value after switching views
Component.onCompleted: {
slider.setLowerValue(UM.LayerView.minimumLayer)
slider.setUpperValue(UM.LayerView.currentLayer)
}
}
}
FontMetrics {
id: fontMetrics
font: UM.Theme.getFont("default")
}
}

View file

@ -1,151 +0,0 @@
from PyQt5.QtCore import QObject, pyqtSignal, pyqtProperty
from UM.FlameProfiler import pyqtSlot
from UM.Application import Application
import LayerView
class LayerViewProxy(QObject):
def __init__(self, parent=None):
super().__init__(parent)
self._current_layer = 0
self._controller = Application.getInstance().getController()
self._controller.activeViewChanged.connect(self._onActiveViewChanged)
self._onActiveViewChanged()
currentLayerChanged = pyqtSignal()
maxLayersChanged = pyqtSignal()
activityChanged = pyqtSignal()
globalStackChanged = pyqtSignal()
preferencesChanged = pyqtSignal()
busyChanged = pyqtSignal()
@pyqtProperty(bool, notify=activityChanged)
def layerActivity(self):
active_view = self._controller.getActiveView()
if type(active_view) == LayerView.LayerView.LayerView:
return active_view.getActivity()
@pyqtProperty(int, notify=maxLayersChanged)
def numLayers(self):
active_view = self._controller.getActiveView()
if type(active_view) == LayerView.LayerView.LayerView:
return active_view.getMaxLayers()
@pyqtProperty(int, notify=currentLayerChanged)
def currentLayer(self):
active_view = self._controller.getActiveView()
if type(active_view) == LayerView.LayerView.LayerView:
return active_view.getCurrentLayer()
@pyqtProperty(int, notify=currentLayerChanged)
def minimumLayer(self):
active_view = self._controller.getActiveView()
if type(active_view) == LayerView.LayerView.LayerView:
return active_view.getMinimumLayer()
@pyqtProperty(bool, notify=busyChanged)
def busy(self):
active_view = self._controller.getActiveView()
if type(active_view) == LayerView.LayerView.LayerView:
return active_view.isBusy()
return False
@pyqtProperty(bool, notify=preferencesChanged)
def compatibilityMode(self):
active_view = self._controller.getActiveView()
if type(active_view) == LayerView.LayerView.LayerView:
return active_view.getCompatibilityMode()
return False
@pyqtSlot(int)
def setCurrentLayer(self, layer_num):
active_view = self._controller.getActiveView()
if type(active_view) == LayerView.LayerView.LayerView:
active_view.setLayer(layer_num)
@pyqtSlot(int)
def setMinimumLayer(self, layer_num):
active_view = self._controller.getActiveView()
if type(active_view) == LayerView.LayerView.LayerView:
active_view.setMinimumLayer(layer_num)
@pyqtSlot(int)
def setLayerViewType(self, layer_view_type):
active_view = self._controller.getActiveView()
if type(active_view) == LayerView.LayerView.LayerView:
active_view.setLayerViewType(layer_view_type)
@pyqtSlot(result=int)
def getLayerViewType(self):
active_view = self._controller.getActiveView()
if type(active_view) == LayerView.LayerView.LayerView:
return active_view.getLayerViewType()
return 0
# Opacity 0..1
@pyqtSlot(int, float)
def setExtruderOpacity(self, extruder_nr, opacity):
active_view = self._controller.getActiveView()
if type(active_view) == LayerView.LayerView.LayerView:
active_view.setExtruderOpacity(extruder_nr, opacity)
@pyqtSlot(int)
def setShowTravelMoves(self, show):
active_view = self._controller.getActiveView()
if type(active_view) == LayerView.LayerView.LayerView:
active_view.setShowTravelMoves(show)
@pyqtSlot(int)
def setShowHelpers(self, show):
active_view = self._controller.getActiveView()
if type(active_view) == LayerView.LayerView.LayerView:
active_view.setShowHelpers(show)
@pyqtSlot(int)
def setShowSkin(self, show):
active_view = self._controller.getActiveView()
if type(active_view) == LayerView.LayerView.LayerView:
active_view.setShowSkin(show)
@pyqtSlot(int)
def setShowInfill(self, show):
active_view = self._controller.getActiveView()
if type(active_view) == LayerView.LayerView.LayerView:
active_view.setShowInfill(show)
@pyqtProperty(int, notify=globalStackChanged)
def extruderCount(self):
active_view = self._controller.getActiveView()
if type(active_view) == LayerView.LayerView.LayerView:
return active_view.getExtruderCount()
return 0
def _layerActivityChanged(self):
self.activityChanged.emit()
def _onLayerChanged(self):
self.currentLayerChanged.emit()
self._layerActivityChanged()
def _onMaxLayersChanged(self):
self.maxLayersChanged.emit()
def _onBusyChanged(self):
self.busyChanged.emit()
def _onGlobalStackChanged(self):
self.globalStackChanged.emit()
def _onPreferencesChanged(self):
self.preferencesChanged.emit()
def _onActiveViewChanged(self):
active_view = self._controller.getActiveView()
if type(active_view) == LayerView.LayerView.LayerView:
active_view.currentLayerNumChanged.connect(self._onLayerChanged)
active_view.maxLayersChanged.connect(self._onMaxLayersChanged)
active_view.busyChanged.connect(self._onBusyChanged)
active_view.globalStackChanged.connect(self._onGlobalStackChanged)
active_view.preferencesChanged.connect(self._onPreferencesChanged)

View file

@ -1,25 +0,0 @@
# Copyright (c) 2015 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from . import LayerView, LayerViewProxy
from PyQt5.QtQml import qmlRegisterType, qmlRegisterSingletonType
from UM.i18n import i18nCatalog
catalog = i18nCatalog("cura")
def getMetaData():
return {
"view": {
"name": catalog.i18nc("@item:inlistbox", "Layer view"),
"view_panel": "LayerView.qml",
"weight": 2
}
}
def createLayerViewProxy(engine, script_engine):
return LayerViewProxy.LayerViewProxy()
def register(app):
layer_view = LayerView.LayerView()
qmlRegisterSingletonType(LayerViewProxy.LayerViewProxy, "UM", 1, 0, "LayerView", layer_view.getProxy)
return { "view": LayerView.LayerView() }

View file

@ -2,6 +2,7 @@
# Cura is released under the terms of the LGPLv3 or higher.
import configparser # For reading the legacy profile INI files.
import io
import json # For reading the Dictionary of Doom.
import math # For mathematical operations included in the Dictionary of Doom.
import os.path # For concatenating the path to the plugin and the relative path to the Dictionary of Doom.
@ -80,8 +81,7 @@ class LegacyProfileReader(ProfileReader):
parser = configparser.ConfigParser(interpolation = None)
try:
with open(file_name) as f:
parser.readfp(f) # Parse the INI file.
parser.read([file_name]) # Parse the INI file.
except Exception as e:
Logger.log("e", "Unable to open legacy profile %s: %s", file_name, str(e))
return None
@ -121,7 +121,7 @@ class LegacyProfileReader(ProfileReader):
Logger.log("e", "Dictionary of Doom has no translation. Is it the correct JSON file?")
return None
current_printer_definition = global_container_stack.getBottom()
profile.setDefinition(current_printer_definition)
profile.setDefinition(current_printer_definition.getId())
for new_setting in dict_of_doom["translation"]: # Evaluate all new settings that would get a value from the translations.
old_setting_expression = dict_of_doom["translation"][new_setting]
compiled = compile(old_setting_expression, new_setting, "eval")
@ -138,7 +138,25 @@ class LegacyProfileReader(ProfileReader):
if len(profile.getAllKeys()) == 0:
Logger.log("i", "A legacy profile was imported but everything evaluates to the defaults, creating an empty profile.")
profile.setDirty(True)
profile.addMetaDataEntry("type", "quality_changes")
# We need to downgrade the container to version 1 (in Cura 2.1) so the upgrade system can correctly upgrade
# it to the latest version.
profile.addMetaDataEntry("type", "profile")
# don't know what quality_type it is based on, so use "normal" by default
profile.addMetaDataEntry("quality_type", "normal")
return profile
profile.setDirty(True)
parser = configparser.ConfigParser(interpolation=None)
data = profile.serialize()
parser.read_string(data)
parser["general"]["version"] = "1"
if parser.has_section("values"):
parser["settings"] = parser["values"]
del parser["values"]
stream = io.StringIO()
parser.write(stream)
data = stream.getvalue()
profile.deserialize(data)
return profile

View file

@ -116,8 +116,7 @@ class MachineSettingsAction(MachineAction):
@pyqtSlot(int)
def setMachineExtruderCount(self, extruder_count):
machine_manager = Application.getInstance().getMachineManager()
extruder_manager = ExtruderManager.getInstance()
extruder_manager = Application.getInstance().getExtruderManager()
definition_changes_container = self._global_container_stack.definitionChanges
if not self._global_container_stack or definition_changes_container == self._empty_container:
@ -127,34 +126,6 @@ class MachineSettingsAction(MachineAction):
if extruder_count == previous_extruder_count:
return
extruder_material_id = None
extruder_variant_id = None
if extruder_count == 1:
# Get the material and variant of the first extruder before setting the number extruders to 1
if machine_manager.hasMaterials:
extruder_material_id = machine_manager.allActiveMaterialIds[extruder_manager.extruderIds["0"]]
if machine_manager.hasVariants:
extruder_variant_id = machine_manager.allActiveVariantIds[extruder_manager.extruderIds["0"]]
# Copy any settable_per_extruder setting value from the extruders to the global stack
extruder_stacks = ExtruderManager.getInstance().getActiveExtruderStacks()
extruder_stacks.reverse() # make sure the first extruder is done last, so its settings override any higher extruder settings
global_user_container = self._global_container_stack.getTop()
for extruder_stack in extruder_stacks:
extruder_index = extruder_stack.getMetaDataEntry("position")
extruder_user_container = extruder_stack.getTop()
for setting_instance in extruder_user_container.findInstances():
setting_key = setting_instance.definition.key
settable_per_extruder = self._global_container_stack.getProperty(setting_key, "settable_per_extruder")
if settable_per_extruder:
limit_to_extruder = self._global_container_stack.getProperty(setting_key, "limit_to_extruder")
if limit_to_extruder == "-1" or limit_to_extruder == extruder_index:
global_user_container.setProperty(setting_key, "value", extruder_user_container.getProperty(setting_key, "value"))
extruder_user_container.removeInstance(setting_key)
# reset all extruder number settings whose value is no longer valid
for setting_instance in self._global_container_stack.userChanges.findInstances():
setting_key = setting_instance.definition.key
@ -177,52 +148,29 @@ class MachineSettingsAction(MachineAction):
definition_changes_container.setProperty("machine_extruder_count", "value", extruder_count)
if extruder_count > 1:
# Multiextrusion
# Make sure one of the extruder stacks is active
extruder_manager.setActiveExtruderIndex(0)
# Make sure one of the extruder stacks is active
if extruder_manager.activeExtruderIndex == -1:
extruder_manager.setActiveExtruderIndex(0)
# Move settable_per_extruder values out of the global container
# After CURA-4482 this should not be the case anymore, but we still want to support older project files.
global_user_container = self._global_container_stack.getTop()
# Move settable_per_extruder values out of the global container
if previous_extruder_count == 1:
extruder_stacks = ExtruderManager.getInstance().getActiveExtruderStacks()
global_user_container = self._global_container_stack.getTop()
if previous_extruder_count == 1:
extruder_stacks = ExtruderManager.getInstance().getActiveExtruderStacks()
global_user_container = self._global_container_stack.getTop()
for setting_instance in global_user_container.findInstances():
setting_key = setting_instance.definition.key
settable_per_extruder = self._global_container_stack.getProperty(setting_key, "settable_per_extruder")
if settable_per_extruder:
limit_to_extruder = int(self._global_container_stack.getProperty(setting_key, "limit_to_extruder"))
extruder_stack = extruder_stacks[max(0, limit_to_extruder)]
extruder_stack.getTop().setProperty(setting_key, "value", global_user_container.getProperty(setting_key, "value"))
global_user_container.removeInstance(setting_key)
else:
# Single extrusion
for setting_instance in global_user_container.findInstances():
setting_key = setting_instance.definition.key
settable_per_extruder = self._global_container_stack.getProperty(setting_key, "settable_per_extruder")
# Make sure the machine stack is active
if extruder_manager.activeExtruderIndex > -1:
extruder_manager.setActiveExtruderIndex(-1)
# Restore material and variant on global stack
# MachineManager._onGlobalContainerChanged removes the global material and variant of multiextruder machines
if extruder_material_id or extruder_variant_id:
# Prevent the DiscardOrKeepProfileChangesDialog from popping up (twice) if there are user changes
# The dialog is not relevant here, since we're restoring the previous situation as good as possible
preferences = Preferences.getInstance()
choice_on_profile_override = preferences.getValue("cura/choice_on_profile_override")
preferences.setValue("cura/choice_on_profile_override", "always_keep")
if extruder_material_id:
machine_manager.setActiveMaterial(extruder_material_id)
if extruder_variant_id:
machine_manager.setActiveVariant(extruder_variant_id)
preferences.setValue("cura/choice_on_profile_override", choice_on_profile_override)
if settable_per_extruder:
limit_to_extruder = int(self._global_container_stack.getProperty(setting_key, "limit_to_extruder"))
extruder_stack = extruder_stacks[max(0, limit_to_extruder)]
extruder_stack.getTop().setProperty(setting_key, "value", global_user_container.getProperty(setting_key, "value"))
global_user_container.removeInstance(setting_key)
self.forceUpdate()
@pyqtSlot()
def forceUpdate(self):
# Force rebuilding the build volume by reloading the global container stack.
@ -240,29 +188,32 @@ class MachineSettingsAction(MachineAction):
# In other words: only continue for the UM2 (extended), but not for the UM2+
return
stacks = ExtruderManager.getInstance().getExtruderStacks()
has_materials = self._global_container_stack.getProperty("machine_gcode_flavor", "value") != "UltiGCode"
material_container = self._global_container_stack.material
if has_materials:
if "has_materials" in self._global_container_stack.getMetaData():
self._global_container_stack.setMetaDataEntry("has_materials", True)
else:
self._global_container_stack.addMetaDataEntry("has_materials", True)
# Set the material container to a sane default
if material_container == self._empty_container:
search_criteria = { "type": "material", "definition": "fdmprinter", "id": self._global_container_stack.getMetaDataEntry("preferred_material")}
materials = self._container_registry.findInstanceContainers(**search_criteria)
if materials:
self._global_container_stack.material = materials[0]
# Set the material container for each extruder to a sane default
for stack in stacks:
material_container = stack.material
if material_container == self._empty_container:
machine_approximate_diameter = str(round(self._global_container_stack.getProperty("material_diameter", "value")))
search_criteria = { "type": "material", "definition": "fdmprinter", "id": self._global_container_stack.getMetaDataEntry("preferred_material"), "approximate_diameter": machine_approximate_diameter}
materials = self._container_registry.findInstanceContainers(**search_criteria)
if materials:
stack.material = materials[0]
else:
# The metadata entry is stored in an ini, and ini files are parsed as strings only.
# Because any non-empty string evaluates to a boolean True, we have to remove the entry to make it False.
if "has_materials" in self._global_container_stack.getMetaData():
self._global_container_stack.removeMetaDataEntry("has_materials")
self._global_container_stack.material = ContainerRegistry.getInstance().getEmptyInstanceContainer()
for stack in stacks:
stack.material = ContainerRegistry.getInstance().getEmptyInstanceContainer()
Application.getInstance().globalContainerStackChanged.emit()
@ -275,16 +226,13 @@ class MachineSettingsAction(MachineAction):
if not self._global_container_stack.getMetaDataEntry("has_materials", False):
return
machine_extruder_count = self._global_container_stack.getProperty("machine_extruder_count", "value")
if machine_extruder_count > 1:
material = ExtruderManager.getInstance().getActiveExtruderStack().material
else:
material = self._global_container_stack.material
material = ExtruderManager.getInstance().getActiveExtruderStack().material
material_diameter = material.getProperty("material_diameter", "value")
if not material_diameter: # in case of "empty" material
if not material_diameter:
# in case of "empty" material
material_diameter = 0
material_approximate_diameter = str(round(material_diameter))
material_approximate_diameter = str(round(material_diameter))
definition_changes = self._global_container_stack.definitionChanges
machine_diameter = definition_changes.getProperty("material_diameter", "value")
if not machine_diameter:
@ -294,10 +242,7 @@ class MachineSettingsAction(MachineAction):
if material_approximate_diameter != machine_approximate_diameter:
Logger.log("i", "The the currently active material(s) do not match the diameter set for the printer. Finding alternatives.")
if machine_extruder_count > 1:
stacks = ExtruderManager.getInstance().getExtruderStacks()
else:
stacks = [self._global_container_stack]
stacks = ExtruderManager.getInstance().getExtruderStacks()
if self._global_container_stack.getMetaDataEntry("has_machine_materials", False):
materials_definition = self._global_container_stack.definition.getId()
@ -338,7 +283,7 @@ class MachineSettingsAction(MachineAction):
search_criteria["id"] = stack.getMetaDataEntry("preferred_material")
materials = self._container_registry.findInstanceContainers(**search_criteria)
if not materials:
# Preferrd material with new diameter is not found, search for any material
# Preferred material with new diameter is not found, search for any material
search_criteria.pop("id", None)
materials = self._container_registry.findInstanceContainers(**search_criteria)
if not materials:

View file

@ -22,7 +22,7 @@ Cura.MachineAction
onModelChanged:
{
var extruderCount = base.extrudersModel.rowCount();
base.extruderTabsCount = extruderCount > 1 ? extruderCount : 0;
base.extruderTabsCount = extruderCount;
}
}
@ -241,7 +241,6 @@ Cura.MachineAction
UM.TooltipArea
{
visible: manager.definedExtruderCount > 1
height: childrenRect.height
width: childrenRect.width
text: machineExtruderCountProvider.properties.description
@ -283,6 +282,7 @@ Cura.MachineAction
Loader
{
id: materialDiameterField
visible: Cura.MachineManager.hasMaterials
sourceComponent: numericTextFieldWithUnit
property string settingKey: "material_diameter"
property string unit: catalog.i18nc("@label", "mm")
@ -290,15 +290,6 @@ Cura.MachineAction
property var afterOnEditingFinished: manager.updateMaterialForDiameter
property string label: catalog.i18nc("@label", "Material diameter")
}
Loader
{
id: nozzleSizeField
visible: !Cura.MachineManager.hasVariants && machineExtruderCountProvider.properties.value == 1
sourceComponent: numericTextFieldWithUnit
property string settingKey: "machine_nozzle_size"
property string label: catalog.i18nc("@label", "Nozzle size")
property string unit: catalog.i18nc("@label", "mm")
}
}
}
@ -355,7 +346,7 @@ Cura.MachineAction
if(currentIndex > 0)
{
contentItem.forceActiveFocus();
ExtruderManager.setActiveExtruderIndex(currentIndex - 1);
Cura.ExtruderManager.setActiveExtruderIndex(currentIndex - 1);
}
}
@ -585,11 +576,11 @@ Cura.MachineAction
propertyProvider.setPropertyValue("value", text);
if(_forceUpdateOnChange)
{
var extruderIndex = ExtruderManager.activeExtruderIndex;
var extruderIndex = Cura.ExtruderManager.activeExtruderIndex;
manager.forceUpdate();
if(ExtruderManager.activeExtruderIndex != extruderIndex)
if(Cura.ExtruderManager.activeExtruderIndex != extruderIndex)
{
ExtruderManager.setActiveExtruderIndex(extruderIndex)
Cura.ExtruderManager.setActiveExtruderIndex(extruderIndex)
}
}
if(_afterOnEditingFinished)

View file

@ -0,0 +1,44 @@
// Copyright (c) 2017 Ultimaker B.V.
import QtQuick 2.2
import QtQuick.Controls 1.1
import UM 1.3 as UM
import Cura 1.0 as Cura
Item
{
width: parent.width
height: parent.height
// We show a nice overlay on the 3D viewer when the current output device has no monitor view
Rectangle
{
id: viewportOverlay
color: UM.Theme.getColor("viewport_overlay")
width: parent.width
height: parent.height
MouseArea
{
anchors.fill: parent
acceptedButtons: Qt.AllButtons
onWheel: wheel.accepted = true
}
}
Loader
{
id: monitorViewComponent
width: parent.width
height: parent.height
property real maximumWidth: parent.width
property real maximumHeight: parent.height
sourceComponent: Cura.MachineManager.printerOutputDevices.length > 0 ? Cura.MachineManager.printerOutputDevices[0].monitorItem: null
visible: sourceComponent != null
}
}

View file

@ -0,0 +1,73 @@
# Copyright (c) 2017 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
import os.path
from UM.Application import Application
from UM.PluginRegistry import PluginRegistry
from UM.Resources import Resources
from cura.Stages.CuraStage import CuraStage
## Stage for monitoring a 3D printing while it's printing.
class MonitorStage(CuraStage):
def __init__(self, parent = None):
super().__init__(parent)
# Wait until QML engine is created, otherwise creating the new QML components will fail
Application.getInstance().engineCreatedSignal.connect(self._setComponents)
# Update the status icon when the output device is changed
Application.getInstance().getOutputDeviceManager().activeDeviceChanged.connect(self._setIconSource)
def _setComponents(self):
self._setMainOverlay()
self._setSidebar()
self._setIconSource()
def _setMainOverlay(self):
main_component_path = os.path.join(PluginRegistry.getInstance().getPluginPath("MonitorStage"), "MonitorMainView.qml")
self.addDisplayComponent("main", main_component_path)
def _setSidebar(self):
# TODO: currently the sidebar component for prepare and monitor stages is the same, this will change with the printer output device refactor!
sidebar_component_path = os.path.join(Resources.getPath(Application.getInstance().ResourceTypes.QmlFiles), "Sidebar.qml")
self.addDisplayComponent("sidebar", sidebar_component_path)
def _setIconSource(self):
if Application.getInstance().getTheme() is not None:
icon_name = self._getActiveOutputDeviceStatusIcon()
self.setIconSource(Application.getInstance().getTheme().getIcon(icon_name))
## Find the correct status icon depending on the active output device state
def _getActiveOutputDeviceStatusIcon(self):
output_device = Application.getInstance().getOutputDeviceManager().getActiveDevice()
if not output_device:
return "tab_status_unknown"
if hasattr(output_device, "acceptsCommands") and not output_device.acceptsCommands:
return "tab_status_unknown"
if not hasattr(output_device, "printerState") or not hasattr(output_device, "jobState"):
return "tab_status_unknown"
# TODO: refactor to use enum instead of hardcoded strings?
if output_device.printerState == "maintenance":
return "tab_status_busy"
if output_device.jobState in ["printing", "pre_print", "pausing", "resuming"]:
return "tab_status_busy"
if output_device.jobState == "wait_cleanup":
return "tab_status_finished"
if output_device.jobState in ["ready", ""]:
return "tab_status_connected"
if output_device.jobState == "paused":
return "tab_status_paused"
if output_device.jobState == "error":
return "tab_status_stopped"
return "tab_status_unknown"

View file

@ -0,0 +1,20 @@
# Copyright (c) 2017 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from . import MonitorStage
from UM.i18n import i18nCatalog
i18n_catalog = i18nCatalog("cura")
def getMetaData():
return {
"stage": {
"name": i18n_catalog.i18nc("@item:inmenu", "Monitor"),
"weight": 1
}
}
def register(app):
return {
"stage": MonitorStage.MonitorStage()
}

View file

@ -0,0 +1,8 @@
{
"name": "Monitor Stage",
"author": "Ultimaker B.V.",
"version": "1.0.0",
"description": "Provides a monitor stage in Cura.",
"api": 4,
"i18n-catalog": "cura"
}

View file

@ -26,17 +26,92 @@ Item {
spacing: UM.Theme.getSize("default_margin").height
Row
{
spacing: UM.Theme.getSize("default_margin").width
Label
{
text: catalog.i18nc("@label","Mesh Type")
font: UM.Theme.getFont("default")
color: UM.Theme.getColor("text")
height: UM.Theme.getSize("setting").height
verticalAlignment: Text.AlignVCenter
}
ComboBox
{
id: meshTypeSelection
style: UM.Theme.styles.combobox
onActivated: {
UM.ActiveTool.setProperty("MeshType", model.get(index).type)
}
model: ListModel
{
id: meshTypeModel
Component.onCompleted:
{
meshTypeModel.append({
type: "",
text: catalog.i18nc("@label", "Normal model")
});
meshTypeModel.append({
type: "support_mesh",
text: catalog.i18nc("@label", "Print as support")
});
meshTypeModel.append({
type: "anti_overhang_mesh",
text: catalog.i18nc("@label", "Don't support overlap with other models")
});
meshTypeModel.append({
type: "cutting_mesh",
text: catalog.i18nc("@label", "Modify settings for overlap with other models")
});
meshTypeModel.append({
type: "infill_mesh",
text: catalog.i18nc("@label", "Modify settings for infill of other models")
});
meshTypeSelection.updateCurrentIndex();
}
}
function updateCurrentIndex()
{
var mesh_type = UM.ActiveTool.properties.getValue("MeshType");
for(var index=0; index < meshTypeSelection.model.count; index++)
{
if(meshTypeSelection.model.get(index).type == mesh_type)
{
meshTypeSelection.currentIndex = index;
return;
}
}
meshTypeSelection.currentIndex = 0;
}
}
Connections
{
target: UM.Selection
onSelectionChanged: meshTypeSelection.updateCurrentIndex()
}
}
Column
{
// This is to ensure that the panel is first increasing in size up to 200 and then shows a scrollbar.
// It kinda looks ugly otherwise (big panel, no content on it)
id: currentSettings
property int maximumHeight: 200 * screenScaleFactor
height: Math.min(contents.count * (UM.Theme.getSize("section").height + UM.Theme.getSize("default_lining").height), maximumHeight)
visible: ["support_mesh", "anti_overhang_mesh"].indexOf(meshTypeSelection.model.get(meshTypeSelection.currentIndex).type) == -1
ScrollView
{
height: parent.height
width: UM.Theme.getSize("setting").width + UM.Theme.getSize("setting").height
width: UM.Theme.getSize("setting").width
style: UM.Theme.styles.scrollview
ListView
@ -49,6 +124,7 @@ Item {
id: addedSettingsModel;
containerId: Cura.MachineManager.activeDefinitionId
expanded: [ "*" ]
exclude: [ "support_mesh", "anti_overhang_mesh", "cutting_mesh", "infill_mesh" ]
visibilityHandler: Cura.PerObjectSettingVisibilityHandler
{
@ -58,6 +134,7 @@ Item {
delegate: Row
{
spacing: - UM.Theme.getSize("default_margin").width
Loader
{
id: settingLoader
@ -68,6 +145,7 @@ Item {
property var settingDefinitionsModel: addedSettingsModel
property var propertyProvider: provider
property var globalPropertyProvider: inheritStackProvider
property var externalResetHandler: false
//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,
@ -112,7 +190,7 @@ Item {
Button
{
width: (UM.Theme.getSize("setting").height / 2) | 0
width: Math.floor(UM.Theme.getSize("setting").height / 2)
height: UM.Theme.getSize("setting").height
onClicked: addedSettingsModel.setVisible(model.key, false)
@ -125,7 +203,7 @@ Item {
{
anchors.verticalCenter: parent.verticalCenter
width: parent.width
height: parent.height / 2
height: width
sourceSize.width: width
sourceSize.height: width
color: control.hovered ? UM.Theme.getColor("setting_control_button_hover") : UM.Theme.getColor("setting_control_button")
@ -201,9 +279,9 @@ Item {
Button
{
id: customise_settings_button;
height: UM.Theme.getSize("setting").height;
visible: parseInt(UM.Preferences.getValue("cura/active_mode")) == 1
id: customiseSettingsButton;
height: UM.Theme.getSize("setting_control").height;
visible: currentSettings.visible
text: catalog.i18nc("@action:button", "Select settings");
@ -223,21 +301,12 @@ Item {
{
text: control.text;
color: UM.Theme.getColor("setting_control_text");
font: UM.Theme.getFont("default")
anchors.centerIn: parent
}
}
onClicked: settingPickDialog.visible = true;
Connections
{
target: UM.Preferences;
onPreferenceChanged:
{
customise_settings_button.visible = parseInt(UM.Preferences.getValue("cura/active_mode"))
}
}
}
}
@ -325,7 +394,7 @@ Item {
}
visibilityHandler: UM.SettingPreferenceVisibilityHandler {}
expanded: [ "*" ]
exclude: [ "machine_settings", "command_line_settings" ]
exclude: [ "machine_settings", "command_line_settings", "support_mesh", "anti_overhang_mesh", "cutting_mesh", "infill_mesh" ]
}
delegate:Loader
{

View file

@ -8,6 +8,7 @@ from UM.Application import Application
from UM.Preferences import Preferences
from cura.Settings.SettingOverrideDecorator import SettingOverrideDecorator
from cura.Settings.ExtruderManager import ExtruderManager
from UM.Settings.SettingInstance import SettingInstance
from UM.Event import Event
@ -18,7 +19,7 @@ class PerObjectSettingsTool(Tool):
super().__init__()
self._model = None
self.setExposedProperties("SelectedObjectId", "ContainerID", "SelectedActiveExtruder")
self.setExposedProperties("SelectedObjectId", "ContainerID", "SelectedActiveExtruder", "MeshType")
self._advanced_mode = False
self._multi_extrusion = False
@ -70,6 +71,39 @@ class PerObjectSettingsTool(Tool):
selected_object.addDecorator(SettingOverrideDecorator())
selected_object.callDecoration("setActiveExtruder", extruder_stack_id)
def setMeshType(self, mesh_type):
selected_object = Selection.getSelectedObject(0)
stack = selected_object.callDecoration("getStack") #Don't try to get the active extruder since it may be None anyway.
if not stack:
selected_object.addDecorator(SettingOverrideDecorator())
stack = selected_object.callDecoration("getStack")
settings = stack.getTop()
for property_key in ["infill_mesh", "cutting_mesh", "support_mesh", "anti_overhang_mesh"]:
if property_key != mesh_type:
if settings.getInstance(property_key):
settings.removeInstance(property_key)
else:
if not (settings.getInstance(property_key) and settings.getProperty(property_key, "value")):
definition = stack.getSettingDefinition(property_key)
new_instance = SettingInstance(definition, settings)
new_instance.setProperty("value", True)
new_instance.resetState() # Ensure that the state is not seen as a user state.
settings.addInstance(new_instance)
def getMeshType(self):
selected_object = Selection.getSelectedObject(0)
stack = selected_object.callDecoration("getStack") #Don't try to get the active extruder since it may be None anyway.
if not stack:
return ""
settings = stack.getTop()
for property_key in ["infill_mesh", "cutting_mesh", "support_mesh", "anti_overhang_mesh"]:
if settings.getInstance(property_key) and settings.getProperty(property_key, "value"):
return property_key
return ""
def _onPreferenceChanged(self, preference):
if preference == "cura/active_mode":
self._advanced_mode = Preferences.getInstance().getValue(preference) == 1
@ -78,31 +112,26 @@ class PerObjectSettingsTool(Tool):
def _onGlobalContainerChanged(self):
global_container_stack = Application.getInstance().getGlobalContainerStack()
if global_container_stack:
# used for enabling or disabling per extruder settings per object
self._multi_extrusion = global_container_stack.getProperty("machine_extruder_count", "value") > 1
# Ensure that all extruder data is reset
if not self._multi_extrusion:
default_stack_id = global_container_stack.getId()
else:
default_stack = ExtruderManager.getInstance().getExtruderStack(0)
if default_stack:
default_stack_id = default_stack.getId()
else:
default_stack_id = global_container_stack.getId()
extruder_stack = ExtruderManager.getInstance().getExtruderStack(0)
root_node = Application.getInstance().getController().getScene().getRoot()
for node in DepthFirstIterator(root_node):
new_stack_id = default_stack_id
# Get position of old extruder stack for this node
old_extruder_pos = node.callDecoration("getActiveExtruderPosition")
if old_extruder_pos is not None:
# Fetch current (new) extruder stack at position
new_stack = ExtruderManager.getInstance().getExtruderStack(old_extruder_pos)
if new_stack:
new_stack_id = new_stack.getId()
node.callDecoration("setActiveExtruder", new_stack_id)
if extruder_stack:
root_node = Application.getInstance().getController().getScene().getRoot()
for node in DepthFirstIterator(root_node):
new_stack_id = extruder_stack.getId()
# Get position of old extruder stack for this node
old_extruder_pos = node.callDecoration("getActiveExtruderPosition")
if old_extruder_pos is not None:
# Fetch current (new) extruder stack at position
new_stack = ExtruderManager.getInstance().getExtruderStack(old_extruder_pos)
if new_stack:
new_stack_id = new_stack.getId()
node.callDecoration("setActiveExtruder", new_stack_id)
self._updateEnabled()
self._updateEnabled()
def _updateEnabled(self):
selected_objects = Selection.getAllSelectedObjects()

View file

@ -11,7 +11,6 @@ from UM.Message import Message
from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkRequest, QNetworkReply
from PyQt5.QtCore import QUrl, QObject, Qt, pyqtProperty, pyqtSignal, pyqtSlot
from PyQt5.QtQml import QQmlComponent, QQmlContext
import json
import os
@ -39,8 +38,6 @@ class PluginBrowser(QObject, Extension):
self._plugins_metadata = []
self._plugins_model = None
self._qml_component = None
self._qml_context = None
self._dialog = None
self._download_progress = 0
@ -111,17 +108,8 @@ class PluginBrowser(QObject, Extension):
def _createDialog(self, qml_name):
Logger.log("d", "Creating dialog [%s]", qml_name)
path = QUrl.fromLocalFile(os.path.join(PluginRegistry.getInstance().getPluginPath(self.getPluginId()), qml_name))
self._qml_component = QQmlComponent(Application.getInstance()._engine, path)
# We need access to engine (although technically we can't)
self._qml_context = QQmlContext(Application.getInstance()._engine.rootContext())
self._qml_context.setContextProperty("manager", self)
dialog = self._qml_component.create(self._qml_context)
if dialog is None:
Logger.log("e", "QQmlComponent status %s", self._qml_component.status())
Logger.log("e", "QQmlComponent errorString %s", self._qml_component.errorString())
path = os.path.join(PluginRegistry.getInstance().getPluginPath(self.getPluginId()), qml_name)
dialog = Application.getInstance().createQmlComponent(path, {"manager": self})
return dialog
def setIsDownloading(self, is_downloading):

View file

@ -0,0 +1,18 @@
# Copyright (c) 2017 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
import os.path
from UM.Application import Application
from UM.Resources import Resources
from cura.Stages.CuraStage import CuraStage
## Stage for preparing model (slicing).
class PrepareStage(CuraStage):
def __init__(self, parent = None):
super().__init__(parent)
Application.getInstance().engineCreatedSignal.connect(self._engineCreated)
def _engineCreated(self):
sidebar_component_path = os.path.join(Resources.getPath(Application.getInstance().ResourceTypes.QmlFiles), "Sidebar.qml")
self.addDisplayComponent("sidebar", sidebar_component_path)

View file

@ -0,0 +1,20 @@
# Copyright (c) 2017 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from . import PrepareStage
from UM.i18n import i18nCatalog
i18n_catalog = i18nCatalog("cura")
def getMetaData():
return {
"stage": {
"name": i18n_catalog.i18nc("@item:inmenu", "Prepare"),
"weight": 0
}
}
def register(app):
return {
"stage": PrepareStage.PrepareStage()
}

View file

@ -0,0 +1,8 @@
{
"name": "Prepare Stage",
"author": "Ultimaker B.V.",
"version": "1.0.0",
"description": "Provides a prepare stage in Cura.",
"api": 4,
"i18n-catalog": "cura"
}

View file

@ -1,312 +1,325 @@
// Copyright (c) 2017 Ultimaker B.V.
// Cura is released under the terms of the LGPLv3 or higher.
import QtQuick 2.2
import QtQuick.Controls 1.2
import QtQuick.Layouts 1.1
import QtQuick.Controls.Styles 1.1
import UM 1.0 as UM
import Cura 1.0 as Cura
Item {
id: sliderRoot
// handle properties
property real handleSize: 10
property real handleRadius: handleSize / 2
property real minimumRangeHandleSize: handleSize / 2
property color upperHandleColor: "black"
property color lowerHandleColor: "black"
property color rangeHandleColor: "black"
property real handleLabelWidth: width
property var activeHandle: upperHandle
// track properties
property real trackThickness: 4 // width of the slider track
property real trackRadius: trackThickness / 2
property color trackColor: "white"
property real trackBorderWidth: 1 // width of the slider track border
property color trackBorderColor: "black"
// value properties
property real maximumValue: 100
property real minimumValue: 0
property real minimumRange: 0 // minimum range allowed between min and max values
property bool roundValues: true
property real upperValue: maximumValue
property real lowerValue: minimumValue
property bool layersVisible: true
function getUpperValueFromSliderHandle () {
return upperHandle.getValue()
}
function setUpperValue (value) {
upperHandle.setValue(value)
updateRangeHandle()
}
function getLowerValueFromSliderHandle () {
return lowerHandle.getValue()
}
function setLowerValue (value) {
lowerHandle.setValue(value)
updateRangeHandle()
}
function updateRangeHandle () {
rangeHandle.height = lowerHandle.y - (upperHandle.y + upperHandle.height)
}
// set the active handle to show only one label at a time
function setActiveHandle (handle) {
activeHandle = handle
}
// slider track
Rectangle {
id: track
width: sliderRoot.trackThickness
height: sliderRoot.height - sliderRoot.handleSize
radius: sliderRoot.trackRadius
anchors.centerIn: sliderRoot
color: sliderRoot.trackColor
border.width: sliderRoot.trackBorderWidth
border.color: sliderRoot.trackBorderColor
visible: sliderRoot.layersVisible
}
// Range handle
Item {
id: rangeHandle
y: upperHandle.y + upperHandle.height
width: sliderRoot.handleSize
height: sliderRoot.minimumRangeHandleSize
anchors.horizontalCenter: sliderRoot.horizontalCenter
visible: sliderRoot.layersVisible
// set the new value when dragging
function onHandleDragged () {
upperHandle.y = y - upperHandle.height
lowerHandle.y = y + height
var upperValue = sliderRoot.getUpperValueFromSliderHandle()
var lowerValue = sliderRoot.getLowerValueFromSliderHandle()
// set both values after moving the handle position
UM.LayerView.setCurrentLayer(upperValue)
UM.LayerView.setMinimumLayer(lowerValue)
}
function setValue (value) {
var range = sliderRoot.upperValue - sliderRoot.lowerValue
value = Math.min(value, sliderRoot.maximumValue)
value = Math.max(value, sliderRoot.minimumValue + range)
UM.LayerView.setCurrentLayer(value)
UM.LayerView.setMinimumLayer(value - range)
}
Rectangle {
width: sliderRoot.trackThickness - 2 * sliderRoot.trackBorderWidth
height: parent.height + sliderRoot.handleSize
anchors.centerIn: parent
color: sliderRoot.rangeHandleColor
}
MouseArea {
anchors.fill: parent
drag {
target: parent
axis: Drag.YAxis
minimumY: upperHandle.height
maximumY: sliderRoot.height - (rangeHandle.height + lowerHandle.height)
}
onPositionChanged: parent.onHandleDragged()
onPressed: sliderRoot.setActiveHandle(rangeHandle)
}
LayerSliderLabel {
id: rangleHandleLabel
height: sliderRoot.handleSize + UM.Theme.getSize("default_margin").height
x: parent.x - width - UM.Theme.getSize("default_margin").width
anchors.verticalCenter: parent.verticalCenter
target: Qt.point(sliderRoot.width, y + height / 2)
visible: sliderRoot.activeHandle == parent
// custom properties
maximumValue: sliderRoot.maximumValue
value: sliderRoot.upperValue
busy: UM.LayerView.busy
setValue: rangeHandle.setValue // connect callback functions
}
}
// Upper handle
Rectangle {
id: upperHandle
y: sliderRoot.height - (sliderRoot.minimumRangeHandleSize + 2 * sliderRoot.handleSize)
width: sliderRoot.handleSize
height: sliderRoot.handleSize
anchors.horizontalCenter: sliderRoot.horizontalCenter
radius: sliderRoot.handleRadius
color: sliderRoot.upperHandleColor
visible: sliderRoot.layersVisible
function onHandleDragged () {
// don't allow the lower handle to be heigher than the upper handle
if (lowerHandle.y - (y + height) < sliderRoot.minimumRangeHandleSize) {
lowerHandle.y = y + height + sliderRoot.minimumRangeHandleSize
}
// update the range handle
sliderRoot.updateRangeHandle()
// set the new value after moving the handle position
UM.LayerView.setCurrentLayer(getValue())
}
// get the upper value based on the slider position
function getValue () {
var result = y / (sliderRoot.height - (2 * sliderRoot.handleSize + sliderRoot.minimumRangeHandleSize))
result = sliderRoot.maximumValue + result * (sliderRoot.minimumValue - (sliderRoot.maximumValue - sliderRoot.minimumValue))
result = sliderRoot.roundValues ? Math.round(result) : result
return result
}
// set the slider position based on the upper value
function setValue (value) {
UM.LayerView.setCurrentLayer(value)
var diff = (value - sliderRoot.maximumValue) / (sliderRoot.minimumValue - sliderRoot.maximumValue)
var newUpperYPosition = Math.round(diff * (sliderRoot.height - (2 * sliderRoot.handleSize + sliderRoot.minimumRangeHandleSize)))
y = newUpperYPosition
// update the range handle
sliderRoot.updateRangeHandle()
}
// dragging
MouseArea {
anchors.fill: parent
drag {
target: parent
axis: Drag.YAxis
minimumY: 0
maximumY: sliderRoot.height - (2 * sliderRoot.handleSize + sliderRoot.minimumRangeHandleSize)
}
onPositionChanged: parent.onHandleDragged()
onPressed: sliderRoot.setActiveHandle(upperHandle)
}
LayerSliderLabel {
id: upperHandleLabel
height: sliderRoot.handleSize + UM.Theme.getSize("default_margin").height
x: parent.x - width - UM.Theme.getSize("default_margin").width
anchors.verticalCenter: parent.verticalCenter
target: Qt.point(sliderRoot.width, y + height / 2)
visible: sliderRoot.activeHandle == parent
// custom properties
maximumValue: sliderRoot.maximumValue
value: sliderRoot.upperValue
busy: UM.LayerView.busy
setValue: upperHandle.setValue // connect callback functions
}
}
// Lower handle
Rectangle {
id: lowerHandle
y: sliderRoot.height - sliderRoot.handleSize
width: parent.handleSize
height: parent.handleSize
anchors.horizontalCenter: parent.horizontalCenter
radius: sliderRoot.handleRadius
color: sliderRoot.lowerHandleColor
visible: slider.layersVisible
function onHandleDragged () {
// don't allow the upper handle to be lower than the lower handle
if (y - (upperHandle.y + upperHandle.height) < sliderRoot.minimumRangeHandleSize) {
upperHandle.y = y - (upperHandle.heigth + sliderRoot.minimumRangeHandleSize)
}
// update the range handle
sliderRoot.updateRangeHandle()
// set the new value after moving the handle position
UM.LayerView.setMinimumLayer(getValue())
}
// get the lower value from the current slider position
function getValue () {
var result = (y - (sliderRoot.handleSize + sliderRoot.minimumRangeHandleSize)) / (sliderRoot.height - (2 * sliderRoot.handleSize + sliderRoot.minimumRangeHandleSize));
result = sliderRoot.maximumValue - sliderRoot.minimumRange + result * (sliderRoot.minimumValue - (sliderRoot.maximumValue - sliderRoot.minimumRange))
result = sliderRoot.roundValues ? Math.round(result) : result
return result
}
// set the slider position based on the lower value
function setValue (value) {
UM.LayerView.setMinimumLayer(value)
var diff = (value - sliderRoot.maximumValue) / (sliderRoot.minimumValue - sliderRoot.maximumValue)
var newLowerYPosition = Math.round((sliderRoot.handleSize + sliderRoot.minimumRangeHandleSize) + diff * (sliderRoot.height - (2 * sliderRoot.handleSize + sliderRoot.minimumRangeHandleSize)))
y = newLowerYPosition
// update the range handle
sliderRoot.updateRangeHandle()
}
// dragging
MouseArea {
anchors.fill: parent
drag {
target: parent
axis: Drag.YAxis
minimumY: upperHandle.height + sliderRoot.minimumRangeHandleSize
maximumY: sliderRoot.height - parent.height
}
onPositionChanged: parent.onHandleDragged()
onPressed: sliderRoot.setActiveHandle(lowerHandle)
}
LayerSliderLabel {
id: lowerHandleLabel
height: sliderRoot.handleSize + UM.Theme.getSize("default_margin").height
x: parent.x - width - UM.Theme.getSize("default_margin").width
anchors.verticalCenter: parent.verticalCenter
target: Qt.point(sliderRoot.width, y + height / 2)
visible: sliderRoot.activeHandle == parent
// custom properties
maximumValue: sliderRoot.maximumValue
value: sliderRoot.lowerValue
busy: UM.LayerView.busy
setValue: lowerHandle.setValue // connect callback functions
}
}
}
// Copyright (c) 2017 Ultimaker B.V.
// Cura is released under the terms of the LGPLv3 or higher.
import QtQuick 2.2
import QtQuick.Controls 1.2
import QtQuick.Layouts 1.1
import QtQuick.Controls.Styles 1.1
import UM 1.0 as UM
import Cura 1.0 as Cura
Item {
id: sliderRoot
// handle properties
property real handleSize: 10
property real handleRadius: handleSize / 2
property real minimumRangeHandleSize: handleSize / 2
property color upperHandleColor: "black"
property color lowerHandleColor: "black"
property color rangeHandleColor: "black"
property color handleActiveColor: "white"
property real handleLabelWidth: width
property var activeHandle: upperHandle
// track properties
property real trackThickness: 4 // width of the slider track
property real trackRadius: trackThickness / 2
property color trackColor: "white"
property real trackBorderWidth: 1 // width of the slider track border
property color trackBorderColor: "black"
// value properties
property real maximumValue: 100
property real minimumValue: 0
property real minimumRange: 0 // minimum range allowed between min and max values
property bool roundValues: true
property real upperValue: maximumValue
property real lowerValue: minimumValue
property bool layersVisible: true
function getUpperValueFromSliderHandle () {
return upperHandle.getValue()
}
function setUpperValue (value) {
upperHandle.setValue(value)
updateRangeHandle()
}
function getLowerValueFromSliderHandle () {
return lowerHandle.getValue()
}
function setLowerValue (value) {
lowerHandle.setValue(value)
updateRangeHandle()
}
function updateRangeHandle () {
rangeHandle.height = lowerHandle.y - (upperHandle.y + upperHandle.height)
}
// set the active handle to show only one label at a time
function setActiveHandle (handle) {
activeHandle = handle
}
// slider track
Rectangle {
id: track
width: sliderRoot.trackThickness
height: sliderRoot.height - sliderRoot.handleSize
radius: sliderRoot.trackRadius
anchors.centerIn: sliderRoot
color: sliderRoot.trackColor
border.width: sliderRoot.trackBorderWidth
border.color: sliderRoot.trackBorderColor
visible: sliderRoot.layersVisible
}
// Range handle
Item {
id: rangeHandle
y: upperHandle.y + upperHandle.height
width: sliderRoot.handleSize
height: sliderRoot.minimumRangeHandleSize
anchors.horizontalCenter: sliderRoot.horizontalCenter
visible: sliderRoot.layersVisible
// set the new value when dragging
function onHandleDragged () {
upperHandle.y = y - upperHandle.height
lowerHandle.y = y + height
var upperValue = sliderRoot.getUpperValueFromSliderHandle()
var lowerValue = sliderRoot.getLowerValueFromSliderHandle()
// set both values after moving the handle position
UM.SimulationView.setCurrentLayer(upperValue)
UM.SimulationView.setMinimumLayer(lowerValue)
}
function setValue (value) {
var range = sliderRoot.upperValue - sliderRoot.lowerValue
value = Math.min(value, sliderRoot.maximumValue)
value = Math.max(value, sliderRoot.minimumValue + range)
UM.SimulationView.setCurrentLayer(value)
UM.SimulationView.setMinimumLayer(value - range)
}
Rectangle {
width: sliderRoot.trackThickness - 2 * sliderRoot.trackBorderWidth
height: parent.height + sliderRoot.handleSize
anchors.centerIn: parent
color: sliderRoot.rangeHandleColor
}
MouseArea {
anchors.fill: parent
drag {
target: parent
axis: Drag.YAxis
minimumY: upperHandle.height
maximumY: sliderRoot.height - (rangeHandle.height + lowerHandle.height)
}
onPositionChanged: parent.onHandleDragged()
onPressed: sliderRoot.setActiveHandle(rangeHandle)
}
SimulationSliderLabel {
id: rangleHandleLabel
height: sliderRoot.handleSize + UM.Theme.getSize("default_margin").height
x: parent.x - width - UM.Theme.getSize("default_margin").width
anchors.verticalCenter: parent.verticalCenter
target: Qt.point(sliderRoot.width, y + height / 2)
visible: sliderRoot.activeHandle == parent
// custom properties
maximumValue: sliderRoot.maximumValue
value: sliderRoot.upperValue
busy: UM.SimulationView.busy
setValue: rangeHandle.setValue // connect callback functions
}
}
// Upper handle
Rectangle {
id: upperHandle
y: sliderRoot.height - (sliderRoot.minimumRangeHandleSize + 2 * sliderRoot.handleSize)
width: sliderRoot.handleSize
height: sliderRoot.handleSize
anchors.horizontalCenter: sliderRoot.horizontalCenter
radius: sliderRoot.handleRadius
color: upperHandleLabel.activeFocus ? sliderRoot.handleActiveColor : sliderRoot.upperHandleColor
visible: sliderRoot.layersVisible
function onHandleDragged () {
// don't allow the lower handle to be heigher than the upper handle
if (lowerHandle.y - (y + height) < sliderRoot.minimumRangeHandleSize) {
lowerHandle.y = y + height + sliderRoot.minimumRangeHandleSize
}
// update the range handle
sliderRoot.updateRangeHandle()
// set the new value after moving the handle position
UM.SimulationView.setCurrentLayer(getValue())
}
// get the upper value based on the slider position
function getValue () {
var result = y / (sliderRoot.height - (2 * sliderRoot.handleSize + sliderRoot.minimumRangeHandleSize))
result = sliderRoot.maximumValue + result * (sliderRoot.minimumValue - (sliderRoot.maximumValue - sliderRoot.minimumValue))
result = sliderRoot.roundValues ? Math.round(result) : result
return result
}
// set the slider position based on the upper value
function setValue (value) {
UM.SimulationView.setCurrentLayer(value)
var diff = (value - sliderRoot.maximumValue) / (sliderRoot.minimumValue - sliderRoot.maximumValue)
var newUpperYPosition = Math.round(diff * (sliderRoot.height - (2 * sliderRoot.handleSize + sliderRoot.minimumRangeHandleSize)))
y = newUpperYPosition
// update the range handle
sliderRoot.updateRangeHandle()
}
Keys.onUpPressed: upperHandleLabel.setValue(upperHandleLabel.value + ((event.modifiers & Qt.ShiftModifier) ? 10 : 1))
Keys.onDownPressed: upperHandleLabel.setValue(upperHandleLabel.value - ((event.modifiers & Qt.ShiftModifier) ? 10 : 1))
// dragging
MouseArea {
anchors.fill: parent
drag {
target: parent
axis: Drag.YAxis
minimumY: 0
maximumY: sliderRoot.height - (2 * sliderRoot.handleSize + sliderRoot.minimumRangeHandleSize)
}
onPositionChanged: parent.onHandleDragged()
onPressed: {
sliderRoot.setActiveHandle(upperHandle)
upperHandleLabel.forceActiveFocus()
}
}
SimulationSliderLabel {
id: upperHandleLabel
height: sliderRoot.handleSize + UM.Theme.getSize("default_margin").height
x: parent.x - width - UM.Theme.getSize("default_margin").width
anchors.verticalCenter: parent.verticalCenter
target: Qt.point(sliderRoot.width, y + height / 2)
visible: sliderRoot.activeHandle == parent
// custom properties
maximumValue: sliderRoot.maximumValue
value: sliderRoot.upperValue
busy: UM.SimulationView.busy
setValue: upperHandle.setValue // connect callback functions
}
}
// Lower handle
Rectangle {
id: lowerHandle
y: sliderRoot.height - sliderRoot.handleSize
width: parent.handleSize
height: parent.handleSize
anchors.horizontalCenter: parent.horizontalCenter
radius: sliderRoot.handleRadius
color: lowerHandleLabel.activeFocus ? sliderRoot.handleActiveColor : sliderRoot.lowerHandleColor
visible: sliderRoot.layersVisible
function onHandleDragged () {
// don't allow the upper handle to be lower than the lower handle
if (y - (upperHandle.y + upperHandle.height) < sliderRoot.minimumRangeHandleSize) {
upperHandle.y = y - (upperHandle.heigth + sliderRoot.minimumRangeHandleSize)
}
// update the range handle
sliderRoot.updateRangeHandle()
// set the new value after moving the handle position
UM.SimulationView.setMinimumLayer(getValue())
}
// get the lower value from the current slider position
function getValue () {
var result = (y - (sliderRoot.handleSize + sliderRoot.minimumRangeHandleSize)) / (sliderRoot.height - (2 * sliderRoot.handleSize + sliderRoot.minimumRangeHandleSize));
result = sliderRoot.maximumValue - sliderRoot.minimumRange + result * (sliderRoot.minimumValue - (sliderRoot.maximumValue - sliderRoot.minimumRange))
result = sliderRoot.roundValues ? Math.round(result) : result
return result
}
// set the slider position based on the lower value
function setValue (value) {
UM.SimulationView.setMinimumLayer(value)
var diff = (value - sliderRoot.maximumValue) / (sliderRoot.minimumValue - sliderRoot.maximumValue)
var newLowerYPosition = Math.round((sliderRoot.handleSize + sliderRoot.minimumRangeHandleSize) + diff * (sliderRoot.height - (2 * sliderRoot.handleSize + sliderRoot.minimumRangeHandleSize)))
y = newLowerYPosition
// update the range handle
sliderRoot.updateRangeHandle()
}
Keys.onUpPressed: lowerHandleLabel.setValue(lowerHandleLabel.value + ((event.modifiers & Qt.ShiftModifier) ? 10 : 1))
Keys.onDownPressed: lowerHandleLabel.setValue(lowerHandleLabel.value - ((event.modifiers & Qt.ShiftModifier) ? 10 : 1))
// dragging
MouseArea {
anchors.fill: parent
drag {
target: parent
axis: Drag.YAxis
minimumY: upperHandle.height + sliderRoot.minimumRangeHandleSize
maximumY: sliderRoot.height - parent.height
}
onPositionChanged: parent.onHandleDragged()
onPressed: {
sliderRoot.setActiveHandle(lowerHandle)
lowerHandleLabel.forceActiveFocus()
}
}
SimulationSliderLabel {
id: lowerHandleLabel
height: sliderRoot.handleSize + UM.Theme.getSize("default_margin").height
x: parent.x - width - UM.Theme.getSize("default_margin").width
anchors.verticalCenter: parent.verticalCenter
target: Qt.point(sliderRoot.width, y + height / 2)
visible: sliderRoot.activeHandle == parent
// custom properties
maximumValue: sliderRoot.maximumValue
value: sliderRoot.lowerValue
busy: UM.SimulationView.busy
setValue: lowerHandle.setValue // connect callback functions
}
}
}

View file

@ -0,0 +1,49 @@
# Copyright (c) 2017 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from UM.Application import Application
from UM.Math.Color import Color
from UM.Math.Vector import Vector
from UM.PluginRegistry import PluginRegistry
from UM.Scene.SceneNode import SceneNode
from UM.View.GL.OpenGL import OpenGL
from UM.Resources import Resources
import os
class NozzleNode(SceneNode):
def __init__(self, parent = None):
super().__init__(parent)
self._shader = None
self.setCalculateBoundingBox(False)
self._createNozzleMesh()
def _createNozzleMesh(self):
mesh_file = "resources/nozzle.stl"
try:
path = os.path.join(PluginRegistry.getInstance().getPluginPath("SimulationView"), mesh_file)
except FileNotFoundError:
path = ""
reader = Application.getInstance().getMeshFileHandler().getReaderForFile(path)
node = reader.read(path)
if node.getMeshData():
self.setMeshData(node.getMeshData())
def render(self, renderer):
# Avoid to render if it is not visible
if not self.isVisible():
return False
if not self._shader:
# We now misuse the platform shader, as it actually supports textures
self._shader = OpenGL.getInstance().createShaderProgram(Resources.getPath(Resources.Shaders, "color.shader"))
self._shader.setUniformValue("u_color", Color(*Application.getInstance().getTheme().getColor("layerview_nozzle").getRgb()))
# Set the opacity to 0, so that the template is in full control.
self._shader.setUniformValue("u_opacity", 0)
if self.getMeshData():
renderer.queueNode(self, shader = self._shader, transparent = True)
return True

View file

@ -0,0 +1,161 @@
// Copyright (c) 2017 Ultimaker B.V.
// Cura is released under the terms of the LGPLv3 or higher.
import QtQuick 2.2
import QtQuick.Controls 1.2
import QtQuick.Layouts 1.1
import QtQuick.Controls.Styles 1.1
import UM 1.0 as UM
import Cura 1.0 as Cura
Item {
id: sliderRoot
// handle properties
property real handleSize: 10
property real handleRadius: handleSize / 2
property color handleColor: "black"
property color handleActiveColor: "white"
property color rangeColor: "black"
property real handleLabelWidth: width
// track properties
property real trackThickness: 4 // width of the slider track
property real trackRadius: trackThickness / 2
property color trackColor: "white"
property real trackBorderWidth: 1 // width of the slider track border
property color trackBorderColor: "black"
// value properties
property real maximumValue: 100
property bool roundValues: true
property real handleValue: maximumValue
property bool pathsVisible: true
function getHandleValueFromSliderHandle () {
return handle.getValue()
}
function setHandleValue (value) {
handle.setValue(value)
updateRangeHandle()
}
function updateRangeHandle () {
rangeHandle.width = handle.x - sliderRoot.handleSize
}
// slider track
Rectangle {
id: track
width: sliderRoot.width - sliderRoot.handleSize
height: sliderRoot.trackThickness
radius: sliderRoot.trackRadius
anchors.centerIn: sliderRoot
color: sliderRoot.trackColor
border.width: sliderRoot.trackBorderWidth
border.color: sliderRoot.trackBorderColor
visible: sliderRoot.pathsVisible
}
// Progress indicator
Item {
id: rangeHandle
x: handle.width
height: sliderRoot.handleSize
width: handle.x - sliderRoot.handleSize
anchors.verticalCenter: sliderRoot.verticalCenter
visible: sliderRoot.pathsVisible
Rectangle {
height: sliderRoot.trackThickness - 2 * sliderRoot.trackBorderWidth
width: parent.width + sliderRoot.handleSize
anchors.centerIn: parent
color: sliderRoot.rangeColor
}
}
// Handle
Rectangle {
id: handle
x: sliderRoot.handleSize
width: sliderRoot.handleSize
height: sliderRoot.handleSize
anchors.verticalCenter: sliderRoot.verticalCenter
radius: sliderRoot.handleRadius
color: handleLabel.activeFocus ? sliderRoot.handleActiveColor : sliderRoot.handleColor
visible: sliderRoot.pathsVisible
function onHandleDragged () {
// update the range handle
sliderRoot.updateRangeHandle()
// set the new value after moving the handle position
UM.SimulationView.setCurrentPath(getValue())
}
// get the value based on the slider position
function getValue () {
var result = x / (sliderRoot.width - sliderRoot.handleSize)
result = result * sliderRoot.maximumValue
result = sliderRoot.roundValues ? Math.round(result) : result
return result
}
// set the slider position based on the value
function setValue (value) {
UM.SimulationView.setCurrentPath(value)
var diff = value / sliderRoot.maximumValue
var newXPosition = Math.round(diff * (sliderRoot.width - sliderRoot.handleSize))
x = newXPosition
// update the range handle
sliderRoot.updateRangeHandle()
}
Keys.onRightPressed: handleLabel.setValue(handleLabel.value + ((event.modifiers & Qt.ShiftModifier) ? 10 : 1))
Keys.onLeftPressed: handleLabel.setValue(handleLabel.value - ((event.modifiers & Qt.ShiftModifier) ? 10 : 1))
// dragging
MouseArea {
anchors.fill: parent
drag {
target: parent
axis: Drag.XAxis
minimumX: 0
maximumX: sliderRoot.width - sliderRoot.handleSize
}
onPressed: {
handleLabel.forceActiveFocus()
}
onPositionChanged: parent.onHandleDragged()
}
SimulationSliderLabel {
id: handleLabel
height: sliderRoot.handleSize + UM.Theme.getSize("default_margin").height
y: parent.y + sliderRoot.handleSize + UM.Theme.getSize("default_margin").height
anchors.horizontalCenter: parent.horizontalCenter
target: Qt.point(x + width / 2, sliderRoot.height)
visible: false
startFrom: 0
// custom properties
maximumValue: sliderRoot.maximumValue
value: sliderRoot.handleValue
busy: UM.SimulationView.busy
setValue: handle.setValue // connect callback functions
}
}
}

View file

@ -0,0 +1,190 @@
# Copyright (c) 2017 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from UM.Math.Color import Color
from UM.Math.Vector import Vector
from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
from UM.Resources import Resources
from UM.Scene.SceneNode import SceneNode
from UM.Scene.ToolHandle import ToolHandle
from UM.Application import Application
from UM.PluginRegistry import PluginRegistry
from UM.View.RenderPass import RenderPass
from UM.View.RenderBatch import RenderBatch
from UM.View.GL.OpenGL import OpenGL
from cura.Settings.ExtruderManager import ExtruderManager
import os.path
## RenderPass used to display g-code paths.
from .NozzleNode import NozzleNode
class SimulationPass(RenderPass):
def __init__(self, width, height):
super().__init__("simulationview", width, height)
self._layer_shader = None
self._layer_shadow_shader = None
self._current_shader = None # This shader will be the shadow or the normal depending if the user wants to see the paths or the layers
self._tool_handle_shader = None
self._nozzle_shader = None
self._old_current_layer = 0
self._old_current_path = 0
self._switching_layers = True # It tracks when the user is moving the layers' slider
self._gl = OpenGL.getInstance().getBindingsObject()
self._scene = Application.getInstance().getController().getScene()
self._extruder_manager = ExtruderManager.getInstance()
self._layer_view = None
self._compatibility_mode = None
def setSimulationView(self, layerview):
self._layer_view = layerview
self._compatibility_mode = layerview.getCompatibilityMode()
def render(self):
if not self._layer_shader:
if self._compatibility_mode:
shader_filename = "layers.shader"
shadow_shader_filename = "layers_shadow.shader"
else:
shader_filename = "layers3d.shader"
shadow_shader_filename = "layers3d_shadow.shader"
self._layer_shader = OpenGL.getInstance().createShaderProgram(os.path.join(PluginRegistry.getInstance().getPluginPath("SimulationView"), shader_filename))
self._layer_shadow_shader = OpenGL.getInstance().createShaderProgram(os.path.join(PluginRegistry.getInstance().getPluginPath("SimulationView"), shadow_shader_filename))
self._current_shader = self._layer_shader
# Use extruder 0 if the extruder manager reports extruder index -1 (for single extrusion printers)
self._layer_shader.setUniformValue("u_active_extruder", float(max(0, self._extruder_manager.activeExtruderIndex)))
if self._layer_view:
self._layer_shader.setUniformValue("u_max_feedrate", self._layer_view.getMaxFeedrate())
self._layer_shader.setUniformValue("u_min_feedrate", self._layer_view.getMinFeedrate())
self._layer_shader.setUniformValue("u_max_thickness", self._layer_view.getMaxThickness())
self._layer_shader.setUniformValue("u_min_thickness", self._layer_view.getMinThickness())
self._layer_shader.setUniformValue("u_layer_view_type", self._layer_view.getSimulationViewType())
self._layer_shader.setUniformValue("u_extruder_opacity", self._layer_view.getExtruderOpacities())
self._layer_shader.setUniformValue("u_show_travel_moves", self._layer_view.getShowTravelMoves())
self._layer_shader.setUniformValue("u_show_helpers", self._layer_view.getShowHelpers())
self._layer_shader.setUniformValue("u_show_skin", self._layer_view.getShowSkin())
self._layer_shader.setUniformValue("u_show_infill", self._layer_view.getShowInfill())
else:
#defaults
self._layer_shader.setUniformValue("u_max_feedrate", 1)
self._layer_shader.setUniformValue("u_min_feedrate", 0)
self._layer_shader.setUniformValue("u_max_thickness", 1)
self._layer_shader.setUniformValue("u_min_thickness", 0)
self._layer_shader.setUniformValue("u_layer_view_type", 1)
self._layer_shader.setUniformValue("u_extruder_opacity", [1, 1, 1, 1])
self._layer_shader.setUniformValue("u_show_travel_moves", 0)
self._layer_shader.setUniformValue("u_show_helpers", 1)
self._layer_shader.setUniformValue("u_show_skin", 1)
self._layer_shader.setUniformValue("u_show_infill", 1)
if not self._tool_handle_shader:
self._tool_handle_shader = OpenGL.getInstance().createShaderProgram(Resources.getPath(Resources.Shaders, "toolhandle.shader"))
if not self._nozzle_shader:
self._nozzle_shader = OpenGL.getInstance().createShaderProgram(Resources.getPath(Resources.Shaders, "color.shader"))
self._nozzle_shader.setUniformValue("u_color", Color(*Application.getInstance().getTheme().getColor("layerview_nozzle").getRgb()))
self.bind()
tool_handle_batch = RenderBatch(self._tool_handle_shader, type = RenderBatch.RenderType.Overlay, backface_cull = True)
head_position = None # Indicates the current position of the print head
nozzle_node = None
for node in DepthFirstIterator(self._scene.getRoot()):
if isinstance(node, ToolHandle):
tool_handle_batch.addItem(node.getWorldTransformation(), mesh = node.getSolidMesh())
elif isinstance(node, NozzleNode):
nozzle_node = node
nozzle_node.setVisible(False)
elif isinstance(node, SceneNode) and (node.getMeshData() or node.callDecoration("isBlockSlicing")) and node.isVisible():
layer_data = node.callDecoration("getLayerData")
if not layer_data:
continue
# Render all layers below a certain number as line mesh instead of vertices.
if self._layer_view._current_layer_num > -1 and ((not self._layer_view._only_show_top_layers) or (not self._layer_view.getCompatibilityMode())):
start = 0
end = 0
element_counts = layer_data.getElementCounts()
for layer in sorted(element_counts.keys()):
# In the current layer, we show just the indicated paths
if layer == self._layer_view._current_layer_num:
# We look for the position of the head, searching the point of the current path
index = self._layer_view._current_path_num
offset = 0
for polygon in layer_data.getLayer(layer).polygons:
# The size indicates all values in the two-dimension array, and the second dimension is
# always size 3 because we have 3D points.
if index >= polygon.data.size // 3 - offset:
index -= polygon.data.size // 3 - offset
offset = 1 # This is to avoid the first point when there is more than one polygon, since has the same value as the last point in the previous polygon
continue
# The head position is calculated and translated
head_position = Vector(polygon.data[index+offset][0], polygon.data[index+offset][1], polygon.data[index+offset][2]) + node.getWorldPosition()
break
break
if self._layer_view._minimum_layer_num > layer:
start += element_counts[layer]
end += element_counts[layer]
# Calculate the range of paths in the last layer
current_layer_start = end
current_layer_end = end + self._layer_view._current_path_num * 2 # Because each point is used twice
# This uses glDrawRangeElements internally to only draw a certain range of lines.
# All the layers but the current selected layer are rendered first
if self._old_current_path != self._layer_view._current_path_num:
self._current_shader = self._layer_shadow_shader
self._switching_layers = False
if not self._layer_view.isSimulationRunning() and self._old_current_layer != self._layer_view._current_layer_num:
self._current_shader = self._layer_shader
self._switching_layers = True
layers_batch = RenderBatch(self._current_shader, type = RenderBatch.RenderType.Solid, mode = RenderBatch.RenderMode.Lines, range = (start, end), backface_cull = True)
layers_batch.addItem(node.getWorldTransformation(), layer_data)
layers_batch.render(self._scene.getActiveCamera())
# Current selected layer is rendered
current_layer_batch = RenderBatch(self._layer_shader, type = RenderBatch.RenderType.Solid, mode = RenderBatch.RenderMode.Lines, range = (current_layer_start, current_layer_end))
current_layer_batch.addItem(node.getWorldTransformation(), layer_data)
current_layer_batch.render(self._scene.getActiveCamera())
self._old_current_layer = self._layer_view._current_layer_num
self._old_current_path = self._layer_view._current_path_num
# Create a new batch that is not range-limited
batch = RenderBatch(self._layer_shader, type = RenderBatch.RenderType.Solid)
if self._layer_view.getCurrentLayerMesh():
batch.addItem(node.getWorldTransformation(), self._layer_view.getCurrentLayerMesh())
if self._layer_view.getCurrentLayerJumps():
batch.addItem(node.getWorldTransformation(), self._layer_view.getCurrentLayerJumps())
if len(batch.items) > 0:
batch.render(self._scene.getActiveCamera())
# The nozzle is drawn when once we know the correct position of the head,
# but the user is not using the layer slider, and the compatibility mode is not enabled
if not self._switching_layers and not self._compatibility_mode and self._layer_view.getActivity() and nozzle_node is not None:
if head_position is not None:
nozzle_node.setVisible(True)
nozzle_node.setPosition(head_position)
nozzle_batch = RenderBatch(self._nozzle_shader, type = RenderBatch.RenderType.Transparent)
nozzle_batch.addItem(nozzle_node.getWorldTransformation(), mesh = nozzle_node.getMeshData())
nozzle_batch.render(self._scene.getActiveCamera())
# Render toolhandles on top of the layerview
if len(tool_handle_batch.items) > 0:
tool_handle_batch.render(self._scene.getActiveCamera())
self.release()

View file

@ -1,103 +1,104 @@
// Copyright (c) 2017 Ultimaker B.V.
// Cura is released under the terms of the LGPLv3 or higher.
import QtQuick 2.2
import QtQuick.Controls 1.2
import QtQuick.Layouts 1.1
import QtQuick.Controls.Styles 1.1
import UM 1.0 as UM
import Cura 1.0 as Cura
UM.PointingRectangle {
id: sliderLabelRoot
// custom properties
property real maximumValue: 100
property real value: 0
property var setValue // Function
property bool busy: false
target: Qt.point(parent.width, y + height / 2)
arrowSize: UM.Theme.getSize("default_arrow").width
height: parent.height
width: valueLabel.width + UM.Theme.getSize("default_margin").width
visible: false
// make sure the text field is focussed when pressing the parent handle
// needed to connect the key bindings when switching active handle
onVisibleChanged: if (visible) valueLabel.forceActiveFocus()
color: UM.Theme.getColor("tool_panel_background")
borderColor: UM.Theme.getColor("lining")
borderWidth: UM.Theme.getSize("default_lining").width
Behavior on height {
NumberAnimation {
duration: 50
}
}
// catch all mouse events so they're not handled by underlying 3D scene
MouseArea {
anchors.fill: parent
}
TextField {
id: valueLabel
anchors {
left: parent.left
leftMargin: Math.floor(UM.Theme.getSize("default_margin").width / 2)
verticalCenter: parent.verticalCenter
}
width: 40 * screenScaleFactor
text: sliderLabelRoot.value + 1 // the current handle value, add 1 because layers is an array
horizontalAlignment: TextInput.AlignRight
// key bindings, work when label is currenctly focused (active handle in LayerSlider)
Keys.onUpPressed: sliderLabelRoot.setValue(sliderLabelRoot.value + ((event.modifiers & Qt.ShiftModifier) ? 10 : 1))
Keys.onDownPressed: sliderLabelRoot.setValue(sliderLabelRoot.value - ((event.modifiers & Qt.ShiftModifier) ? 10 : 1))
style: TextFieldStyle {
textColor: UM.Theme.getColor("setting_control_text")
font: UM.Theme.getFont("default")
background: Item { }
}
onEditingFinished: {
// Ensure that the cursor is at the first position. On some systems the text isn't fully visible
// Seems to have to do something with different dpi densities that QML doesn't quite handle.
// Another option would be to increase the size even further, but that gives pretty ugly results.
cursorPosition = 0
if (valueLabel.text != "") {
// -1 because we need to convert back to an array structure
sliderLabelRoot.setValue(parseInt(valueLabel.text) - 1)
}
}
validator: IntValidator {
bottom: 1
top: sliderLabelRoot.maximumValue + 1 // +1 because actual layers is an array
}
}
BusyIndicator {
id: busyIndicator
anchors {
left: parent.right
leftMargin: Math.floor(UM.Theme.getSize("default_margin").width / 2)
verticalCenter: parent.verticalCenter
}
width: sliderLabelRoot.height
height: width
visible: sliderLabelRoot.busy
running: sliderLabelRoot.busy
}
}
// Copyright (c) 2017 Ultimaker B.V.
// Cura is released under the terms of the LGPLv3 or higher.
import QtQuick 2.2
import QtQuick.Controls 1.2
import QtQuick.Layouts 1.1
import QtQuick.Controls.Styles 1.1
import UM 1.0 as UM
import Cura 1.0 as Cura
UM.PointingRectangle {
id: sliderLabelRoot
// custom properties
property real maximumValue: 100
property real value: 0
property var setValue // Function
property bool busy: false
property int startFrom: 1
target: Qt.point(parent.width, y + height / 2)
arrowSize: UM.Theme.getSize("default_arrow").width
height: parent.height
width: valueLabel.width + UM.Theme.getSize("default_margin").width
visible: false
// make sure the text field is focussed when pressing the parent handle
// needed to connect the key bindings when switching active handle
onVisibleChanged: if (visible) valueLabel.forceActiveFocus()
color: UM.Theme.getColor("tool_panel_background")
borderColor: UM.Theme.getColor("lining")
borderWidth: UM.Theme.getSize("default_lining").width
Behavior on height {
NumberAnimation {
duration: 50
}
}
// catch all mouse events so they're not handled by underlying 3D scene
MouseArea {
anchors.fill: parent
}
TextField {
id: valueLabel
anchors {
left: parent.left
leftMargin: Math.floor(UM.Theme.getSize("default_margin").width / 2)
verticalCenter: parent.verticalCenter
}
width: 40 * screenScaleFactor
text: sliderLabelRoot.value + startFrom // the current handle value, add 1 because layers is an array
horizontalAlignment: TextInput.AlignRight
// key bindings, work when label is currenctly focused (active handle in LayerSlider)
Keys.onUpPressed: sliderLabelRoot.setValue(sliderLabelRoot.value + ((event.modifiers & Qt.ShiftModifier) ? 10 : 1))
Keys.onDownPressed: sliderLabelRoot.setValue(sliderLabelRoot.value - ((event.modifiers & Qt.ShiftModifier) ? 10 : 1))
style: TextFieldStyle {
textColor: UM.Theme.getColor("setting_control_text")
font: UM.Theme.getFont("default")
background: Item { }
}
onEditingFinished: {
// Ensure that the cursor is at the first position. On some systems the text isn't fully visible
// Seems to have to do something with different dpi densities that QML doesn't quite handle.
// Another option would be to increase the size even further, but that gives pretty ugly results.
cursorPosition = 0
if (valueLabel.text != "") {
// -startFrom because we need to convert back to an array structure
sliderLabelRoot.setValue(parseInt(valueLabel.text) - startFrom)
}
}
validator: IntValidator {
bottom:startFrom
top: sliderLabelRoot.maximumValue + startFrom // +startFrom because maybe we want to start in a different value rather than 0
}
}
BusyIndicator {
id: busyIndicator
anchors {
left: parent.right
leftMargin: Math.floor(UM.Theme.getSize("default_margin").width / 2)
verticalCenter: parent.verticalCenter
}
width: sliderLabelRoot.height
height: width
visible: sliderLabelRoot.busy
running: sliderLabelRoot.busy
}
}

View file

@ -1,46 +1,46 @@
# Copyright (c) 2015 Ultimaker B.V.
# Copyright (c) 2017 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
import sys
from UM.PluginRegistry import PluginRegistry
from UM.View.View import View
from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
from UM.Resources import Resources
from UM.Event import Event, KeyEvent
from UM.Signal import Signal
from UM.Scene.Selection import Selection
from UM.Math.Color import Color
from UM.Mesh.MeshBuilder import MeshBuilder
from UM.Job import Job
from UM.Preferences import Preferences
from UM.Logger import Logger
from UM.View.GL.OpenGL import OpenGL
from UM.Message import Message
from UM.Application import Application
from UM.View.GL.OpenGLContext import OpenGLContext
from cura.ConvexHullNode import ConvexHullNode
from cura.Settings.ExtruderManager import ExtruderManager
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import QApplication
from . import LayerViewProxy
from UM.Application import Application
from UM.Event import Event, KeyEvent
from UM.Job import Job
from UM.Logger import Logger
from UM.Math.Color import Color
from UM.Mesh.MeshBuilder import MeshBuilder
from UM.Message import Message
from UM.PluginRegistry import PluginRegistry
from UM.Preferences import Preferences
from UM.Resources import Resources
from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
from UM.Scene.Selection import Selection
from UM.Signal import Signal
from UM.View.GL.OpenGL import OpenGL
from UM.View.GL.OpenGLContext import OpenGLContext
from UM.View.View import View
from UM.i18n import i18nCatalog
catalog = i18nCatalog("cura")
from cura.ConvexHullNode import ConvexHullNode
from . import LayerPass
from .NozzleNode import NozzleNode
from .SimulationPass import SimulationPass
from .SimulationViewProxy import SimulationViewProxy
catalog = i18nCatalog("cura")
import numpy
import os.path
## View used to display g-code paths.
class LayerView(View):
# Must match LayerView.qml
class SimulationView(View):
# Must match SimulationView.qml
LAYER_VIEW_TYPE_MATERIAL_TYPE = 0
LAYER_VIEW_TYPE_LINE_TYPE = 1
LAYER_VIEW_TYPE_FEEDRATE = 2
LAYER_VIEW_TYPE_THICKNESS = 3
def __init__(self):
super().__init__()
@ -54,22 +54,29 @@ class LayerView(View):
self._activity = False
self._old_max_layers = 0
self._max_paths = 0
self._current_path_num = 0
self._minimum_path_num = 0
self.currentLayerNumChanged.connect(self._onCurrentLayerNumChanged)
self._busy = False
self._simulation_running = False
self._ghost_shader = None
self._layer_pass = None
self._composite_pass = None
self._old_layer_bindings = None
self._layerview_composite_shader = None
self._simulationview_composite_shader = None
self._old_composite_shader = None
self._global_container_stack = None
self._proxy = LayerViewProxy.LayerViewProxy()
self._proxy = SimulationViewProxy()
self._controller.getScene().getRoot().childrenChanged.connect(self._onSceneChanged)
self._resetSettings()
self._legend_items = None
self._show_travel_moves = False
self._nozzle_node = None
Preferences.getInstance().addPreference("view/top_layer_count", 5)
Preferences.getInstance().addPreference("view/only_show_top_layers", False)
@ -91,7 +98,7 @@ class LayerView(View):
self._compatibility_mode = True # for safety
self._wireprint_warning_message = Message(catalog.i18nc("@info:status", "Cura does not accurately display layers when Wire Printing is enabled"),
title = catalog.i18nc("@info:title", "Layer View"))
title = catalog.i18nc("@info:title", "Simulation View"))
def _resetSettings(self):
self._layer_view_type = 0 # 0 is material color, 1 is color by linetype, 2 is speed
@ -101,17 +108,24 @@ class LayerView(View):
self._show_helpers = 1
self._show_skin = 1
self._show_infill = 1
self.resetLayerData()
def getActivity(self):
return self._activity
def getLayerPass(self):
def setActivity(self, activity):
if self._activity == activity:
return
self._activity = activity
self.activityChanged.emit()
def getSimulationPass(self):
if not self._layer_pass:
# Currently the RenderPass constructor requires a size > 0
# This should be fixed in RenderPass's constructor.
self._layer_pass = LayerPass.LayerPass(1, 1)
self._layer_pass = SimulationPass(1, 1)
self._compatibility_mode = OpenGLContext.isLegacyOpenGL() or bool(Preferences.getInstance().getValue("view/force_layer_view_compatibility_mode"))
self._layer_pass.setLayerView(self)
self._layer_pass.setSimulationView(self)
return self._layer_pass
def getCurrentLayer(self):
@ -120,13 +134,27 @@ class LayerView(View):
def getMinimumLayer(self):
return self._minimum_layer_num
def _onSceneChanged(self, node):
self.calculateMaxLayers()
def getMaxLayers(self):
return self._max_layers
busyChanged = Signal()
def getCurrentPath(self):
return self._current_path_num
def getMinimumPath(self):
return self._minimum_path_num
def getMaxPaths(self):
return self._max_paths
def getNozzleNode(self):
if not self._nozzle_node:
self._nozzle_node = NozzleNode()
return self._nozzle_node
def _onSceneChanged(self, node):
self.setActivity(False)
self.calculateMaxLayers()
self.calculateMaxPathsOnLayer(self._current_layer_num)
def isBusy(self):
return self._busy
@ -136,9 +164,19 @@ class LayerView(View):
self._busy = busy
self.busyChanged.emit()
def isSimulationRunning(self):
return self._simulation_running
def setSimulationRunning(self, running):
self._simulation_running = running
def resetLayerData(self):
self._current_layer_mesh = None
self._current_layer_jumps = None
self._max_feedrate = sys.float_info.min
self._min_feedrate = sys.float_info.max
self._max_thickness = sys.float_info.min
self._min_thickness = sys.float_info.max
def beginRendering(self):
scene = self.getController().getScene()
@ -186,15 +224,43 @@ class LayerView(View):
self.currentLayerNumChanged.emit()
def setPath(self, value):
if self._current_path_num != value:
self._current_path_num = value
if self._current_path_num < 0:
self._current_path_num = 0
if self._current_path_num > self._max_paths:
self._current_path_num = self._max_paths
if self._current_path_num < self._minimum_path_num:
self._minimum_path_num = self._current_path_num
self._startUpdateTopLayers()
self.currentPathNumChanged.emit()
def setMinimumPath(self, value):
if self._minimum_path_num != value:
self._minimum_path_num = value
if self._minimum_path_num < 0:
self._minimum_path_num = 0
if self._minimum_path_num > self._max_layers:
self._minimum_path_num = self._max_layers
if self._minimum_path_num > self._current_path_num:
self._current_path_num = self._minimum_path_num
self._startUpdateTopLayers()
self.currentPathNumChanged.emit()
## Set the layer view type
#
# \param layer_view_type integer as in LayerView.qml and this class
def setLayerViewType(self, layer_view_type):
# \param layer_view_type integer as in SimulationView.qml and this class
def setSimulationViewType(self, layer_view_type):
self._layer_view_type = layer_view_type
self.currentLayerNumChanged.emit()
## Return the layer view type, integer as in LayerView.qml and this class
def getLayerViewType(self):
## Return the layer view type, integer as in SimulationView.qml and this class
def getSimulationViewType(self):
return self._layer_view_type
## Set the extruder opacity
@ -243,9 +309,20 @@ class LayerView(View):
def getExtruderCount(self):
return self._extruder_count
def getMinFeedrate(self):
return self._min_feedrate
def getMaxFeedrate(self):
return self._max_feedrate
def getMinThickness(self):
return self._min_thickness
def getMaxThickness(self):
return self._max_thickness
def calculateMaxLayers(self):
scene = self.getController().getScene()
self._activity = True
self._old_max_layers = self._max_layers
## Recalculate num max layers
@ -255,9 +332,16 @@ class LayerView(View):
if not layer_data:
continue
self.setActivity(True)
min_layer_number = sys.maxsize
max_layer_number = -sys.maxsize
for layer_id in layer_data.getLayers():
# Store the max and min feedrates and thicknesses for display purposes
for p in layer_data.getLayer(layer_id).polygons:
self._max_feedrate = max(float(p.lineFeedrates.max()), self._max_feedrate)
self._min_feedrate = min(float(p.lineFeedrates.min()), self._min_feedrate)
self._max_thickness = max(float(p.lineThicknesses.max()), self._max_thickness)
self._min_thickness = min(float(p.lineThicknesses.min()), self._min_thickness)
if max_layer_number < layer_id:
max_layer_number = layer_id
if min_layer_number > layer_id:
@ -281,10 +365,32 @@ class LayerView(View):
self.maxLayersChanged.emit()
self._startUpdateTopLayers()
def calculateMaxPathsOnLayer(self, layer_num):
# Update the currentPath
scene = self.getController().getScene()
for node in DepthFirstIterator(scene.getRoot()):
layer_data = node.callDecoration("getLayerData")
if not layer_data:
continue
layer = layer_data.getLayer(layer_num)
if layer is None:
return
new_max_paths = layer.lineMeshElementCount()
if new_max_paths >= 0 and new_max_paths != self._max_paths:
self._max_paths = new_max_paths
self.maxPathsChanged.emit()
self.setPath(int(new_max_paths))
maxLayersChanged = Signal()
maxPathsChanged = Signal()
currentLayerNumChanged = Signal()
currentPathNumChanged = Signal()
globalStackChanged = Signal()
preferencesChanged = Signal()
busyChanged = Signal()
activityChanged = Signal()
## Hackish way to ensure the proxy is already created, which ensures that the layerview.qml is already created
# as this caused some issues.
@ -308,26 +414,31 @@ class LayerView(View):
return True
if event.type == Event.ViewActivateEvent:
# Make sure the LayerPass is created
layer_pass = self.getLayerPass()
# Make sure the SimulationPass is created
layer_pass = self.getSimulationPass()
self.getRenderer().addRenderPass(layer_pass)
# Make sure the NozzleNode is add to the root
nozzle = self.getNozzleNode()
nozzle.setParent(self.getController().getScene().getRoot())
nozzle.setVisible(False)
Application.getInstance().globalContainerStackChanged.connect(self._onGlobalStackChanged)
self._onGlobalStackChanged()
if not self._layerview_composite_shader:
self._layerview_composite_shader = OpenGL.getInstance().createShaderProgram(os.path.join(PluginRegistry.getInstance().getPluginPath("LayerView"), "layerview_composite.shader"))
if not self._simulationview_composite_shader:
self._simulationview_composite_shader = OpenGL.getInstance().createShaderProgram(os.path.join(PluginRegistry.getInstance().getPluginPath("SimulationView"), "simulationview_composite.shader"))
theme = Application.getInstance().getTheme()
self._layerview_composite_shader.setUniformValue("u_background_color", Color(*theme.getColor("viewport_background").getRgb()))
self._layerview_composite_shader.setUniformValue("u_outline_color", Color(*theme.getColor("model_selection_outline").getRgb()))
self._simulationview_composite_shader.setUniformValue("u_background_color", Color(*theme.getColor("viewport_background").getRgb()))
self._simulationview_composite_shader.setUniformValue("u_outline_color", Color(*theme.getColor("model_selection_outline").getRgb()))
if not self._composite_pass:
self._composite_pass = self.getRenderer().getRenderPass("composite")
self._old_layer_bindings = self._composite_pass.getLayerBindings()[:] # make a copy so we can restore to it later
self._composite_pass.getLayerBindings().append("layerview")
self._composite_pass.getLayerBindings().append("simulationview")
self._old_composite_shader = self._composite_pass.getCompositeShader()
self._composite_pass.setCompositeShader(self._layerview_composite_shader)
self._composite_pass.setCompositeShader(self._simulationview_composite_shader)
elif event.type == Event.ViewDeactivateEvent:
self._wireprint_warning_message.hide()
@ -335,6 +446,7 @@ class LayerView(View):
if self._global_container_stack:
self._global_container_stack.propertyChanged.disconnect(self._onPropertyChanged)
self._nozzle_node.setParent(None)
self.getRenderer().removeRenderPass(self._layer_pass)
self._composite_pass.setLayerBindings(self._old_layer_bindings)
self._composite_pass.setCompositeShader(self._old_composite_shader)
@ -364,6 +476,9 @@ class LayerView(View):
else:
self._wireprint_warning_message.hide()
def _onCurrentLayerNumChanged(self):
self.calculateMaxPathsOnLayer(self._current_layer_num)
def _startUpdateTopLayers(self):
if not self._compatibility_mode:
return
@ -397,7 +512,7 @@ class LayerView(View):
self._compatibility_mode = OpenGLContext.isLegacyOpenGL() or bool(
Preferences.getInstance().getValue("view/force_layer_view_compatibility_mode"))
self.setLayerViewType(int(float(Preferences.getInstance().getValue("layerview/layer_view_type"))));
self.setSimulationViewType(int(float(Preferences.getInstance().getValue("layerview/layer_view_type"))));
for extruder_nr, extruder_opacity in enumerate(Preferences.getInstance().getValue("layerview/extruder_opacities").split("|")):
try:

View file

@ -0,0 +1,707 @@
// Copyright (c) 2017 Ultimaker B.V.
// Cura is released under the terms of the LGPLv3 or higher.
import QtQuick 2.4
import QtQuick.Controls 1.2
import QtQuick.Layouts 1.1
import QtQuick.Controls.Styles 1.1
import UM 1.0 as UM
import Cura 1.0 as Cura
Item
{
id: base
width: {
if (UM.SimulationView.compatibilityMode) {
return UM.Theme.getSize("layerview_menu_size_compatibility").width;
} else {
return UM.Theme.getSize("layerview_menu_size").width;
}
}
height: {
if (viewSettings.collapsed) {
if (UM.SimulationView.compatibilityMode) {
return UM.Theme.getSize("layerview_menu_size_compatibility_collapsed").height;
}
return UM.Theme.getSize("layerview_menu_size_collapsed").height;
} else if (UM.SimulationView.compatibilityMode) {
return UM.Theme.getSize("layerview_menu_size_compatibility").height;
} else if (UM.Preferences.getValue("layerview/layer_view_type") == 0) {
return UM.Theme.getSize("layerview_menu_size_material_color_mode").height + UM.SimulationView.extruderCount * (UM.Theme.getSize("layerview_row").height + UM.Theme.getSize("layerview_row_spacing").height)
} else {
return UM.Theme.getSize("layerview_menu_size").height + UM.SimulationView.extruderCount * (UM.Theme.getSize("layerview_row").height + UM.Theme.getSize("layerview_row_spacing").height)
}
}
Behavior on height { NumberAnimation { duration: 100 } }
property var buttonTarget: {
if(parent != null)
{
var force_binding = parent.y; // ensure this gets reevaluated when the panel moves
return base.mapFromItem(parent.parent, parent.buttonTarget.x, parent.buttonTarget.y)
}
return Qt.point(0,0)
}
visible: parent != null ? !parent.parent.monitoringPrint: true
Rectangle {
id: layerViewMenu
anchors.right: parent.right
anchors.top: parent.top
width: parent.width
height: parent.height
clip: true
z: layerSlider.z - 1
color: UM.Theme.getColor("tool_panel_background")
border.width: UM.Theme.getSize("default_lining").width
border.color: UM.Theme.getColor("lining")
Button {
id: collapseButton
anchors.top: parent.top
anchors.topMargin: Math.floor(UM.Theme.getSize("default_margin").height + (UM.Theme.getSize("layerview_row").height - UM.Theme.getSize("default_margin").height) / 2)
anchors.right: parent.right
anchors.rightMargin: UM.Theme.getSize("default_margin").width
width: UM.Theme.getSize("standard_arrow").width
height: UM.Theme.getSize("standard_arrow").height
onClicked: viewSettings.collapsed = !viewSettings.collapsed
style: ButtonStyle
{
background: UM.RecolorImage
{
width: control.width
height: control.height
sourceSize.width: width
sourceSize.height: width
color: UM.Theme.getColor("setting_control_text")
source: viewSettings.collapsed ? UM.Theme.getIcon("arrow_left") : UM.Theme.getIcon("arrow_bottom")
}
label: Label{ }
}
}
ColumnLayout {
id: viewSettings
property bool collapsed: false
property var extruder_opacities: UM.Preferences.getValue("layerview/extruder_opacities").split("|")
property bool show_travel_moves: UM.Preferences.getValue("layerview/show_travel_moves")
property bool show_helpers: UM.Preferences.getValue("layerview/show_helpers")
property bool show_skin: UM.Preferences.getValue("layerview/show_skin")
property bool show_infill: UM.Preferences.getValue("layerview/show_infill")
// if we are in compatibility mode, we only show the "line type"
property bool show_legend: UM.SimulationView.compatibilityMode ? true : UM.Preferences.getValue("layerview/layer_view_type") == 1
property bool show_gradient: UM.SimulationView.compatibilityMode ? false : UM.Preferences.getValue("layerview/layer_view_type") == 2 || UM.Preferences.getValue("layerview/layer_view_type") == 3
property bool show_feedrate_gradient: show_gradient && UM.Preferences.getValue("layerview/layer_view_type") == 2
property bool show_thickness_gradient: show_gradient && UM.Preferences.getValue("layerview/layer_view_type") == 3
property bool only_show_top_layers: UM.Preferences.getValue("view/only_show_top_layers")
property int top_layer_count: UM.Preferences.getValue("view/top_layer_count")
anchors.top: parent.top
anchors.topMargin: UM.Theme.getSize("default_margin").height
anchors.left: parent.left
anchors.leftMargin: UM.Theme.getSize("default_margin").width
anchors.right: parent.right
anchors.rightMargin: UM.Theme.getSize("default_margin").width
spacing: UM.Theme.getSize("layerview_row_spacing").height
Label
{
id: layerViewTypesLabel
anchors.left: parent.left
text: catalog.i18nc("@label","Color scheme")
font: UM.Theme.getFont("default");
visible: !UM.SimulationView.compatibilityMode
Layout.fillWidth: true
color: UM.Theme.getColor("setting_control_text")
}
ListModel // matches SimulationView.py
{
id: layerViewTypes
}
Component.onCompleted:
{
layerViewTypes.append({
text: catalog.i18nc("@label:listbox", "Material Color"),
type_id: 0
})
layerViewTypes.append({
text: catalog.i18nc("@label:listbox", "Line Type"),
type_id: 1
})
layerViewTypes.append({
text: catalog.i18nc("@label:listbox", "Feedrate"),
type_id: 2
})
layerViewTypes.append({
text: catalog.i18nc("@label:listbox", "Layer thickness"),
type_id: 3 // these ids match the switching in the shader
})
}
ComboBox
{
id: layerTypeCombobox
anchors.left: parent.left
Layout.fillWidth: true
Layout.preferredWidth: UM.Theme.getSize("layerview_row").width
model: layerViewTypes
visible: !UM.SimulationView.compatibilityMode
style: UM.Theme.styles.combobox
anchors.right: parent.right
onActivated:
{
UM.Preferences.setValue("layerview/layer_view_type", index);
}
Component.onCompleted:
{
currentIndex = UM.SimulationView.compatibilityMode ? 1 : UM.Preferences.getValue("layerview/layer_view_type");
updateLegends(currentIndex);
}
function updateLegends(type_id)
{
// update visibility of legends
viewSettings.show_legend = UM.SimulationView.compatibilityMode || (type_id == 1);
viewSettings.show_gradient = !UM.SimulationView.compatibilityMode && (type_id == 2 || type_id == 3);
viewSettings.show_feedrate_gradient = viewSettings.show_gradient && (type_id == 2);
viewSettings.show_thickness_gradient = viewSettings.show_gradient && (type_id == 3);
}
}
Label
{
id: compatibilityModeLabel
anchors.left: parent.left
text: catalog.i18nc("@label","Compatibility Mode")
font: UM.Theme.getFont("default")
color: UM.Theme.getColor("text")
visible: UM.SimulationView.compatibilityMode
Layout.fillWidth: true
Layout.preferredHeight: UM.Theme.getSize("layerview_row").height
Layout.preferredWidth: UM.Theme.getSize("layerview_row").width
}
Item
{
height: Math.floor(UM.Theme.getSize("default_margin").width / 2)
width: width
}
Connections {
target: UM.Preferences
onPreferenceChanged:
{
layerTypeCombobox.currentIndex = UM.SimulationView.compatibilityMode ? 1 : UM.Preferences.getValue("layerview/layer_view_type");
layerTypeCombobox.updateLegends(layerTypeCombobox.currentIndex);
playButton.pauseSimulation();
viewSettings.extruder_opacities = UM.Preferences.getValue("layerview/extruder_opacities").split("|");
viewSettings.show_travel_moves = UM.Preferences.getValue("layerview/show_travel_moves");
viewSettings.show_helpers = UM.Preferences.getValue("layerview/show_helpers");
viewSettings.show_skin = UM.Preferences.getValue("layerview/show_skin");
viewSettings.show_infill = UM.Preferences.getValue("layerview/show_infill");
viewSettings.only_show_top_layers = UM.Preferences.getValue("view/only_show_top_layers");
viewSettings.top_layer_count = UM.Preferences.getValue("view/top_layer_count");
}
}
Repeater {
model: Cura.ExtrudersModel{}
CheckBox {
id: extrudersModelCheckBox
checked: viewSettings.extruder_opacities[index] > 0.5 || viewSettings.extruder_opacities[index] == undefined || viewSettings.extruder_opacities[index] == ""
onClicked: {
viewSettings.extruder_opacities[index] = checked ? 1.0 : 0.0
UM.Preferences.setValue("layerview/extruder_opacities", viewSettings.extruder_opacities.join("|"));
}
visible: !UM.SimulationView.compatibilityMode
enabled: index + 1 <= 4
Rectangle {
anchors.verticalCenter: parent.verticalCenter
anchors.right: extrudersModelCheckBox.right
width: UM.Theme.getSize("layerview_legend_size").width
height: UM.Theme.getSize("layerview_legend_size").height
color: model.color
radius: width / 2
border.width: UM.Theme.getSize("default_lining").width
border.color: UM.Theme.getColor("lining")
visible: !viewSettings.show_legend & !viewSettings.show_gradient
}
Layout.fillWidth: true
Layout.preferredHeight: UM.Theme.getSize("layerview_row").height + UM.Theme.getSize("default_lining").height
Layout.preferredWidth: UM.Theme.getSize("layerview_row").width
style: UM.Theme.styles.checkbox
Label
{
text: model.name
elide: Text.ElideRight
color: UM.Theme.getColor("setting_control_text")
font: UM.Theme.getFont("default")
anchors.verticalCenter: parent.verticalCenter
anchors.left: extrudersModelCheckBox.left;
anchors.right: extrudersModelCheckBox.right;
anchors.leftMargin: UM.Theme.getSize("checkbox").width + UM.Theme.getSize("default_margin").width /2
anchors.rightMargin: UM.Theme.getSize("default_margin").width * 2
}
}
}
Repeater {
model: ListModel {
id: typesLegendModel
Component.onCompleted:
{
typesLegendModel.append({
label: catalog.i18nc("@label", "Show Travels"),
initialValue: viewSettings.show_travel_moves,
preference: "layerview/show_travel_moves",
colorId: "layerview_move_combing"
});
typesLegendModel.append({
label: catalog.i18nc("@label", "Show Helpers"),
initialValue: viewSettings.show_helpers,
preference: "layerview/show_helpers",
colorId: "layerview_support"
});
typesLegendModel.append({
label: catalog.i18nc("@label", "Show Shell"),
initialValue: viewSettings.show_skin,
preference: "layerview/show_skin",
colorId: "layerview_inset_0"
});
typesLegendModel.append({
label: catalog.i18nc("@label", "Show Infill"),
initialValue: viewSettings.show_infill,
preference: "layerview/show_infill",
colorId: "layerview_infill"
});
}
}
CheckBox {
id: legendModelCheckBox
checked: model.initialValue
onClicked: {
UM.Preferences.setValue(model.preference, checked);
}
Rectangle {
anchors.verticalCenter: parent.verticalCenter
anchors.right: legendModelCheckBox.right
width: UM.Theme.getSize("layerview_legend_size").width
height: UM.Theme.getSize("layerview_legend_size").height
color: UM.Theme.getColor(model.colorId)
border.width: UM.Theme.getSize("default_lining").width
border.color: UM.Theme.getColor("lining")
visible: viewSettings.show_legend
}
Layout.fillWidth: true
Layout.preferredHeight: UM.Theme.getSize("layerview_row").height + UM.Theme.getSize("default_lining").height
Layout.preferredWidth: UM.Theme.getSize("layerview_row").width
style: UM.Theme.styles.checkbox
Label
{
text: label
font: UM.Theme.getFont("default")
elide: Text.ElideRight
color: UM.Theme.getColor("setting_control_text")
anchors.verticalCenter: parent.verticalCenter
anchors.left: legendModelCheckBox.left;
anchors.right: legendModelCheckBox.right;
anchors.leftMargin: UM.Theme.getSize("checkbox").width + UM.Theme.getSize("default_margin").width /2
anchors.rightMargin: UM.Theme.getSize("default_margin").width * 2
}
}
}
CheckBox {
checked: viewSettings.only_show_top_layers
onClicked: {
UM.Preferences.setValue("view/only_show_top_layers", checked ? 1.0 : 0.0);
}
text: catalog.i18nc("@label", "Only Show Top Layers")
visible: UM.SimulationView.compatibilityMode
style: UM.Theme.styles.checkbox
}
CheckBox {
checked: viewSettings.top_layer_count == 5
onClicked: {
UM.Preferences.setValue("view/top_layer_count", checked ? 5 : 1);
}
text: catalog.i18nc("@label", "Show 5 Detailed Layers On Top")
visible: UM.SimulationView.compatibilityMode
style: UM.Theme.styles.checkbox
}
Repeater {
model: ListModel {
id: typesLegendModelNoCheck
Component.onCompleted:
{
typesLegendModelNoCheck.append({
label: catalog.i18nc("@label", "Top / Bottom"),
colorId: "layerview_skin",
});
typesLegendModelNoCheck.append({
label: catalog.i18nc("@label", "Inner Wall"),
colorId: "layerview_inset_x",
});
}
}
Label {
text: label
visible: viewSettings.show_legend
id: typesLegendModelLabel
Rectangle {
anchors.verticalCenter: parent.verticalCenter
anchors.right: typesLegendModelLabel.right
width: UM.Theme.getSize("layerview_legend_size").width
height: UM.Theme.getSize("layerview_legend_size").height
color: UM.Theme.getColor(model.colorId)
border.width: UM.Theme.getSize("default_lining").width
border.color: UM.Theme.getColor("lining")
visible: viewSettings.show_legend
}
Layout.fillWidth: true
Layout.preferredHeight: UM.Theme.getSize("layerview_row").height + UM.Theme.getSize("default_lining").height
Layout.preferredWidth: UM.Theme.getSize("layerview_row").width
color: UM.Theme.getColor("setting_control_text")
font: UM.Theme.getFont("default")
}
}
// Text for the minimum, maximum and units for the feedrates and layer thickness
Item {
id: gradientLegend
visible: viewSettings.show_gradient
width: parent.width
height: UM.Theme.getSize("layerview_row").height
anchors {
topMargin: UM.Theme.getSize("slider_layerview_margin").height
horizontalCenter: parent.horizontalCenter
}
Label {
text: minText()
anchors.left: parent.left
color: UM.Theme.getColor("setting_control_text")
font: UM.Theme.getFont("default")
function minText() {
if (UM.SimulationView.layerActivity && CuraApplication.platformActivity) {
// Feedrate selected
if (UM.Preferences.getValue("layerview/layer_view_type") == 2) {
return parseFloat(UM.SimulationView.getMinFeedrate()).toFixed(2)
}
// Layer thickness selected
if (UM.Preferences.getValue("layerview/layer_view_type") == 3) {
return parseFloat(UM.SimulationView.getMinThickness()).toFixed(2)
}
}
return catalog.i18nc("@label","min")
}
}
Label {
text: unitsText()
anchors.horizontalCenter: parent.horizontalCenter
color: UM.Theme.getColor("setting_control_text")
font: UM.Theme.getFont("default")
function unitsText() {
if (UM.SimulationView.layerActivity && CuraApplication.platformActivity) {
// Feedrate selected
if (UM.Preferences.getValue("layerview/layer_view_type") == 2) {
return "mm/s"
}
// Layer thickness selected
if (UM.Preferences.getValue("layerview/layer_view_type") == 3) {
return "mm"
}
}
return ""
}
}
Label {
text: maxText()
anchors.right: parent.right
color: UM.Theme.getColor("setting_control_text")
font: UM.Theme.getFont("default")
function maxText() {
if (UM.SimulationView.layerActivity && CuraApplication.platformActivity) {
// Feedrate selected
if (UM.Preferences.getValue("layerview/layer_view_type") == 2) {
return parseFloat(UM.SimulationView.getMaxFeedrate()).toFixed(2)
}
// Layer thickness selected
if (UM.Preferences.getValue("layerview/layer_view_type") == 3) {
return parseFloat(UM.SimulationView.getMaxThickness()).toFixed(2)
}
}
return catalog.i18nc("@label","max")
}
}
}
// Gradient colors for feedrate
Rectangle { // In QML 5.9 can be changed by LinearGradient
// Invert values because then the bar is rotated 90 degrees
id: feedrateGradient
visible: viewSettings.show_feedrate_gradient
anchors.left: parent.right
height: parent.width
width: UM.Theme.getSize("layerview_row").height * 1.5
border.width: UM.Theme.getSize("default_lining").width
border.color: UM.Theme.getColor("lining")
transform: Rotation {origin.x: 0; origin.y: 0; angle: 90}
gradient: Gradient {
GradientStop {
position: 0.000
color: Qt.rgba(1, 0.5, 0, 1)
}
GradientStop {
position: 0.625
color: Qt.rgba(0.375, 0.5, 0, 1)
}
GradientStop {
position: 0.75
color: Qt.rgba(0.25, 1, 0, 1)
}
GradientStop {
position: 1.0
color: Qt.rgba(0, 0, 1, 1)
}
}
}
// Gradient colors for layer thickness
Rectangle { // In QML 5.9 can be changed by LinearGradient
// Invert values because then the bar is rotated 90 degrees
id: thicknessGradient
visible: viewSettings.show_thickness_gradient
anchors.left: parent.right
height: parent.width
width: UM.Theme.getSize("layerview_row").height * 1.5
border.width: UM.Theme.getSize("default_lining").width
border.color: UM.Theme.getColor("lining")
transform: Rotation {origin.x: 0; origin.y: 0; angle: 90}
gradient: Gradient {
GradientStop {
position: 0.000
color: Qt.rgba(1, 0, 0, 1)
}
GradientStop {
position: 0.25
color: Qt.rgba(0.5, 0.5, 0, 1)
}
GradientStop {
position: 0.5
color: Qt.rgba(0, 1, 0, 1)
}
GradientStop {
position: 0.75
color: Qt.rgba(0, 0.5, 0.5, 1)
}
GradientStop {
position: 1.0
color: Qt.rgba(0, 0, 1, 1)
}
}
}
}
}
Item {
id: slidersBox
width: parent.width
visible: UM.SimulationView.layerActivity && CuraApplication.platformActivity
anchors {
top: parent.bottom
topMargin: UM.Theme.getSize("slider_layerview_margin").height
left: parent.left
}
PathSlider {
id: pathSlider
height: UM.Theme.getSize("slider_handle").width
anchors.left: playButton.right
anchors.leftMargin: UM.Theme.getSize("default_margin").width
anchors.right: parent.right
visible: !UM.SimulationView.compatibilityMode
// custom properties
handleValue: UM.SimulationView.currentPath
maximumValue: UM.SimulationView.numPaths
handleSize: UM.Theme.getSize("slider_handle").width
trackThickness: UM.Theme.getSize("slider_groove").width
trackColor: UM.Theme.getColor("slider_groove")
trackBorderColor: UM.Theme.getColor("slider_groove_border")
handleColor: UM.Theme.getColor("slider_handle")
handleActiveColor: UM.Theme.getColor("slider_handle_active")
rangeColor: UM.Theme.getColor("slider_groove_fill")
// update values when layer data changes
Connections {
target: UM.SimulationView
onMaxPathsChanged: pathSlider.setHandleValue(UM.SimulationView.currentPath)
onCurrentPathChanged: pathSlider.setHandleValue(UM.SimulationView.currentPath)
}
// make sure the slider handlers show the correct value after switching views
Component.onCompleted: {
pathSlider.setHandleValue(UM.SimulationView.currentPath)
}
}
LayerSlider {
id: layerSlider
width: UM.Theme.getSize("slider_handle").width
height: UM.Theme.getSize("layerview_menu_size").height
anchors {
top: !UM.SimulationView.compatibilityMode ? pathSlider.bottom : parent.top
topMargin: !UM.SimulationView.compatibilityMode ? UM.Theme.getSize("default_margin").height : 0
right: parent.right
rightMargin: UM.Theme.getSize("slider_layerview_margin").width
}
// custom properties
upperValue: UM.SimulationView.currentLayer
lowerValue: UM.SimulationView.minimumLayer
maximumValue: UM.SimulationView.numLayers
handleSize: UM.Theme.getSize("slider_handle").width
trackThickness: UM.Theme.getSize("slider_groove").width
trackColor: UM.Theme.getColor("slider_groove")
trackBorderColor: UM.Theme.getColor("slider_groove_border")
upperHandleColor: UM.Theme.getColor("slider_handle")
lowerHandleColor: UM.Theme.getColor("slider_handle")
rangeHandleColor: UM.Theme.getColor("slider_groove_fill")
handleActiveColor: UM.Theme.getColor("slider_handle_active")
handleLabelWidth: UM.Theme.getSize("slider_layerview_background").width
// update values when layer data changes
Connections {
target: UM.SimulationView
onMaxLayersChanged: layerSlider.setUpperValue(UM.SimulationView.currentLayer)
onMinimumLayerChanged: layerSlider.setLowerValue(UM.SimulationView.minimumLayer)
onCurrentLayerChanged: layerSlider.setUpperValue(UM.SimulationView.currentLayer)
}
// make sure the slider handlers show the correct value after switching views
Component.onCompleted: {
layerSlider.setLowerValue(UM.SimulationView.minimumLayer)
layerSlider.setUpperValue(UM.SimulationView.currentLayer)
}
}
// Play simulation button
Button {
id: playButton
iconSource: "./resources/simulation_resume.svg"
style: UM.Theme.styles.small_tool_button
visible: !UM.SimulationView.compatibilityMode
anchors {
verticalCenter: pathSlider.verticalCenter
}
property var status: 0 // indicates if it's stopped (0) or playing (1)
onClicked: {
switch(status) {
case 0: {
resumeSimulation()
break
}
case 1: {
pauseSimulation()
break
}
}
}
function pauseSimulation() {
UM.SimulationView.setSimulationRunning(false)
iconSource = "./resources/simulation_resume.svg"
simulationTimer.stop()
status = 0
}
function resumeSimulation() {
UM.SimulationView.setSimulationRunning(true)
iconSource = "./resources/simulation_pause.svg"
simulationTimer.start()
}
}
Timer
{
id: simulationTimer
interval: 100
running: false
repeat: true
onTriggered: {
var currentPath = UM.SimulationView.currentPath
var numPaths = UM.SimulationView.numPaths
var currentLayer = UM.SimulationView.currentLayer
var numLayers = UM.SimulationView.numLayers
// When the user plays the simulation, if the path slider is at the end of this layer, we start
// the simulation at the beginning of the current layer.
if (playButton.status == 0)
{
if (currentPath >= numPaths)
{
UM.SimulationView.setCurrentPath(0)
}
else
{
UM.SimulationView.setCurrentPath(currentPath+1)
}
}
// If the simulation is already playing and we reach the end of a layer, then it automatically
// starts at the beginning of the next layer.
else
{
if (currentPath >= numPaths)
{
// At the end of the model, the simulation stops
if (currentLayer >= numLayers)
{
playButton.pauseSimulation()
}
else
{
UM.SimulationView.setCurrentLayer(currentLayer+1)
UM.SimulationView.setCurrentPath(0)
}
}
else
{
UM.SimulationView.setCurrentPath(currentPath+1)
}
}
playButton.status = 1
}
}
}
FontMetrics {
id: fontMetrics
font: UM.Theme.getFont("default")
}
}

View file

@ -0,0 +1,259 @@
# Copyright (c) 2017 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from PyQt5.QtCore import QObject, pyqtSignal, pyqtProperty
from UM.FlameProfiler import pyqtSlot
from UM.Application import Application
import SimulationView
class SimulationViewProxy(QObject):
def __init__(self, parent=None):
super().__init__(parent)
self._current_layer = 0
self._controller = Application.getInstance().getController()
self._controller.activeViewChanged.connect(self._onActiveViewChanged)
self._onActiveViewChanged()
self.is_simulationView_selected = False
currentLayerChanged = pyqtSignal()
currentPathChanged = pyqtSignal()
maxLayersChanged = pyqtSignal()
maxPathsChanged = pyqtSignal()
activityChanged = pyqtSignal()
globalStackChanged = pyqtSignal()
preferencesChanged = pyqtSignal()
busyChanged = pyqtSignal()
@pyqtProperty(bool, notify=activityChanged)
def layerActivity(self):
active_view = self._controller.getActiveView()
if isinstance(active_view, SimulationView.SimulationView.SimulationView):
return active_view.getActivity()
return False
@pyqtProperty(int, notify=maxLayersChanged)
def numLayers(self):
active_view = self._controller.getActiveView()
if isinstance(active_view, SimulationView.SimulationView.SimulationView):
return active_view.getMaxLayers()
return 0
@pyqtProperty(int, notify=currentLayerChanged)
def currentLayer(self):
active_view = self._controller.getActiveView()
if isinstance(active_view, SimulationView.SimulationView.SimulationView):
return active_view.getCurrentLayer()
return 0
@pyqtProperty(int, notify=currentLayerChanged)
def minimumLayer(self):
active_view = self._controller.getActiveView()
if isinstance(active_view, SimulationView.SimulationView.SimulationView):
return active_view.getMinimumLayer()
return 0
@pyqtProperty(int, notify=maxPathsChanged)
def numPaths(self):
active_view = self._controller.getActiveView()
if isinstance(active_view, SimulationView.SimulationView.SimulationView):
return active_view.getMaxPaths()
return 0
@pyqtProperty(int, notify=currentPathChanged)
def currentPath(self):
active_view = self._controller.getActiveView()
if isinstance(active_view, SimulationView.SimulationView.SimulationView):
return active_view.getCurrentPath()
return 0
@pyqtProperty(int, notify=currentPathChanged)
def minimumPath(self):
active_view = self._controller.getActiveView()
if isinstance(active_view, SimulationView.SimulationView.SimulationView):
return active_view.getMinimumPath()
return 0
@pyqtProperty(bool, notify=busyChanged)
def busy(self):
active_view = self._controller.getActiveView()
if isinstance(active_view, SimulationView.SimulationView.SimulationView):
return active_view.isBusy()
return False
@pyqtProperty(bool, notify=preferencesChanged)
def compatibilityMode(self):
active_view = self._controller.getActiveView()
if isinstance(active_view, SimulationView.SimulationView.SimulationView):
return active_view.getCompatibilityMode()
return False
@pyqtSlot(int)
def setCurrentLayer(self, layer_num):
active_view = self._controller.getActiveView()
if isinstance(active_view, SimulationView.SimulationView.SimulationView):
active_view.setLayer(layer_num)
@pyqtSlot(int)
def setMinimumLayer(self, layer_num):
active_view = self._controller.getActiveView()
if isinstance(active_view, SimulationView.SimulationView.SimulationView):
active_view.setMinimumLayer(layer_num)
@pyqtSlot(int)
def setCurrentPath(self, path_num):
active_view = self._controller.getActiveView()
if isinstance(active_view, SimulationView.SimulationView.SimulationView):
active_view.setPath(path_num)
@pyqtSlot(int)
def setMinimumPath(self, path_num):
active_view = self._controller.getActiveView()
if isinstance(active_view, SimulationView.SimulationView.SimulationView):
active_view.setMinimumPath(path_num)
@pyqtSlot(int)
def setSimulationViewType(self, layer_view_type):
active_view = self._controller.getActiveView()
if isinstance(active_view, SimulationView.SimulationView.SimulationView):
active_view.setSimulationViewisinstance(layer_view_type)
@pyqtSlot(result=int)
def getSimulationViewType(self):
active_view = self._controller.getActiveView()
if isinstance(active_view, SimulationView.SimulationView.SimulationView):
return active_view.getSimulationViewType()
return 0
@pyqtSlot(bool)
def setSimulationRunning(self, running):
active_view = self._controller.getActiveView()
if isinstance(active_view, SimulationView.SimulationView.SimulationView):
active_view.setSimulationRunning(running)
@pyqtSlot(result=bool)
def getSimulationRunning(self):
active_view = self._controller.getActiveView()
if isinstance(active_view, SimulationView.SimulationView.SimulationView):
return active_view.isSimulationRunning()
return False
@pyqtSlot(result=float)
def getMinFeedrate(self):
active_view = self._controller.getActiveView()
if isinstance(active_view, SimulationView.SimulationView.SimulationView):
return active_view.getMinFeedrate()
return 0
@pyqtSlot(result=float)
def getMaxFeedrate(self):
active_view = self._controller.getActiveView()
if isinstance(active_view, SimulationView.SimulationView.SimulationView):
return active_view.getMaxFeedrate()
return 0
@pyqtSlot(result=float)
def getMinThickness(self):
active_view = self._controller.getActiveView()
if isinstance(active_view, SimulationView.SimulationView.SimulationView):
return active_view.getMinThickness()
return 0
@pyqtSlot(result=float)
def getMaxThickness(self):
active_view = self._controller.getActiveView()
if isinstance(active_view, SimulationView.SimulationView.SimulationView):
return active_view.getMaxThickness()
return 0
# Opacity 0..1
@pyqtSlot(int, float)
def setExtruderOpacity(self, extruder_nr, opacity):
active_view = self._controller.getActiveView()
if isinstance(active_view, SimulationView.SimulationView.SimulationView):
active_view.setExtruderOpacity(extruder_nr, opacity)
@pyqtSlot(int)
def setShowTravelMoves(self, show):
active_view = self._controller.getActiveView()
if isinstance(active_view, SimulationView.SimulationView.SimulationView):
active_view.setShowTravelMoves(show)
@pyqtSlot(int)
def setShowHelpers(self, show):
active_view = self._controller.getActiveView()
if isinstance(active_view, SimulationView.SimulationView.SimulationView):
active_view.setShowHelpers(show)
@pyqtSlot(int)
def setShowSkin(self, show):
active_view = self._controller.getActiveView()
if isinstance(active_view, SimulationView.SimulationView.SimulationView):
active_view.setShowSkin(show)
@pyqtSlot(int)
def setShowInfill(self, show):
active_view = self._controller.getActiveView()
if isinstance(active_view, SimulationView.SimulationView.SimulationView):
active_view.setShowInfill(show)
@pyqtProperty(int, notify=globalStackChanged)
def extruderCount(self):
active_view = self._controller.getActiveView()
if isinstance(active_view, SimulationView.SimulationView.SimulationView):
return active_view.getExtruderCount()
return 0
def _layerActivityChanged(self):
self.activityChanged.emit()
def _onLayerChanged(self):
self.currentLayerChanged.emit()
self._layerActivityChanged()
def _onPathChanged(self):
self.currentPathChanged.emit()
self._layerActivityChanged()
def _onMaxLayersChanged(self):
self.maxLayersChanged.emit()
def _onMaxPathsChanged(self):
self.maxPathsChanged.emit()
def _onBusyChanged(self):
self.busyChanged.emit()
def _onActivityChanged(self):
self.activityChanged.emit()
def _onGlobalStackChanged(self):
self.globalStackChanged.emit()
def _onPreferencesChanged(self):
self.preferencesChanged.emit()
def _onActiveViewChanged(self):
active_view = self._controller.getActiveView()
if isinstance(active_view, SimulationView.SimulationView.SimulationView):
# remove other connection if once the SimulationView was created.
if self.is_simulationView_selected:
active_view.currentLayerNumChanged.disconnect(self._onLayerChanged)
active_view.currentPathNumChanged.disconnect(self._onPathChanged)
active_view.maxLayersChanged.disconnect(self._onMaxLayersChanged)
active_view.maxPathsChanged.disconnect(self._onMaxPathsChanged)
active_view.busyChanged.disconnect(self._onBusyChanged)
active_view.activityChanged.disconnect(self._onActivityChanged)
active_view.globalStackChanged.disconnect(self._onGlobalStackChanged)
active_view.preferencesChanged.disconnect(self._onPreferencesChanged)
self.is_simulationView_selected = True
active_view.currentLayerNumChanged.connect(self._onLayerChanged)
active_view.currentPathNumChanged.connect(self._onPathChanged)
active_view.maxLayersChanged.connect(self._onMaxLayersChanged)
active_view.maxPathsChanged.connect(self._onMaxPathsChanged)
active_view.busyChanged.connect(self._onBusyChanged)
active_view.activityChanged.connect(self._onActivityChanged)
active_view.globalStackChanged.connect(self._onGlobalStackChanged)
active_view.preferencesChanged.connect(self._onPreferencesChanged)

View file

@ -0,0 +1,26 @@
# Copyright (c) 2017 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from PyQt5.QtQml import qmlRegisterSingletonType
from UM.i18n import i18nCatalog
from . import SimulationViewProxy, SimulationView
catalog = i18nCatalog("cura")
def getMetaData():
return {
"view": {
"name": catalog.i18nc("@item:inlistbox", "Layer view"),
"view_panel": "SimulationView.qml",
"weight": 2
}
}
def createSimulationViewProxy(engine, script_engine):
return SimulationViewProxy.SimulatorViewProxy()
def register(app):
simulation_view = SimulationView.SimulationView()
qmlRegisterSingletonType(SimulationViewProxy.SimulationViewProxy, "UM", 1, 0, "SimulationView", simulation_view.getProxy)
return { "view": SimulationView.SimulationView()}

View file

@ -6,6 +6,10 @@ vertex41core =
uniform highp mat4 u_modelMatrix;
uniform highp mat4 u_viewProjectionMatrix;
uniform lowp float u_active_extruder;
uniform lowp float u_max_feedrate;
uniform lowp float u_min_feedrate;
uniform lowp float u_max_thickness;
uniform lowp float u_min_thickness;
uniform lowp int u_layer_view_type;
uniform lowp vec4 u_extruder_opacity; // currently only for max 4 extruders, others always visible
@ -18,6 +22,8 @@ vertex41core =
in highp vec2 a_line_dim; // line width and thickness
in highp float a_extruder;
in highp float a_line_type;
in highp float a_feedrate;
in highp float a_thickness;
out lowp vec4 v_color;
@ -32,6 +38,28 @@ vertex41core =
out highp vec3 f_vertex;
out highp vec3 f_normal;
vec4 feedrateGradientColor(float abs_value, float min_value, float max_value)
{
float value = (abs_value - min_value)/(max_value - min_value);
float red = value;
float green = 1-abs(1-4*value);
if (value > 0.375)
{
green = 0.5;
}
float blue = max(1-4*value, 0);
return vec4(red, green, blue, 1.0);
}
vec4 layerThicknessGradientColor(float abs_value, float min_value, float max_value)
{
float value = (abs_value - min_value)/(max_value - min_value);
float red = max(2*value-1, 0);
float green = 1-abs(1-2*value);
float blue = max(1-2*value, 0);
return vec4(red, green, blue, 1.0);
}
void main()
{
vec4 v1_vertex = a_vertex;
@ -48,6 +76,12 @@ vertex41core =
case 1: // "Line type"
v_color = a_color;
break;
case 2: // "Feedrate"
v_color = feedrateGradientColor(a_feedrate, u_min_feedrate, u_max_feedrate);
break;
case 3: // "Layer thickness"
v_color = layerThicknessGradientColor(a_line_dim.y, u_min_thickness, u_max_thickness);
break;
}
v_vertex = world_space_vert.xyz;
@ -153,20 +187,27 @@ geometry41core =
myEmitVertex(v_vertex[1], v_color[1], g_vertex_normal_vert, u_viewProjectionMatrix * (gl_in[1].gl_Position - g_vertex_offset_horz + g_vertex_offset_vert));
myEmitVertex(v_vertex[1], v_color[1], g_vertex_normal_vert, u_viewProjectionMatrix * (gl_in[1].gl_Position + g_vertex_offset_horz + g_vertex_offset_vert));
myEmitVertex(v_vertex[1], v_color[1], g_vertex_normal_vert, u_viewProjectionMatrix * (gl_in[1].gl_Position - g_vertex_offset_horz_head + g_vertex_offset_vert));
//And reverse so that the line is also visible from the back side.
myEmitVertex(v_vertex[1], v_color[1], g_vertex_normal_vert, u_viewProjectionMatrix * (gl_in[1].gl_Position + g_vertex_offset_horz + g_vertex_offset_vert));
myEmitVertex(v_vertex[1], v_color[1], g_vertex_normal_vert, u_viewProjectionMatrix * (gl_in[1].gl_Position - g_vertex_offset_horz + g_vertex_offset_vert));
myEmitVertex(v_vertex[0], v_color[0], g_vertex_normal_vert, u_viewProjectionMatrix * (gl_in[0].gl_Position + g_vertex_offset_horz + g_vertex_offset_vert));
myEmitVertex(v_vertex[0], v_color[0], g_vertex_normal_vert, u_viewProjectionMatrix * (gl_in[0].gl_Position - g_vertex_offset_horz + g_vertex_offset_vert));
myEmitVertex(v_vertex[0], v_color[0], g_vertex_normal_vert, u_viewProjectionMatrix * (gl_in[0].gl_Position + g_vertex_offset_horz_head + g_vertex_offset_vert));
myEmitVertex(v_vertex[0], v_color[0], g_vertex_normal_vert, u_viewProjectionMatrix * (gl_in[0].gl_Position + g_vertex_offset_horz + g_vertex_offset_vert));
EndPrimitive();
} else {
// All normal lines are rendered as 3d tubes.
myEmitVertex(v_vertex[0], v_color[0], g_vertex_normal_horz, u_viewProjectionMatrix * (gl_in[0].gl_Position + g_vertex_offset_horz));
myEmitVertex(v_vertex[1], v_color[1], g_vertex_normal_horz, u_viewProjectionMatrix * (gl_in[1].gl_Position + g_vertex_offset_horz));
myEmitVertex(v_vertex[0], v_color[0], g_vertex_normal_vert, u_viewProjectionMatrix * (gl_in[0].gl_Position + g_vertex_offset_vert));
myEmitVertex(v_vertex[1], v_color[1], g_vertex_normal_vert, u_viewProjectionMatrix * (gl_in[1].gl_Position + g_vertex_offset_vert));
myEmitVertex(v_vertex[0], v_color[0], -g_vertex_normal_horz, u_viewProjectionMatrix * (gl_in[0].gl_Position - g_vertex_offset_horz));
myEmitVertex(v_vertex[1], v_color[1], -g_vertex_normal_horz, u_viewProjectionMatrix * (gl_in[1].gl_Position - g_vertex_offset_horz));
myEmitVertex(v_vertex[0], v_color[0], -g_vertex_normal_vert, u_viewProjectionMatrix * (gl_in[0].gl_Position - g_vertex_offset_vert));
myEmitVertex(v_vertex[1], v_color[1], -g_vertex_normal_vert, u_viewProjectionMatrix * (gl_in[1].gl_Position - g_vertex_offset_vert));
myEmitVertex(v_vertex[0], v_color[0], g_vertex_normal_vert, u_viewProjectionMatrix * (gl_in[0].gl_Position + g_vertex_offset_vert));
myEmitVertex(v_vertex[1], v_color[1], g_vertex_normal_vert, u_viewProjectionMatrix * (gl_in[1].gl_Position + g_vertex_offset_vert));
myEmitVertex(v_vertex[0], v_color[0], g_vertex_normal_horz, u_viewProjectionMatrix * (gl_in[0].gl_Position + g_vertex_offset_horz));
myEmitVertex(v_vertex[1], v_color[1], g_vertex_normal_horz, u_viewProjectionMatrix * (gl_in[1].gl_Position + g_vertex_offset_horz));
myEmitVertex(v_vertex[0], v_color[0], -g_vertex_normal_vert, u_viewProjectionMatrix * (gl_in[0].gl_Position - g_vertex_offset_vert));
myEmitVertex(v_vertex[1], v_color[1], -g_vertex_normal_vert, u_viewProjectionMatrix * (gl_in[1].gl_Position - g_vertex_offset_vert));
myEmitVertex(v_vertex[0], v_color[0], -g_vertex_normal_horz, u_viewProjectionMatrix * (gl_in[0].gl_Position - g_vertex_offset_horz));
myEmitVertex(v_vertex[1], v_color[1], -g_vertex_normal_horz, u_viewProjectionMatrix * (gl_in[1].gl_Position - g_vertex_offset_horz));
EndPrimitive();
@ -247,6 +288,12 @@ u_show_helpers = 1
u_show_skin = 1
u_show_infill = 1
u_min_feedrate = 0
u_max_feedrate = 1
u_min_thickness = 0
u_max_thickness = 1
[bindings]
u_modelViewProjectionMatrix = model_view_projection_matrix
u_modelMatrix = model_matrix
@ -262,3 +309,5 @@ a_line_dim = line_dim
a_extruder = extruder
a_material_color = material_color
a_line_type = line_type
a_feedrate = feedrate
a_thickness = thickness

View file

@ -0,0 +1,256 @@
[shaders]
vertex41core =
#version 410
uniform highp mat4 u_modelViewProjectionMatrix;
uniform highp mat4 u_modelMatrix;
uniform highp mat4 u_viewProjectionMatrix;
uniform lowp float u_active_extruder;
uniform lowp vec4 u_extruder_opacity; // currently only for max 4 extruders, others always visible
uniform highp mat4 u_normalMatrix;
in highp vec4 a_vertex;
in lowp vec4 a_color;
in lowp vec4 a_grayColor;
in lowp vec4 a_material_color;
in highp vec4 a_normal;
in highp vec2 a_line_dim; // line width and thickness
in highp float a_extruder;
in highp float a_line_type;
out lowp vec4 v_color;
out highp vec3 v_vertex;
out highp vec3 v_normal;
out lowp vec2 v_line_dim;
out highp int v_extruder;
out highp vec4 v_extruder_opacity;
out float v_line_type;
out lowp vec4 f_color;
out highp vec3 f_vertex;
out highp vec3 f_normal;
void main()
{
vec4 v1_vertex = a_vertex;
v1_vertex.y -= a_line_dim.y / 2; // half layer down
vec4 world_space_vert = u_modelMatrix * v1_vertex;
gl_Position = world_space_vert;
// shade the color depending on the extruder index stored in the alpha component of the color
v_color = vec4(0.4, 0.4, 0.4, 0.9); // default color for not current layer
v_vertex = world_space_vert.xyz;
v_normal = (u_normalMatrix * normalize(a_normal)).xyz;
v_line_dim = a_line_dim;
v_extruder = int(a_extruder);
v_line_type = a_line_type;
v_extruder_opacity = u_extruder_opacity;
// for testing without geometry shader
f_color = v_color;
f_vertex = v_vertex;
f_normal = v_normal;
}
geometry41core =
#version 410
uniform highp mat4 u_viewProjectionMatrix;
uniform int u_show_travel_moves;
uniform int u_show_helpers;
uniform int u_show_skin;
uniform int u_show_infill;
layout(lines) in;
layout(triangle_strip, max_vertices = 26) out;
in vec4 v_color[];
in vec3 v_vertex[];
in vec3 v_normal[];
in vec2 v_line_dim[];
in int v_extruder[];
in vec4 v_extruder_opacity[];
in float v_line_type[];
out vec4 f_color;
out vec3 f_normal;
out vec3 f_vertex;
// Set the set of variables and EmitVertex
void myEmitVertex(vec3 vertex, vec4 color, vec3 normal, vec4 pos) {
f_vertex = vertex;
f_color = color;
f_normal = normal;
gl_Position = pos;
EmitVertex();
}
void main()
{
vec4 g_vertex_delta;
vec3 g_vertex_normal_horz; // horizontal and vertical in respect to layers
vec4 g_vertex_offset_horz; // vec4 to match gl_in[x].gl_Position
vec3 g_vertex_normal_vert;
vec4 g_vertex_offset_vert;
vec3 g_vertex_normal_horz_head;
vec4 g_vertex_offset_horz_head;
float size_x;
float size_y;
if ((v_extruder_opacity[0][v_extruder[0]] == 0.0) && (v_line_type[0] != 8) && (v_line_type[0] != 9)) {
return;
}
// See LayerPolygon; 8 is MoveCombingType, 9 is RetractionType
if ((u_show_travel_moves == 0) && ((v_line_type[0] == 8) || (v_line_type[0] == 9))) {
return;
}
if ((u_show_helpers == 0) && ((v_line_type[0] == 4) || (v_line_type[0] == 5) || (v_line_type[0] == 7) || (v_line_type[0] == 10))) {
return;
}
if ((u_show_skin == 0) && ((v_line_type[0] == 1) || (v_line_type[0] == 2) || (v_line_type[0] == 3))) {
return;
}
if ((u_show_infill == 0) && (v_line_type[0] == 6)) {
return;
}
if ((v_line_type[0] == 8) || (v_line_type[0] == 9)) {
// fixed size for movements
size_x = 0.05;
} else {
size_x = v_line_dim[1].x / 2 + 0.01; // radius, and make it nicely overlapping
}
size_y = v_line_dim[1].y / 2 + 0.01;
g_vertex_delta = gl_in[1].gl_Position - gl_in[0].gl_Position;
g_vertex_normal_horz_head = normalize(vec3(-g_vertex_delta.x, -g_vertex_delta.y, -g_vertex_delta.z));
g_vertex_offset_horz_head = vec4(g_vertex_normal_horz_head * size_x, 0.0);
g_vertex_normal_horz = normalize(vec3(g_vertex_delta.z, g_vertex_delta.y, -g_vertex_delta.x));
g_vertex_offset_horz = vec4(g_vertex_normal_horz * size_x, 0.0); //size * g_vertex_normal_horz;
g_vertex_normal_vert = vec3(0.0, 1.0, 0.0);
g_vertex_offset_vert = vec4(g_vertex_normal_vert * size_y, 0.0);
if ((v_line_type[0] == 8) || (v_line_type[0] == 9)) {
// Travels: flat plane with pointy ends
myEmitVertex(v_vertex[0], v_color[0], g_vertex_normal_vert, u_viewProjectionMatrix * (gl_in[0].gl_Position + g_vertex_offset_horz + g_vertex_offset_vert));
myEmitVertex(v_vertex[0], v_color[0], g_vertex_normal_vert, u_viewProjectionMatrix * (gl_in[0].gl_Position + g_vertex_offset_horz_head + g_vertex_offset_vert));
myEmitVertex(v_vertex[0], v_color[0], g_vertex_normal_vert, u_viewProjectionMatrix * (gl_in[0].gl_Position - g_vertex_offset_horz + g_vertex_offset_vert));
myEmitVertex(v_vertex[0], v_color[0], g_vertex_normal_vert, u_viewProjectionMatrix * (gl_in[0].gl_Position + g_vertex_offset_horz + g_vertex_offset_vert));
myEmitVertex(v_vertex[1], v_color[1], g_vertex_normal_vert, u_viewProjectionMatrix * (gl_in[1].gl_Position - g_vertex_offset_horz + g_vertex_offset_vert));
myEmitVertex(v_vertex[1], v_color[1], g_vertex_normal_vert, u_viewProjectionMatrix * (gl_in[1].gl_Position + g_vertex_offset_horz + g_vertex_offset_vert));
myEmitVertex(v_vertex[1], v_color[1], g_vertex_normal_vert, u_viewProjectionMatrix * (gl_in[1].gl_Position - g_vertex_offset_horz_head + g_vertex_offset_vert));
EndPrimitive();
} else {
// All normal lines are rendered as 3d tubes.
myEmitVertex(v_vertex[0], v_color[0], g_vertex_normal_horz, u_viewProjectionMatrix * (gl_in[0].gl_Position + g_vertex_offset_horz));
myEmitVertex(v_vertex[1], v_color[1], g_vertex_normal_horz, u_viewProjectionMatrix * (gl_in[1].gl_Position + g_vertex_offset_horz));
myEmitVertex(v_vertex[0], v_color[0], g_vertex_normal_vert, u_viewProjectionMatrix * (gl_in[0].gl_Position + g_vertex_offset_vert));
myEmitVertex(v_vertex[1], v_color[1], g_vertex_normal_vert, u_viewProjectionMatrix * (gl_in[1].gl_Position + g_vertex_offset_vert));
myEmitVertex(v_vertex[0], v_color[0], -g_vertex_normal_horz, u_viewProjectionMatrix * (gl_in[0].gl_Position - g_vertex_offset_horz));
myEmitVertex(v_vertex[1], v_color[1], -g_vertex_normal_horz, u_viewProjectionMatrix * (gl_in[1].gl_Position - g_vertex_offset_horz));
myEmitVertex(v_vertex[0], v_color[0], -g_vertex_normal_vert, u_viewProjectionMatrix * (gl_in[0].gl_Position - g_vertex_offset_vert));
myEmitVertex(v_vertex[1], v_color[1], -g_vertex_normal_vert, u_viewProjectionMatrix * (gl_in[1].gl_Position - g_vertex_offset_vert));
myEmitVertex(v_vertex[0], v_color[0], g_vertex_normal_horz, u_viewProjectionMatrix * (gl_in[0].gl_Position + g_vertex_offset_horz));
myEmitVertex(v_vertex[1], v_color[1], g_vertex_normal_horz, u_viewProjectionMatrix * (gl_in[1].gl_Position + g_vertex_offset_horz));
EndPrimitive();
// left side
myEmitVertex(v_vertex[0], v_color[0], g_vertex_normal_horz, u_viewProjectionMatrix * (gl_in[0].gl_Position + g_vertex_offset_horz));
myEmitVertex(v_vertex[0], v_color[0], g_vertex_normal_vert, u_viewProjectionMatrix * (gl_in[0].gl_Position + g_vertex_offset_vert));
myEmitVertex(v_vertex[0], v_color[0], g_vertex_normal_horz_head, u_viewProjectionMatrix * (gl_in[0].gl_Position + g_vertex_offset_horz_head));
myEmitVertex(v_vertex[0], v_color[0], -g_vertex_normal_horz, u_viewProjectionMatrix * (gl_in[0].gl_Position - g_vertex_offset_horz));
EndPrimitive();
myEmitVertex(v_vertex[0], v_color[0], -g_vertex_normal_horz, u_viewProjectionMatrix * (gl_in[0].gl_Position - g_vertex_offset_horz));
myEmitVertex(v_vertex[0], v_color[0], -g_vertex_normal_vert, u_viewProjectionMatrix * (gl_in[0].gl_Position - g_vertex_offset_vert));
myEmitVertex(v_vertex[0], v_color[0], g_vertex_normal_horz_head, u_viewProjectionMatrix * (gl_in[0].gl_Position + g_vertex_offset_horz_head));
myEmitVertex(v_vertex[0], v_color[0], g_vertex_normal_horz, u_viewProjectionMatrix * (gl_in[0].gl_Position + g_vertex_offset_horz));
EndPrimitive();
// right side
myEmitVertex(v_vertex[1], v_color[1], g_vertex_normal_horz, u_viewProjectionMatrix * (gl_in[1].gl_Position + g_vertex_offset_horz));
myEmitVertex(v_vertex[1], v_color[1], g_vertex_normal_vert, u_viewProjectionMatrix * (gl_in[1].gl_Position + g_vertex_offset_vert));
myEmitVertex(v_vertex[1], v_color[1], -g_vertex_normal_horz_head, u_viewProjectionMatrix * (gl_in[1].gl_Position - g_vertex_offset_horz_head));
myEmitVertex(v_vertex[1], v_color[1], -g_vertex_normal_horz, u_viewProjectionMatrix * (gl_in[1].gl_Position - g_vertex_offset_horz));
EndPrimitive();
myEmitVertex(v_vertex[1], v_color[1], -g_vertex_normal_horz, u_viewProjectionMatrix * (gl_in[1].gl_Position - g_vertex_offset_horz));
myEmitVertex(v_vertex[1], v_color[1], -g_vertex_normal_vert, u_viewProjectionMatrix * (gl_in[1].gl_Position - g_vertex_offset_vert));
myEmitVertex(v_vertex[1], v_color[1], -g_vertex_normal_horz_head, u_viewProjectionMatrix * (gl_in[1].gl_Position - g_vertex_offset_horz_head));
myEmitVertex(v_vertex[1], v_color[1], g_vertex_normal_horz, u_viewProjectionMatrix * (gl_in[1].gl_Position + g_vertex_offset_horz));
EndPrimitive();
}
}
fragment41core =
#version 410
in lowp vec4 f_color;
in lowp vec3 f_normal;
in lowp vec3 f_vertex;
out vec4 frag_color;
uniform mediump vec4 u_ambientColor;
uniform highp vec3 u_lightPosition;
void main()
{
mediump vec4 finalColor = vec4(0.0);
float alpha = f_color.a;
finalColor.rgb += f_color.rgb * 0.3;
highp vec3 normal = normalize(f_normal);
highp vec3 light_dir = normalize(u_lightPosition - f_vertex);
// Diffuse Component
highp float NdotL = clamp(dot(normal, light_dir), 0.0, 1.0);
finalColor += (NdotL * f_color);
finalColor.a = alpha; // Do not change alpha in any way
frag_color = finalColor;
}
[defaults]
u_active_extruder = 0.0
u_extruder_opacity = [1.0, 1.0, 1.0, 1.0]
u_specularColor = [0.4, 0.4, 0.4, 1.0]
u_ambientColor = [0.3, 0.3, 0.3, 0.0]
u_diffuseColor = [1.0, 0.79, 0.14, 1.0]
u_shininess = 20.0
u_show_travel_moves = 0
u_show_helpers = 1
u_show_skin = 1
u_show_infill = 1
[bindings]
u_modelViewProjectionMatrix = model_view_projection_matrix
u_modelMatrix = model_matrix
u_viewProjectionMatrix = view_projection_matrix
u_normalMatrix = normal_matrix
u_lightPosition = light_0_position
[attributes]
a_vertex = vertex
a_color = color
a_grayColor = vec4(0.87, 0.12, 0.45, 1.0)
a_normal = normal
a_line_dim = line_dim
a_extruder = extruder
a_material_color = material_color
a_line_type = line_type

View file

@ -0,0 +1,156 @@
[shaders]
vertex =
uniform highp mat4 u_modelViewProjectionMatrix;
uniform lowp float u_active_extruder;
uniform lowp float u_shade_factor;
uniform highp int u_layer_view_type;
attribute highp float a_extruder;
attribute highp float a_line_type;
attribute highp vec4 a_vertex;
attribute lowp vec4 a_color;
attribute lowp vec4 a_material_color;
varying lowp vec4 v_color;
varying float v_line_type;
void main()
{
gl_Position = u_modelViewProjectionMatrix * a_vertex;
// shade the color depending on the extruder index
v_color = vec4(0.4, 0.4, 0.4, 0.9); // default color for not current layer;
// 8 and 9 are travel moves
// if ((a_line_type != 8.0) && (a_line_type != 9.0)) {
// v_color = (a_extruder == u_active_extruder) ? v_color : vec4(u_shade_factor * v_color.rgb, v_color.a);
// }
v_line_type = a_line_type;
}
fragment =
varying lowp vec4 v_color;
varying float v_line_type;
uniform int u_show_travel_moves;
uniform int u_show_helpers;
uniform int u_show_skin;
uniform int u_show_infill;
void main()
{
if ((u_show_travel_moves == 0) && (v_line_type >= 7.5) && (v_line_type <= 9.5)) { // actually, 8 and 9
// discard movements
discard;
}
// support: 4, 5, 7, 10
if ((u_show_helpers == 0) && (
((v_line_type >= 3.5) && (v_line_type <= 4.5)) ||
((v_line_type >= 6.5) && (v_line_type <= 7.5)) ||
((v_line_type >= 9.5) && (v_line_type <= 10.5)) ||
((v_line_type >= 4.5) && (v_line_type <= 5.5))
)) {
discard;
}
// skin: 1, 2, 3
if ((u_show_skin == 0) && (
(v_line_type >= 0.5) && (v_line_type <= 3.5)
)) {
discard;
}
// infill:
if ((u_show_infill == 0) && (v_line_type >= 5.5) && (v_line_type <= 6.5)) {
// discard movements
discard;
}
gl_FragColor = v_color;
}
vertex41core =
#version 410
uniform highp mat4 u_modelViewProjectionMatrix;
uniform lowp float u_active_extruder;
uniform lowp float u_shade_factor;
uniform highp int u_layer_view_type;
in highp float a_extruder;
in highp float a_line_type;
in highp vec4 a_vertex;
in lowp vec4 a_color;
in lowp vec4 a_material_color;
out lowp vec4 v_color;
out float v_line_type;
void main()
{
gl_Position = u_modelViewProjectionMatrix * a_vertex;
v_color = vec4(0.4, 0.4, 0.4, 0.9); // default color for not current layer
// if ((a_line_type != 8) && (a_line_type != 9)) {
// v_color = (a_extruder == u_active_extruder) ? v_color : vec4(u_shade_factor * v_color.rgb, v_color.a);
// }
v_line_type = a_line_type;
}
fragment41core =
#version 410
in lowp vec4 v_color;
in float v_line_type;
out vec4 frag_color;
uniform int u_show_travel_moves;
uniform int u_show_helpers;
uniform int u_show_skin;
uniform int u_show_infill;
void main()
{
if ((u_show_travel_moves == 0) && (v_line_type >= 7.5) && (v_line_type <= 9.5)) { // actually, 8 and 9
// discard movements
discard;
}
// helpers: 4, 5, 7, 10
if ((u_show_helpers == 0) && (
((v_line_type >= 3.5) && (v_line_type <= 4.5)) ||
((v_line_type >= 6.5) && (v_line_type <= 7.5)) ||
((v_line_type >= 9.5) && (v_line_type <= 10.5)) ||
((v_line_type >= 4.5) && (v_line_type <= 5.5))
)) {
discard;
}
// skin: 1, 2, 3
if ((u_show_skin == 0) && (
(v_line_type >= 0.5) && (v_line_type <= 3.5)
)) {
discard;
}
// infill:
if ((u_show_infill == 0) && (v_line_type >= 5.5) && (v_line_type <= 6.5)) {
// discard movements
discard;
}
frag_color = v_color;
}
[defaults]
u_active_extruder = 0.0
u_shade_factor = 0.60
u_layer_view_type = 0
u_extruder_opacity = [1.0, 1.0, 1.0, 1.0]
u_show_travel_moves = 0
u_show_helpers = 1
u_show_skin = 1
u_show_infill = 1
[bindings]
u_modelViewProjectionMatrix = model_view_projection_matrix
[attributes]
a_vertex = vertex
a_color = color
a_extruder = extruder
a_line_type = line_type
a_material_color = material_color

View file

@ -1,8 +1,8 @@
{
"name": "Layer View",
"name": "Simulation View",
"author": "Ultimaker B.V.",
"version": "1.0.0",
"description": "Provides the Layer view.",
"description": "Provides the Simulation view.",
"api": 4,
"i18n-catalog": "cura"
}

Binary file not shown.

View file

@ -0,0 +1,79 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="6mm"
height="6mm"
viewBox="0 0 5.9999999 6"
version="1.1"
id="svg877"
inkscape:version="0.92.2 (5c3e80d, 2017-08-06)"
sodipodi:docname="simulation_pause2.svg">
<defs
id="defs871" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="15.839192"
inkscape:cx="-5.3551409"
inkscape:cy="17.386031"
inkscape:document-units="mm"
inkscape:current-layer="layer1"
showgrid="false"
inkscape:window-width="2880"
inkscape:window-height="1675"
inkscape:window-x="-13"
inkscape:window-y="-13"
inkscape:window-maximized="1"
fit-margin-top="0"
fit-margin-left="0"
fit-margin-right="0"
fit-margin-bottom="0" />
<metadata
id="metadata874">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(-11.163774,-122.8006)">
<g
id="g825"
transform="matrix(0.26458333,0,0,0.26458333,10.185689,121.85192)">
<rect
y="5"
x="19"
height="20"
width="2.7552757"
id="rect5192"
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:2.34745646;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
<rect
y="5"
x="9"
height="20"
width="2.7552757"
id="rect5192-5"
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:2.34745646;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

View file

@ -0,0 +1,78 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="6mm"
height="6mm"
viewBox="0 0 6 6"
version="1.1"
id="svg8"
inkscape:version="0.92.2 (5c3e80d, 2017-08-06)"
sodipodi:docname="simulation_resume2.svg">
<defs
id="defs2" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="15.839192"
inkscape:cx="-32.404712"
inkscape:cy="14.267522"
inkscape:document-units="mm"
inkscape:current-layer="layer1"
showgrid="false"
inkscape:window-width="2880"
inkscape:window-height="1675"
inkscape:window-x="-13"
inkscape:window-y="-13"
inkscape:window-maximized="1"
fit-margin-top="0"
fit-margin-left="0"
fit-margin-right="0"
fit-margin-bottom="0" />
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(81.024887,-389.647)">
<path
sodipodi:type="star"
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.50520164;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
id="path847"
sodipodi:sides="3"
sodipodi:cx="-78.732257"
sodipodi:cy="392.65222"
sodipodi:r1="3.0592039"
sodipodi:r2="1.5296021"
sodipodi:arg1="0"
sodipodi:arg2="1.0471976"
inkscape:flatsided="true"
inkscape:rounded="0"
inkscape:randomized="0"
d="m -75.67305,392.65222 -4.588806,2.64935 v -5.2987 z"
inkscape:transform-center-x="0.75529536"
inkscape:transform-center-y="0.40090429" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

View file

@ -39,7 +39,7 @@ class SliceInfo(Extension):
Preferences.getInstance().addPreference("info/send_slice_info", True)
Preferences.getInstance().addPreference("info/asked_send_slice_info", False)
if not Preferences.getInstance().getValue("info/asked_send_slice_info"):
if not Preferences.getInstance().getValue("info/asked_send_slice_info") and Preferences.getInstance().getValue("info/send_slice_info"):
self.send_slice_info_message = Message(catalog.i18nc("@info", "Cura collects anonymised slicing statistics. You can disable this in the preferences."),
lifetime = 0,
dismissable = False,
@ -87,15 +87,10 @@ class SliceInfo(Extension):
data["active_machine"] = {"definition_id": global_container_stack.definition.getId(), "manufacturer": global_container_stack.definition.getMetaData().get("manufacturer","")}
# add extruder specific data to slice info
data["extruders"] = []
extruder_count = len(global_container_stack.extruders)
extruders = []
if extruder_count > 1:
extruders = list(ExtruderManager.getInstance().getMachineExtruders(global_container_stack.getId()))
extruders = sorted(extruders, key = lambda extruder: extruder.getMetaDataEntry("position"))
if not extruders:
extruders = [global_container_stack]
extruders = list(ExtruderManager.getInstance().getMachineExtruders(global_container_stack.getId()))
extruders = sorted(extruders, key = lambda extruder: extruder.getMetaDataEntry("position"))
for extruder in extruders:
extruder_dict = dict()
@ -104,7 +99,9 @@ class SliceInfo(Extension):
"type": extruder.material.getMetaData().get("material", ""),
"brand": extruder.material.getMetaData().get("brand", "")
}
extruder_dict["material_used"] = print_information.materialLengths[int(extruder.getMetaDataEntry("position", "0"))]
extruder_position = int(extruder.getMetaDataEntry("position", "0"))
if extruder_position in print_information.materialLengths:
extruder_dict["material_used"] = print_information.materialLengths[extruder_position]
extruder_dict["variant"] = extruder.variant.getName()
extruder_dict["nozzle_size"] = extruder.getProperty("machine_nozzle_size", "value")

View file

@ -27,40 +27,39 @@ class SolidView(View):
self._enabled_shader = None
self._disabled_shader = None
self._non_printing_shader = None
self._extruders_model = ExtrudersModel()
self._theme = None
def beginRendering(self):
scene = self.getController().getScene()
renderer = self.getRenderer()
if not self._theme:
self._theme = Application.getInstance().getTheme()
if not self._enabled_shader:
self._enabled_shader = OpenGL.getInstance().createShaderProgram(Resources.getPath(Resources.Shaders, "overhang.shader"))
theme = Application.getInstance().getTheme()
self._enabled_shader.setUniformValue("u_overhangColor", Color(*theme.getColor("model_overhang").getRgb()))
self._enabled_shader.setUniformValue("u_overhangColor", Color(*self._theme.getColor("model_overhang").getRgb()))
if not self._disabled_shader:
self._disabled_shader = OpenGL.getInstance().createShaderProgram(Resources.getPath(Resources.Shaders, "striped.shader"))
theme = Application.getInstance().getTheme()
self._disabled_shader.setUniformValue("u_diffuseColor1", Color(*theme.getColor("model_unslicable").getRgb()))
self._disabled_shader.setUniformValue("u_diffuseColor2", Color(*theme.getColor("model_unslicable_alt").getRgb()))
self._disabled_shader.setUniformValue("u_diffuseColor1", Color(*self._theme.getColor("model_unslicable").getRgb()))
self._disabled_shader.setUniformValue("u_diffuseColor2", Color(*self._theme.getColor("model_unslicable_alt").getRgb()))
self._disabled_shader.setUniformValue("u_width", 50.0)
multi_extrusion = False
if not self._non_printing_shader:
self._non_printing_shader = OpenGL.getInstance().createShaderProgram(Resources.getPath(Resources.Shaders, "transparent_object.shader"))
self._non_printing_shader.setUniformValue("u_diffuseColor", Color(*self._theme.getColor("model_non_printing").getRgb()))
self._non_printing_shader.setUniformValue("u_opacity", 0.6)
global_container_stack = Application.getInstance().getGlobalContainerStack()
if global_container_stack:
multi_extrusion = global_container_stack.getProperty("machine_extruder_count", "value") > 1
support_extruder_nr = global_container_stack.getProperty("support_extruder_nr", "value")
support_angle_stack = Application.getInstance().getExtruderManager().getExtruderStack(support_extruder_nr)
if multi_extrusion:
support_extruder_nr = global_container_stack.getProperty("support_extruder_nr", "value")
support_angle_stack = ExtruderManager.getInstance().getExtruderStack(support_extruder_nr)
if not support_angle_stack:
support_angle_stack = global_container_stack
else:
support_angle_stack = global_container_stack
if Preferences.getInstance().getValue("view/show_overhang"):
if support_angle_stack is not None and Preferences.getInstance().getValue("view/show_overhang"):
angle = support_angle_stack.getProperty("support_angle", "value")
# Make sure the overhang angle is valid before passing it to the shader
# Note: if the overhang angle is set to its default value, it does not need to get validated (validationState = None)
@ -71,33 +70,34 @@ class SolidView(View):
else:
self._enabled_shader.setUniformValue("u_overhangAngle", math.cos(math.radians(0)))
for node in DepthFirstIterator(scene.getRoot()):
if not node.render(renderer):
if node.getMeshData() and node.isVisible():
uniforms = {}
shade_factor = 1.0
if not multi_extrusion:
if global_container_stack:
material = global_container_stack.findContainer({ "type": "material" })
material_color = material.getMetaDataEntry("color_code", default = self._extruders_model.defaultColors[0]) if material else self._extruders_model.defaultColors[0]
else:
material_color = self._extruders_model.defaultColors[0]
else:
# Get color to render this mesh in from ExtrudersModel
extruder_index = 0
extruder_id = node.callDecoration("getActiveExtruder")
if extruder_id:
extruder_index = max(0, self._extruders_model.find("id", extruder_id))
try:
material_color = self._extruders_model.getItem(extruder_index)["color"]
except KeyError:
material_color = self._extruders_model.defaultColors[0]
per_mesh_stack = node.callDecoration("getStack")
# Get color to render this mesh in from ExtrudersModel
extruder_index = 0
extruder_id = node.callDecoration("getActiveExtruder")
if extruder_id:
extruder_index = max(0, self._extruders_model.find("id", extruder_id))
# Use the support extruder instead of the active extruder if this is a support_mesh
if per_mesh_stack:
if per_mesh_stack.getProperty("support_mesh", "value"):
extruder_index = int(global_container_stack.getProperty("support_extruder_nr", "value"))
try:
material_color = self._extruders_model.getItem(extruder_index)["color"]
except KeyError:
material_color = self._extruders_model.defaultColors[0]
if extruder_index != ExtruderManager.getInstance().activeExtruderIndex:
# Shade objects that are printed with the non-active extruder 25% darker
shade_factor = 0.6
if extruder_index != ExtruderManager.getInstance().activeExtruderIndex:
# Shade objects that are printed with the non-active extruder 25% darker
shade_factor = 0.6
try:
# Colors are passed as rgb hex strings (eg "#ffffff"), and the shader needs
# an rgba list of floats (eg [1.0, 1.0, 1.0, 1.0])
@ -110,19 +110,17 @@ class SolidView(View):
except ValueError:
pass
if hasattr(node, "_outside_buildarea"):
if node._outside_buildarea:
renderer.queueNode(node, shader = self._disabled_shader)
if getattr(node, "_non_printing_mesh", False):
if per_mesh_stack and (per_mesh_stack.getProperty("infill_mesh", "value") or per_mesh_stack.getProperty("cutting_mesh", "value")):
renderer.queueNode(node, shader = self._non_printing_shader, uniforms = uniforms, transparent = True)
else:
renderer.queueNode(node, shader = self._enabled_shader, uniforms = uniforms)
renderer.queueNode(node, shader = self._non_printing_shader, transparent = True)
elif getattr(node, "_outside_buildarea", False):
renderer.queueNode(node, shader = self._disabled_shader)
else:
renderer.queueNode(node, material = self._enabled_shader, uniforms = uniforms)
renderer.queueNode(node, shader = self._enabled_shader, uniforms = uniforms)
if node.callDecoration("isGroup") and Selection.isSelected(node):
renderer.queueNode(scene.getRoot(), mesh = node.getBoundingBoxMesh(), mode = RenderBatch.RenderMode.LineLoop)
def endRendering(self):
pass
#def _onPreferenceChanged(self, preference):
#if preference == "view/show_overhang": ## Todo: This a printer only setting. Should be removed from Uranium.
#self._enabled_material = None

View file

@ -1,17 +1,15 @@
from cura.MachineAction import MachineAction
import os.path
import time
from PyQt5.QtCore import pyqtSignal, pyqtProperty, pyqtSlot, QObject
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
from cura.MachineAction import MachineAction
catalog = i18nCatalog("cura")
class DiscoverUM3Action(MachineAction):
@ -136,17 +134,14 @@ class DiscoverUM3Action(MachineAction):
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)
# Create networking dialog
path = os.path.join(PluginRegistry.getInstance().getPluginPath("UM3NetworkPrinting"), "UM3InfoComponents.qml")
self.__additional_components_view = Application.getInstance().createQmlComponent(path, {"manager": self})
if not self.__additional_components_view:
Logger.log("w", "Could not create ui components for UM3.")
return
# Create extra components
Application.getInstance().addAdditionalComponent("monitorButtons", self.__additional_components_view.findChild(QObject, "networkPrinterConnectButton"))
Application.getInstance().addAdditionalComponent("machinesDetailPane", self.__additional_components_view.findChild(QObject, "networkPrinterConnectionInfo"))

View file

@ -230,13 +230,13 @@ Cura.MachineAction
{
if(base.selectedPrinter.printerType == "ultimaker3")
{
return catalog.i18nc("@label", "Ultimaker 3")
return catalog.i18nc("@label Printer name", "Ultimaker 3")
} else if(base.selectedPrinter.printerType == "ultimaker3_extended")
{
return catalog.i18nc("@label", "Ultimaker 3 Extended")
return catalog.i18nc("@label Printer name", "Ultimaker 3 Extended")
} else
{
return catalog.i18nc("@label", "Unknown") // We have no idea what type it is. Should not happen 'in the field'
return catalog.i18nc("@label Printer name", "Unknown") // We have no idea what type it is. Should not happen 'in the field'
}
}
else

View file

@ -8,12 +8,10 @@ import time
from enum import Enum
from PyQt5.QtNetwork import QNetworkRequest, QHttpPart, QHttpMultiPart
from PyQt5.QtCore import QUrl, QByteArray, pyqtSlot, pyqtProperty, QCoreApplication, QTimer, pyqtSignal, QObject
from PyQt5.QtCore import QUrl, pyqtSlot, pyqtProperty, QCoreApplication, QTimer, pyqtSignal, QObject
from PyQt5.QtGui import QDesktopServices
from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkReply
from PyQt5.QtQml import QQmlComponent, QQmlContext
from UM.Application import Application
from UM.Decorators import override
from UM.Logger import Logger
from UM.Message import Message
from UM.OutputDevice import OutputDeviceError
@ -131,7 +129,7 @@ class NetworkClusterPrinterOutputDevice(NetworkPrinterOutputDevice.NetworkPrinte
@pyqtProperty(QObject, notify=selectedPrinterChanged)
def controlItem(self):
# TODO: Probably not the nicest way to do this. This needs to be done better at some point in time.
if not self._control_component:
if not self._control_item:
self._createControlViewFromQML()
name = self._selected_printer.get("friendly_name")
if name == self._automatic_printer.get("friendly_name") or name == "":
@ -235,17 +233,8 @@ class NetworkClusterPrinterOutputDevice(NetworkPrinterOutputDevice.NetworkPrinte
def spawnPrintView(self):
if self._print_view is None:
path = QUrl.fromLocalFile(os.path.join(self._plugin_path, "PrintWindow.qml"))
component = QQmlComponent(Application.getInstance()._engine, path)
self._print_context = QQmlContext(Application.getInstance()._engine.rootContext())
self._print_context.setContextProperty("OutputDevice", self)
self._print_view = component.create(self._print_context)
if component.isError():
Logger.log("e", " Errors creating component: \n%s", "\n".join(
[e.toString() for e in component.errors()]))
path = os.path.join(self._plugin_path, "PrintWindow.qml")
self._print_view = Application.getInstance().createQmlComponent(path, {"OutputDevice", self})
if self._print_view is not None:
self._print_view.show()
@ -263,6 +252,8 @@ class NetworkClusterPrinterOutputDevice(NetworkPrinterOutputDevice.NetworkPrinte
self._error_message.show()
return
self.writeStarted.emit(self) # Allow postprocessing before sending data to the printer
if len(self._printers) > 1:
self.spawnPrintView() # Ask user how to print it.
elif len(self._printers) == 1:
@ -486,7 +477,7 @@ class NetworkClusterPrinterOutputDevice(NetworkPrinterOutputDevice.NetworkPrinte
printer_name = self.__getPrinterNameFromUuid(print_job["printer_uuid"])
if printer_name is None:
printer_name = i18n_catalog.i18nc("@label", "Unknown")
printer_name = i18n_catalog.i18nc("@label Printer name", "Unknown")
message_text = (i18n_catalog.i18nc("@info:status",
"Printer '{printer_name}' has finished printing '{job_name}'.")
@ -707,7 +698,7 @@ class NetworkClusterPrinterOutputDevice(NetworkPrinterOutputDevice.NetworkPrinte
if self._reply:
self._reply.abort()
self._stage = OutputStage.ready
Application.getInstance().showPrintMonitor.emit(False)
Application.getInstance().getController().setActiveStage("PrepareStage")
@pyqtSlot(int, result=str)
def formatDuration(self, seconds):

View file

@ -672,7 +672,7 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice):
Logger.log("d", "Attempting to perform an action without authentication for printer %s. Auth state is %s", self._key, self._authentication_state)
return
Application.getInstance().showPrintMonitor.emit(True)
Application.getInstance().getController().setActiveStage("MonitorStage")
self._print_finished = True
self.writeStarted.emit(self)
self._gcode = getattr(Application.getInstance().getController().getScene(), "gcode_list")
@ -726,10 +726,10 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice):
remote_material_guid,
material.getMetaDataEntry("GUID"))
remote_materials = UM.Settings.ContainerRegistry.ContainerRegistry.getInstance().findInstanceContainers(type = "material", GUID = remote_material_guid, read_only = True)
remote_materials = UM.Settings.ContainerRegistry.ContainerRegistry.getInstance().findInstanceContainersMetadata(type = "material", GUID = remote_material_guid, read_only = True)
remote_material_name = "Unknown"
if remote_materials:
remote_material_name = remote_materials[0].getName()
remote_material_name = remote_materials[0]["name"]
warnings.append(i18n_catalog.i18nc("@label", "Different material (Cura: {0}, Printer: {1}) selected for extruder {2}").format(material.getName(), remote_material_name, index + 1))
try:
@ -767,7 +767,7 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice):
if button == QMessageBox.Yes:
self.startPrint()
else:
Application.getInstance().showPrintMonitor.emit(False)
Application.getInstance().getController().setActiveStage("PrepareStage")
# For some unknown reason Cura on OSX will hang if we do the call back code
# immediately without first returning and leaving QML's event system.
QTimer.singleShot(100, delayedCallback)
@ -850,7 +850,7 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice):
self._write_finished = True # post_reply does not always exist, so make sure we unblock writing
if self._post_reply:
self._finalizePostReply()
Application.getInstance().showPrintMonitor.emit(False)
Application.getInstance().getController().setActiveStage("PrepareStage")
## Attempt to start a new print.
# This function can fail to actually start a print due to not being authenticated or another print already
@ -971,7 +971,8 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice):
## Send all material profiles to the printer.
def sendMaterialProfiles(self):
for container in UM.Settings.ContainerRegistry.ContainerRegistry.getInstance().findInstanceContainers(type = "material"):
registry = UM.Settings.ContainerRegistry.ContainerRegistry.getInstance()
for container in registry.findInstanceContainers(type = "material"):
try:
xml_data = container.serialize()
if xml_data == "" or xml_data is None:
@ -980,7 +981,7 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice):
names = ContainerManager.getInstance().getLinkedMaterials(container.getId())
if names:
# There are other materials that share this GUID.
if not container.isReadOnly():
if not registry.isReadOnly(container.getId()):
continue # If it's not readonly, it's created by user, so skip it.
material_multi_part = QHttpMultiPart(QHttpMultiPart.FormDataType)

View file

@ -43,6 +43,7 @@ class NetworkPrinterOutputDevicePlugin(QObject, OutputDevicePlugin):
# List of old printer names. This is used to ensure that a refresh of zeroconf does not needlessly forces
# authentication requests.
self._old_printers = []
self._excluded_addresses = ["127.0.0.1"] # Adding a list of not allowed IP addresses. At this moment, just localhost
# Because the model needs to be created in the same thread as the QMLEngine, we use a signal.
self.addPrinterSignal.connect(self.addPrinter)
@ -300,6 +301,9 @@ class NetworkPrinterOutputDevicePlugin(QObject, OutputDevicePlugin):
if type_of_device:
if type_of_device == b"printer":
address = '.'.join(map(lambda n: str(n), info.address))
if address in self._excluded_addresses:
Logger.log("d", "The IP address %s of the printer \'%s\' is not correct. Trying to reconnect.", address, name)
return False # When getting the localhost IP, then try to reconnect
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 )

View file

@ -44,7 +44,7 @@ Rectangle
case "maintenance": // TODO: new string
case "unknown":
default:
return catalog.i18nc("@label", "Unknown");
return catalog.i18nc("@label Printer status", "Unknown");
}
}

View file

@ -13,6 +13,7 @@ from UM.Application import Application
from UM.Logger import Logger
from cura.PrinterOutputDevice import PrinterOutputDevice, ConnectionState
from UM.Message import Message
from UM.Qt.Duration import DurationFormat
from PyQt5.QtCore import QUrl, pyqtSlot, pyqtSignal, pyqtProperty
@ -52,6 +53,8 @@ class USBPrinterOutputDevice(PrinterOutputDevice):
self._heatup_wait_start_time = time.time()
self.jobStateChanged.connect(self._onJobStateChanged)
## Queue for commands that need to be send. Used when command is sent when a print is active.
self._command_queue = queue.Queue()
@ -60,7 +63,7 @@ class USBPrinterOutputDevice(PrinterOutputDevice):
## Set when print is started in order to check running time.
self._print_start_time = None
self._print_start_time_100 = None
self._print_estimated_time = None
## Keep track where in the provided g-code the print is
self._gcode_position = 0
@ -125,6 +128,29 @@ class USBPrinterOutputDevice(PrinterOutputDevice):
def _homeBed(self):
self._sendCommand("G28 Z")
## Updates the target bed temperature from the printer, and emit a signal if it was changed.
#
# /param temperature The new target temperature of the bed.
# /return boolean, True if the temperature was changed, false if the new temperature has the same value as the already stored temperature
def _updateTargetBedTemperature(self, temperature):
if self._target_bed_temperature == temperature:
return False
self._target_bed_temperature = temperature
self.targetBedTemperatureChanged.emit()
return True
## Updates the target hotend temperature from the printer, and emit a signal if it was changed.
#
# /param index The index of the hotend.
# /param temperature The new target temperature of the hotend.
# /return boolean, True if the temperature was changed, false if the new temperature has the same value as the already stored temperature
def _updateTargetHotendTemperature(self, index, temperature):
if self._target_hotend_temperatures[index] == temperature:
return False
self._target_hotend_temperatures[index] = temperature
self.targetHotendTemperaturesChanged.emit()
return True
## A name for the device.
@pyqtProperty(str, constant = True)
def name(self):
@ -164,7 +190,6 @@ class USBPrinterOutputDevice(PrinterOutputDevice):
# Reset line number. If this is not done, first line is sometimes ignored
self._gcode.insert(0, "M110")
self._gcode_position = 0
self._print_start_time_100 = None
self._is_printing = True
self._print_start_time = time.time()
@ -394,7 +419,8 @@ class USBPrinterOutputDevice(PrinterOutputDevice):
self._listen_thread.join()
except:
pass
self._serial.close()
if self._serial is not None: # Avoid a race condition when a thread can change the value of self._serial to None
self._serial.close()
self._listen_thread = threading.Thread(target = self._listen)
self._listen_thread.daemon = True
@ -446,7 +472,6 @@ class USBPrinterOutputDevice(PrinterOutputDevice):
#
# \param nodes A collection of scene nodes to send. This is ignored.
# \param file_name \type{string} A suggestion for a file name to write.
# This is ignored.
# \param filter_by_machine Whether to filter MIME types by machine. This
# is ignored.
# \param kwargs Keyword arguments.
@ -462,7 +487,10 @@ class USBPrinterOutputDevice(PrinterOutputDevice):
self._error_message.show()
return
Application.getInstance().showPrintMonitor.emit(True)
self.setJobName(file_name)
self._print_estimated_time = int(Application.getInstance().getPrintInformation().currentPrintTime.getDisplayString(DurationFormat.Format.Seconds))
Application.getInstance().getController().setActiveStage("MonitorStage")
self.startPrint()
def _setEndstopState(self, endstop_key, value):
@ -482,6 +510,7 @@ class USBPrinterOutputDevice(PrinterOutputDevice):
## Listen thread function.
def _listen(self):
Logger.log("i", "Printer connection listen thread started for %s" % self._serial_port)
container_stack = Application.getInstance().getGlobalContainerStack()
temperature_request_timeout = time.time()
ok_timeout = time.time()
while self._connection_state == ConnectionState.connected:
@ -511,16 +540,40 @@ class USBPrinterOutputDevice(PrinterOutputDevice):
self._setErrorState(line[6:])
elif b" T:" in line or line.startswith(b"T:"): # Temperature message
temperature_matches = re.findall(b"T(\d*): ?([\d\.]+) ?\/?([\d\.]+)?", line)
temperature_set = False
try:
self._setHotendTemperature(self._temperature_requested_extruder_index, float(re.search(b"T: *([0-9\.]*)", line).group(1)))
for match in temperature_matches:
if match[0]:
extruder_nr = int(match[0])
if extruder_nr >= container_stack.getProperty("machine_extruder_count", "value"):
continue
if match[1]:
self._setHotendTemperature(extruder_nr, float(match[1]))
temperature_set = True
if match[2]:
self._updateTargetHotendTemperature(extruder_nr, float(match[2]))
else:
requested_temperatures = match
if not temperature_set and requested_temperatures:
if requested_temperatures[1]:
self._setHotendTemperature(self._temperature_requested_extruder_index, float(requested_temperatures[1]))
if requested_temperatures[2]:
self._updateTargetHotendTemperature(self._temperature_requested_extruder_index, float(requested_temperatures[2]))
except:
pass
if b"B:" in line: # Check if it's a bed temperature
Logger.log("w", "Could not parse hotend temperatures from response: %s", line)
# Check if there's also a bed temperature
temperature_matches = re.findall(b"B: ?([\d\.]+) ?\/?([\d\.]+)?", line)
if container_stack.getProperty("machine_heated_bed", "value") and len(temperature_matches) > 0:
match = temperature_matches[0]
try:
self._setBedTemperature(float(re.search(b"B: *([0-9\.]*)", line).group(1)))
except Exception as e:
pass
#TODO: temperature changed callback
if match[0]:
self._setBedTemperature(float(match[0]))
if match[1]:
self._updateTargetBedTemperature(float(match[1]))
except:
Logger.log("w", "Could not parse bed temperature from response: %s", line)
elif b"_min" in line or b"_max" in line:
tag, value = line.split(b":", 1)
self._setEndstopState(tag,(b"H" in value or b"TRIGGERED" in value))
@ -559,8 +612,6 @@ class USBPrinterOutputDevice(PrinterOutputDevice):
def _sendNextGcodeLine(self):
if self._gcode_position >= len(self._gcode):
return
if self._gcode_position == 100:
self._print_start_time_100 = time.time()
line = self._gcode[self._gcode_position]
if ";" in line:
@ -584,8 +635,18 @@ class USBPrinterOutputDevice(PrinterOutputDevice):
checksum = functools.reduce(lambda x,y: x^y, map(ord, "N%d%s" % (self._gcode_position, line)))
self._sendCommand("N%d%s*%d" % (self._gcode_position, line, checksum))
progress = (self._gcode_position / len(self._gcode))
elapsed_time = int(time.time() - self._print_start_time)
self.setTimeElapsed(elapsed_time)
estimated_time = self._print_estimated_time
if progress > .1:
estimated_time = self._print_estimated_time * (1-progress) + elapsed_time
self.setTimeTotal(estimated_time)
self._gcode_position += 1
self.setProgress((self._gcode_position / len(self._gcode)) * 100)
self.setProgress(progress * 100)
self.progressChanged.emit()
## Set the state of the print.
@ -600,6 +661,13 @@ class USBPrinterOutputDevice(PrinterOutputDevice):
elif job_state == "abort":
self.cancelPrint()
def _onJobStateChanged(self):
# clear the job name & times when printing is done or aborted
if self._job_state == "ready":
self.setJobName("")
self.setTimeElapsed(0)
self.setTimeTotal(0)
## Set the progress of the print.
# It will be normalized (based on max_progress) to range 0 - 100
def setProgress(self, progress, max_progress = 100):
@ -630,7 +698,7 @@ class USBPrinterOutputDevice(PrinterOutputDevice):
self._is_printing = False
self._is_paused = False
self._updateJobState("ready")
Application.getInstance().showPrintMonitor.emit(False)
Application.getInstance().getController().setActiveStage("PrepareStage")
## Check if the process did not encounter an error yet.
def hasError(self):

View file

@ -16,13 +16,11 @@ from cura.CuraApplication import CuraApplication
import threading
import platform
import glob
import time
import os.path
import serial.tools.list_ports
from UM.Extension import Extension
from PyQt5.QtQml import QQmlComponent, QQmlContext
from PyQt5.QtCore import QUrl, QObject, pyqtSlot, pyqtProperty, pyqtSignal, Qt
from UM.i18n import i18nCatalog
i18n_catalog = i18nCatalog("cura")
@ -91,19 +89,16 @@ class USBPrinterOutputDeviceManager(QObject, OutputDevicePlugin, Extension):
# This will create the view if its not already created.
def spawnFirmwareInterface(self, serial_port):
if self._firmware_view is None:
path = QUrl.fromLocalFile(os.path.join(PluginRegistry.getInstance().getPluginPath("USBPrinting"), "FirmwareUpdateWindow.qml"))
component = QQmlComponent(Application.getInstance()._engine, path)
self._firmware_context = QQmlContext(Application.getInstance()._engine.rootContext())
self._firmware_context.setContextProperty("manager", self)
self._firmware_view = component.create(self._firmware_context)
path = os.path.join(PluginRegistry.getInstance().getPluginPath("USBPrinting"), "FirmwareUpdateWindow.qml")
self._firmware_view = Application.getInstance().createQmlComponent(path, {"manager": self})
self._firmware_view.show()
@pyqtSlot(str)
def updateAllFirmware(self, file_name):
if file_name.startswith("file://"):
file_name = QUrl(file_name).toLocalFile() # File dialogs prepend the path with file://, which we don't need / want
file_name = QUrl(file_name).toLocalFile() # File dialogs prepend the path with file://, which we don't need / want
if not self._usb_output_devices:
Message(i18n_catalog.i18nc("@info", "Unable to update firmware because there are no printers connected."), title = i18n_catalog.i18nc("@info:title", "Warning")).show()
return

View file

@ -37,8 +37,7 @@ class UM2UpgradeSelection(MachineAction):
def setHasVariants(self, has_variants = True):
global_container_stack = Application.getInstance().getGlobalContainerStack()
if global_container_stack:
variant_container = global_container_stack.variant
variant_index = global_container_stack.getContainerIndex(variant_container)
variant_container = global_container_stack.extruders["0"].variant
if has_variants:
if "has_variants" in global_container_stack.getMetaData():
@ -52,7 +51,7 @@ class UM2UpgradeSelection(MachineAction):
search_criteria = { "type": "variant", "definition": "ultimaker2", "id": "*0.4*" }
containers = self._container_registry.findInstanceContainers(**search_criteria)
if containers:
global_container_stack.variant = containers[0]
global_container_stack.extruders["0"].variant = containers[0]
else:
# The metadata entry is stored in an ini, and ini files are parsed as strings only.
# Because any non-empty string evaluates to a boolean True, we have to remove the entry to make it False.
@ -60,6 +59,6 @@ class UM2UpgradeSelection(MachineAction):
global_container_stack.removeMetaDataEntry("has_variants")
# Set the variant container to an empty variant
global_container_stack.variant = ContainerRegistry.getInstance().getEmptyInstanceContainer()
global_container_stack.extruders["0"].variant = ContainerRegistry.getInstance().getEmptyInstanceContainer()
Application.getInstance().globalContainerStackChanged.emit()

View file

@ -9,13 +9,12 @@ from UM.Logger import Logger
from cura.CuraApplication import CuraApplication
from PyQt5.QtQml import QQmlComponent, QQmlContext
from PyQt5.QtCore import QUrl, QObject, pyqtSlot
from PyQt5.QtCore import QObject, pyqtSlot
import os.path
class UserAgreement(QObject, Extension):
def __init__(self, parent = None):
def __init__(self):
super(UserAgreement, self).__init__()
self._user_agreement_window = None
self._user_agreement_context = None
@ -33,8 +32,8 @@ class UserAgreement(QObject, Extension):
self._user_agreement_window.show()
@pyqtSlot(bool)
def didAgree(self, userChoice):
if userChoice:
def didAgree(self, user_choice):
if user_choice:
Logger.log("i", "User agreed to the user agreement")
Preferences.getInstance().setValue("general/accepted_user_agreement", True)
self._user_agreement_window.hide()
@ -45,9 +44,5 @@ class UserAgreement(QObject, Extension):
CuraApplication.getInstance().setNeedToShowUserAgreement(False)
def createUserAgreementWindow(self):
path = QUrl.fromLocalFile(os.path.join(PluginRegistry.getInstance().getPluginPath(self.getPluginId()), "UserAgreement.qml"))
component = QQmlComponent(Application.getInstance()._engine, path)
self._user_agreement_context = QQmlContext(Application.getInstance()._engine.rootContext())
self._user_agreement_context.setContextProperty("manager", self)
self._user_agreement_window = component.create(self._user_agreement_context)
path = os.path.join(PluginRegistry.getInstance().getPluginPath(self.getPluginId()), "UserAgreement.qml")
self._user_agreement_window = Application.getInstance().createQmlComponent(path, {"manager": self})

View file

@ -43,7 +43,7 @@ UM.Dialog
anchors.right: parent.right
text: catalog.i18nc("@action:button", "I understand and agree")
onClicked: {
manager.didAgree(true)
baseDialog.accepted()
}
}
@ -52,13 +52,12 @@ UM.Dialog
anchors.left: parent.left
text: catalog.i18nc("@action:button", "I don't agree")
onClicked: {
manager.didAgree(false)
baseDialog.rejected()
}
}
}
onClosing: {
manager.didAgree(false)
}
onAccepted: manager.didAgree(true)
onRejected: manager.didAgree(false)
onClosing: manager.didAgree(false)
}

View file

@ -98,7 +98,7 @@ class Profile:
config.add_section("metadata")
config.set("metadata", "quality_type", "normal") #This feature doesn't exist in 2.1 yet, so we don't know the actual quality type. For now, always base it on normal.
config.set("metadata", "type", "quality_changes")
config.set("metadata", "type", "quality")
if self._weight:
config.set("metadata", "weight", str(self._weight))
if self._machine_variant_name:

View file

@ -3,9 +3,14 @@
import configparser #To parse preference files.
import io #To serialise the preference files afterwards.
import os
from urllib.parse import quote_plus
from UM.Resources import Resources
from UM.VersionUpgrade import VersionUpgrade #We're inheriting from this.
from cura.CuraApplication import CuraApplication
# a list of all legacy "Not Supported" quality profiles
_OLD_NOT_SUPPORTED_PROFILES = [
@ -45,6 +50,15 @@ _OLD_NOT_SUPPORTED_PROFILES = [
]
# Some containers have their specific empty containers, those need to be set correctly.
_EMPTY_CONTAINER_DICT = {
"1": "empty_quality_changes",
"2": "empty_quality",
"3": "empty_material",
"4": "empty_variant",
}
class VersionUpgrade30to31(VersionUpgrade):
## Gets the version number from a CFG file in Uranium's 3.0 format.
#
@ -97,6 +111,32 @@ class VersionUpgrade30to31(VersionUpgrade):
if not parser.has_section(each_section):
parser.add_section(each_section)
# Copy global quality changes to extruder quality changes for single extrusion machines
if parser["metadata"]["type"] == "quality_changes":
all_quality_changes = self._getSingleExtrusionMachineQualityChanges(parser)
# Note that DO NOT!!! use the quality_changes returned from _getSingleExtrusionMachineQualityChanges().
# Those are loaded from the hard drive which are original files that haven't been upgraded yet.
# NOTE 2: The number can be 0 or 1 depends on whether you are loading it from the qualities folder or
# from a project file. When you load from a project file, the custom profile may not be in cura
# yet, so you will get 0.
if len(all_quality_changes) <= 1 and not parser.has_option("metadata", "extruder"):
self._createExtruderQualityChangesForSingleExtrusionMachine(filename, parser)
if parser["metadata"]["type"] == "definition_changes":
if parser["general"]["definition"] == "custom":
# We are only interested in machine_nozzle_size
if parser.has_option("values", "machine_nozzle_size"):
machine_nozzle_size = parser["values"]["machine_nozzle_size"]
definition_name = parser["general"]["name"]
machine_extruders = self._getSingleExtrusionMachineExtruders(definition_name)
#For single extuder machine we nee only first extruder
if len(machine_extruders) !=0:
if self._updateSingleExtuderDefinitionFile(machine_extruders, machine_nozzle_size):
parser.remove_option("values", "machine_nozzle_size")
# Update version numbers
parser["general"]["version"] = "2"
parser["metadata"]["setting_version"] = "4"
@ -126,6 +166,11 @@ class VersionUpgrade30to31(VersionUpgrade):
if quality_profile_id in _OLD_NOT_SUPPORTED_PROFILES:
parser["containers"]["2"] = "empty_quality"
# fix empty containers
for key, specific_empty_container in _EMPTY_CONTAINER_DICT.items():
if parser.has_option("containers", key) and parser["containers"][key] == "empty":
parser["containers"][key] = specific_empty_container
# Update version numbers
if "general" not in parser:
parser["general"] = {}
@ -139,3 +184,194 @@ class VersionUpgrade30to31(VersionUpgrade):
output = io.StringIO()
parser.write(output)
return [filename], [output.getvalue()]
def _getSingleExtrusionMachineQualityChanges(self, quality_changes_container):
quality_changes_dir = Resources.getPath(CuraApplication.ResourceTypes.QualityInstanceContainer)
quality_changes_containers = []
for item in os.listdir(quality_changes_dir):
file_path = os.path.join(quality_changes_dir, item)
if not os.path.isfile(file_path):
continue
parser = configparser.ConfigParser(interpolation = None)
try:
parser.read([file_path])
except:
# skip, it is not a valid stack file
continue
if not parser.has_option("metadata", "type"):
continue
if "quality_changes" != parser["metadata"]["type"]:
continue
if not parser.has_option("general", "name"):
continue
if quality_changes_container["general"]["name"] != parser["general"]["name"]:
continue
quality_changes_containers.append(parser)
return quality_changes_containers
def _getSingleExtrusionMachineExtruders(self, definition_name):
machine_instances_dir = Resources.getPath(CuraApplication.ResourceTypes.MachineStack)
machine_instances = []
#Find all machine instances
for item in os.listdir(machine_instances_dir):
file_path = os.path.join(machine_instances_dir, item)
if not os.path.isfile(file_path):
continue
parser = configparser.ConfigParser(interpolation=None)
try:
parser.read([file_path])
except:
# skip, it is not a valid stack file
continue
if not parser.has_option("metadata", "type"):
continue
if "machine" != parser["metadata"]["type"]:
continue
if not parser.has_option("general", "id"):
continue
machine_instances.append(parser)
#Find for extruders
extruders_instances_dir = Resources.getPath(CuraApplication.ResourceTypes.ExtruderStack)
#"machine",[extruders]
extruder_instances_per_machine = {}
#Find all custom extruders for founded machines
for item in os.listdir(extruders_instances_dir):
file_path = os.path.join(extruders_instances_dir, item)
if not os.path.isfile(file_path):
continue
parser = configparser.ConfigParser(interpolation=None)
try:
parser.read([file_path])
except:
# skip, it is not a valid stack file
continue
if not parser.has_option("metadata", "type"):
continue
if "extruder_train" != parser["metadata"]["type"]:
continue
if not parser.has_option("metadata", "machine"):
continue
if not parser.has_option("metadata", "position"):
continue
for machine_instace in machine_instances:
machine_id = machine_instace["general"]["id"]
if machine_id != parser["metadata"]["machine"]:
continue
if machine_id + "_settings" != definition_name:
continue
if extruder_instances_per_machine.get(machine_id) is None:
extruder_instances_per_machine.update({machine_id:[]})
extruder_instances_per_machine.get(machine_id).append(parser)
#the extruder can be related only to one machine
break
return extruder_instances_per_machine
#Find extruder defition at index 0 and update its values
def _updateSingleExtuderDefinitionFile(self, extruder_instances_per_machine, machine_nozzle_size):
defintion_instances_dir = Resources.getPath(CuraApplication.ResourceTypes.DefinitionChangesContainer)
for item in os.listdir(defintion_instances_dir):
file_path = os.path.join(defintion_instances_dir, item)
if not os.path.isfile(file_path):
continue
parser = configparser.ConfigParser(interpolation=None)
try:
parser.read([file_path])
except:
# skip, it is not a valid stack file
continue
if not parser.has_option("general", "name"):
continue
name = parser["general"]["name"]
custom_extruder_at_0_position = None
for machine_extruders in extruder_instances_per_machine:
for extruder_instance in extruder_instances_per_machine[machine_extruders]:
if extruder_instance["general"]["id"] + "_settings" == name:
defition_position = extruder_instance["metadata"]["position"]
if defition_position == "0":
custom_extruder_at_0_position = extruder_instance
break
if custom_extruder_at_0_position is not None:
break
#If not null, then parsed file is for first extuder and then can be updated. I need to update only
# first, because this update for single extuder machine
if custom_extruder_at_0_position is not None:
#Add new value
parser["values"]["machine_nozzle_size"] = machine_nozzle_size
definition_output = io.StringIO()
parser.write(definition_output)
with open(file_path, "w") as f:
f.write(definition_output.getvalue())
return True
return False
def _createExtruderQualityChangesForSingleExtrusionMachine(self, filename, global_quality_changes):
suffix = "_" + quote_plus(global_quality_changes["general"]["name"].lower())
machine_name = os.path.os.path.basename(filename).replace(".inst.cfg", "").replace(suffix, "")
# Why is this here?!
# When we load a .curaprofile file the deserialize will trigger a version upgrade, creating a dangling file.
# This file can be recognized by it's lack of a machine name in the target filename.
# So when we detect that situation here, we don't create the file and return.
if machine_name == "":
return
new_filename = machine_name + "_" + "fdmextruder" + suffix
extruder_quality_changes_parser = configparser.ConfigParser(interpolation = None)
extruder_quality_changes_parser.add_section("general")
extruder_quality_changes_parser["general"]["version"] = str(2)
extruder_quality_changes_parser["general"]["name"] = global_quality_changes["general"]["name"]
extruder_quality_changes_parser["general"]["definition"] = global_quality_changes["general"]["definition"]
extruder_quality_changes_parser.add_section("metadata")
extruder_quality_changes_parser["metadata"]["quality_type"] = global_quality_changes["metadata"]["quality_type"]
extruder_quality_changes_parser["metadata"]["type"] = global_quality_changes["metadata"]["type"]
extruder_quality_changes_parser["metadata"]["setting_version"] = str(4)
extruder_quality_changes_parser["metadata"]["extruder"] = "fdmextruder"
extruder_quality_changes_output = io.StringIO()
extruder_quality_changes_parser.write(extruder_quality_changes_output)
extruder_quality_changes_filename = quote_plus(new_filename) + ".inst.cfg"
quality_changes_dir = Resources.getPath(CuraApplication.ResourceTypes.QualityInstanceContainer)
with open(os.path.join(quality_changes_dir, extruder_quality_changes_filename), "w") as f:
f.write(extruder_quality_changes_output.getvalue())

View file

@ -3,7 +3,10 @@
import copy
import io
from typing import List, Optional
import json #To parse the product-to-id mapping file.
import os.path #To find the product-to-id mapping.
import sys
from typing import Any, Dict, List, Optional
import xml.etree.ElementTree as ET
from UM.Resources import Resources
@ -14,7 +17,6 @@ import UM.Dictionary
from UM.Settings.InstanceContainer import InstanceContainer
from UM.Settings.ContainerRegistry import ContainerRegistry
## Handles serializing and deserializing material containers from an XML file
class XmlMaterialProfile(InstanceContainer):
CurrentFdmMaterialVersion = "1.3"
@ -42,25 +44,18 @@ class XmlMaterialProfile(InstanceContainer):
def getInheritedFiles(self):
return self._inherited_files
## Overridden from InstanceContainer
def setReadOnly(self, read_only):
super().setReadOnly(read_only)
basefile = self.getMetaDataEntry("base_file", self._id) # if basefile is self.id, this is a basefile.
for container in ContainerRegistry.getInstance().findInstanceContainers(base_file = basefile):
container._read_only = read_only # prevent loop instead of calling setReadOnly
## Overridden from InstanceContainer
# set the meta data for all machine / variant combinations
def setMetaDataEntry(self, key, value):
if self.isReadOnly():
registry = ContainerRegistry.getInstance()
if registry.isReadOnly(self.getId()):
return
super().setMetaDataEntry(key, value)
basefile = self.getMetaDataEntry("base_file", self._id) #if basefile is self.id, this is a basefile.
basefile = self.getMetaDataEntry("base_file", self.getId()) #if basefile is self.getId, this is a basefile.
# Update all containers that share basefile
for container in ContainerRegistry.getInstance().findInstanceContainers(base_file = basefile):
for container in registry.findInstanceContainers(base_file = basefile):
if container.getMetaDataEntry(key, None) != value: # Prevent recursion
container.setMetaDataEntry(key, value)
@ -68,7 +63,8 @@ class XmlMaterialProfile(InstanceContainer):
# without this function the setName would only set the name of the specific nozzle / material / machine combination container
# The function is a bit tricky. It will not set the name of all containers if it has the correct name itself.
def setName(self, new_name):
if self.isReadOnly():
registry = ContainerRegistry.getInstance()
if registry.isReadOnly(self.getId()):
return
# Not only is this faster, it also prevents a major loop that causes a stack overflow.
@ -77,10 +73,10 @@ class XmlMaterialProfile(InstanceContainer):
super().setName(new_name)
basefile = self.getMetaDataEntry("base_file", self._id) # if basefile is self.id, this is a basefile.
basefile = self.getMetaDataEntry("base_file", self.getId()) # if basefile is self.getId, this is a basefile.
# Update the basefile as well, this is actually what we're trying to do
# Update all containers that share GUID and basefile
containers = ContainerRegistry.getInstance().findInstanceContainers(base_file = basefile)
containers = registry.findInstanceContainers(base_file = basefile)
for container in containers:
container.setName(new_name)
@ -88,33 +84,20 @@ class XmlMaterialProfile(InstanceContainer):
def setDirty(self, dirty):
super().setDirty(dirty)
base_file = self.getMetaDataEntry("base_file", None)
if base_file is not None and base_file != self._id:
containers = ContainerRegistry.getInstance().findContainers(id=base_file)
registry = ContainerRegistry.getInstance()
if base_file is not None and base_file != self.getId() and not registry.isReadOnly(base_file):
containers = registry.findContainers(id = base_file)
if containers:
base_container = containers[0]
if not base_container.isReadOnly():
base_container.setDirty(dirty)
## Overridden from InstanceContainer
# def setProperty(self, key, property_name, property_value, container = None):
# if self.isReadOnly():
# return
#
# super().setProperty(key, property_name, property_value)
#
# basefile = self.getMetaDataEntry("base_file", self._id) #if basefile is self.id, this is a basefile.
# for container in UM.Settings.ContainerRegistry.ContainerRegistry.getInstance().findInstanceContainers(base_file = basefile):
# if not container.isReadOnly():
# container.setDirty(True)
containers[0].setDirty(dirty)
## Overridden from InstanceContainer
# base file: common settings + supported machines
# machine / variant combination: only changes for itself.
def serialize(self, ignored_metadata_keys: Optional[List] = None):
def serialize(self, ignored_metadata_keys: Optional[set] = None):
registry = ContainerRegistry.getInstance()
base_file = self.getMetaDataEntry("base_file", "")
if base_file and self.id != base_file:
if base_file and self.getId() != base_file:
# Since we create an instance of XmlMaterialProfile for each machine and nozzle in the profile,
# we should only serialize the "base" material definition, since that can then take care of
# serializing the machine/nozzle specific profiles.
@ -133,8 +116,8 @@ class XmlMaterialProfile(InstanceContainer):
metadata = copy.deepcopy(self.getMetaData())
# setting_version is derived from the "version" tag in the schema, so don't serialize it into a file
if ignored_metadata_keys is None:
ignored_metadata_keys = []
ignored_metadata_keys = ignored_metadata_keys + ["setting_version"]
ignored_metadata_keys = set()
ignored_metadata_keys |= {"setting_version"}
# remove the keys that we want to ignore in the metadata
for key in ignored_metadata_keys:
if key in metadata:
@ -147,6 +130,9 @@ class XmlMaterialProfile(InstanceContainer):
metadata.pop("type", "")
metadata.pop("base_file", "")
metadata.pop("approximate_diameter", "")
metadata.pop("id", "")
metadata.pop("container_type", "")
metadata.pop("name", "")
## Begin Name Block
builder.start("name")
@ -164,7 +150,7 @@ class XmlMaterialProfile(InstanceContainer):
builder.end("color")
builder.start("label")
builder.data(self._name)
builder.data(self.getName())
builder.end("label")
builder.end("name")
@ -196,16 +182,16 @@ class XmlMaterialProfile(InstanceContainer):
## Begin Settings Block
builder.start("settings")
if self.getDefinition().id == "fdmprinter":
if self.getDefinition().getId() == "fdmprinter":
for instance in self.findInstances():
self._addSettingElement(builder, instance)
machine_container_map = {}
machine_nozzle_map = {}
all_containers = registry.findInstanceContainers(GUID = self.getMetaDataEntry("GUID"), base_file = self._id)
all_containers = registry.findInstanceContainers(GUID = self.getMetaDataEntry("GUID"), base_file = self.getId())
for container in all_containers:
definition_id = container.getDefinition().id
definition_id = container.getDefinition().getId()
if definition_id == "fdmprinter":
continue
@ -223,17 +209,16 @@ class XmlMaterialProfile(InstanceContainer):
machine_container_map[definition_id] = container
# Map machine human-readable names to IDs
product_id_map = {}
for container in registry.findDefinitionContainers(type = "machine"):
product_id_map[container.getName()] = container.getId()
product_id_map = self.getProductIdMap()
for definition_id, container in machine_container_map.items():
definition = container.getDefinition()
try:
product = UM.Dictionary.findKey(product_id_map, definition_id)
except ValueError:
# An unknown product id; export it anyway
product = definition_id
product = definition_id
for product_name, product_id_list in product_id_map.items():
if definition_id in product_id_list:
product = product_name
break
builder.start("machine")
builder.start("machine_identifier", {
@ -243,7 +228,7 @@ class XmlMaterialProfile(InstanceContainer):
builder.end("machine_identifier")
for instance in container.findInstances():
if self.getDefinition().id == "fdmprinter" and self.getInstance(instance.definition.key) and self.getProperty(instance.definition.key, "value") == instance.value:
if self.getDefinition().getId() == "fdmprinter" and self.getInstance(instance.definition.key) and self.getProperty(instance.definition.key, "value") == instance.value:
# If the settings match that of the base profile, just skip since we inherit the base profile.
continue
@ -251,11 +236,12 @@ class XmlMaterialProfile(InstanceContainer):
# Find all hotend sub-profiles corresponding to this material and machine and add them to this profile.
for hotend_id, hotend in machine_nozzle_map[definition_id].items():
variant_containers = registry.findInstanceContainers(id = hotend.getMetaDataEntry("variant"))
variant_containers = registry.findInstanceContainersMetadata(id = hotend.getMetaDataEntry("variant"))
if not variant_containers:
continue
builder.start("hotend", {"id": variant_containers[0].getName()})
# The hotend identifier is not the containers name, but its "name".
builder.start("hotend", {"id": variant_containers[0]["name"]})
# Compatible is a special case, as it's added as a meta data entry (instead of an instance).
compatible = hotend.getMetaDataEntry("compatible")
@ -398,15 +384,18 @@ class XmlMaterialProfile(InstanceContainer):
first.append(element)
def clearData(self):
self._metadata = {}
self._name = ""
self._metadata = {
"id": self.getId(),
"name": ""
}
self._definition = None
self._instances = {}
self._read_only = False
self._dirty = False
self._path = ""
def getConfigurationTypeFromSerialized(self, serialized: str) -> Optional[str]:
@classmethod
def getConfigurationTypeFromSerialized(cls, serialized: str) -> Optional[str]:
return "materials"
@classmethod
@ -416,31 +405,34 @@ class XmlMaterialProfile(InstanceContainer):
version = XmlMaterialProfile.Version
# get setting version
if "version" in data.attrib:
setting_version = XmlMaterialProfile.xmlVersionToSettingVersion(data.attrib["version"])
setting_version = cls.xmlVersionToSettingVersion(data.attrib["version"])
else:
setting_version = XmlMaterialProfile.xmlVersionToSettingVersion("1.2")
setting_version = cls.xmlVersionToSettingVersion("1.2")
return version * 1000000 + setting_version
## Overridden from InstanceContainer
def deserialize(self, serialized):
def deserialize(self, serialized, file_name = None):
containers_to_add = []
# update the serialized data first
from UM.Settings.Interfaces import ContainerInterface
serialized = ContainerInterface.deserialize(self, serialized)
serialized = ContainerInterface.deserialize(self, serialized, file_name)
try:
data = ET.fromstring(serialized)
except:
Logger.logException("e", "An exception occured while parsing the material profile")
Logger.logException("e", "An exception occurred while parsing the material profile")
return
# Reset previous metadata
old_id = self.getId()
self.clearData() # Ensure any previous data is gone.
meta_data = {}
meta_data["type"] = "material"
meta_data["base_file"] = self.id
meta_data["base_file"] = self.getId()
meta_data["status"] = "unknown" # TODO: Add material verification
meta_data["id"] = old_id
meta_data["container_type"] = XmlMaterialProfile
common_setting_values = {}
@ -455,8 +447,8 @@ class XmlMaterialProfile(InstanceContainer):
else:
meta_data["setting_version"] = self.xmlVersionToSettingVersion("1.2") #1.2 and lower didn't have that version number there yet.
metadata = data.iterfind("./um:metadata/*", self.__namespaces)
for entry in metadata:
meta_data["name"] = "Unknown Material" #In case the name tag is missing.
for entry in data.iterfind("./um:metadata/*", self.__namespaces):
tag_name = _tag_without_namespace(entry)
if tag_name == "name":
@ -466,9 +458,9 @@ class XmlMaterialProfile(InstanceContainer):
label = entry.find("./um:label", self.__namespaces)
if label is not None:
self._name = label.text
meta_data["name"] = label.text
else:
self._name = self._profile_name(material.text, color.text)
meta_data["name"] = self._profile_name(material.text, color.text)
meta_data["brand"] = brand.text
meta_data["material"] = material.text
meta_data["color_name"] = color.text
@ -500,8 +492,7 @@ class XmlMaterialProfile(InstanceContainer):
meta_data["approximate_diameter"] = str(round(float(property_values.get("diameter", 2.85)))) # In mm
meta_data["properties"] = property_values
self.setDefinition(ContainerRegistry.getInstance().findDefinitionContainers(id = "fdmprinter")[0])
meta_data["definition"] = "fdmprinter"
common_compatibility = True
settings = data.iterfind("./um:settings/um:setting", self.__namespaces)
@ -526,9 +517,7 @@ class XmlMaterialProfile(InstanceContainer):
self._dirty = False
# Map machine human-readable names to IDs
product_id_map = {}
for container in ContainerRegistry.getInstance().findDefinitionContainers(type = "machine"):
product_id_map[container.getName()] = container.getId()
product_id_map = self.getProductIdMap()
machines = data.iterfind("./um:settings/um:machine", self.__namespaces)
for machine in machines:
@ -556,77 +545,98 @@ class XmlMaterialProfile(InstanceContainer):
identifiers = machine.iterfind("./um:machine_identifier", self.__namespaces)
for identifier in identifiers:
machine_id = product_id_map.get(identifier.get("product"), None)
if machine_id is None:
# Lets try again with some naive heuristics.
machine_id = identifier.get("product").replace(" ", "").lower()
machine_id_list = product_id_map.get(identifier.get("product"), [])
if not machine_id_list:
machine_id_list = self.getPossibleDefinitionIDsFromName(identifier.get("product"))
definitions = ContainerRegistry.getInstance().findDefinitionContainers(id = machine_id)
if not definitions:
Logger.log("w", "No definition found for machine ID %s", machine_id)
continue
definition = definitions[0]
machine_manufacturer = identifier.get("manufacturer", definition.getMetaDataEntry("manufacturer", "Unknown")) #If the XML material doesn't specify a manufacturer, use the one in the actual printer definition.
if machine_compatibility:
new_material_id = self.id + "_" + machine_id
# The child or derived material container may already exist. This can happen when a material in a
# project file and the a material in Cura have the same ID.
# In the case if a derived material already exists, override that material container because if
# the data in the parent material has been changed, the derived ones should be updated too.
found_materials = ContainerRegistry.getInstance().findInstanceContainers(id = new_material_id)
is_new_material = False
if found_materials:
new_material = found_materials[0]
else:
new_material = XmlMaterialProfile(new_material_id)
is_new_material = True
# Update the private directly, as we want to prevent the lookup that is done when using setName
new_material._name = self.getName()
new_material.setMetaData(copy.deepcopy(self.getMetaData()))
new_material.setDefinition(definition)
# Don't use setMetadata, as that overrides it for all materials with same base file
new_material.getMetaData()["compatible"] = machine_compatibility
new_material.getMetaData()["machine_manufacturer"] = machine_manufacturer
new_material.setCachedValues(cached_machine_setting_properties)
new_material._dirty = False
if is_new_material:
containers_to_add.append(new_material)
hotends = machine.iterfind("./um:hotend", self.__namespaces)
for hotend in hotends:
hotend_id = hotend.get("id")
if hotend_id is None:
for machine_id in machine_id_list:
definitions = ContainerRegistry.getInstance().findDefinitionContainersMetadata(id = machine_id)
if not definitions:
Logger.log("w", "No definition found for machine ID %s", machine_id)
continue
variant_containers = ContainerRegistry.getInstance().findInstanceContainers(id = hotend_id)
if not variant_containers:
# It is not really properly defined what "ID" is so also search for variants by name.
variant_containers = ContainerRegistry.getInstance().findInstanceContainers(definition = definition.id, name = hotend_id)
Logger.log("d", "Found definition for machine ID %s", machine_id)
definition = definitions[0]
if not variant_containers:
#Logger.log("d", "No variants found with ID or name %s for machine %s", hotend_id, definition.id)
continue
machine_manufacturer = identifier.get("manufacturer", definition.get("manufacturer", "Unknown")) #If the XML material doesn't specify a manufacturer, use the one in the actual printer definition.
hotend_compatibility = machine_compatibility
hotend_setting_values = {}
settings = hotend.iterfind("./um:setting", self.__namespaces)
for entry in settings:
key = entry.get("key")
if key in self.__material_settings_setting_map:
hotend_setting_values[self.__material_settings_setting_map[key]] = entry.text
elif key in self.__unmapped_settings:
if key == "hardware compatible":
hotend_compatibility = self._parseCompatibleValue(entry.text)
if machine_compatibility:
new_material_id = self.getId() + "_" + machine_id
# The child or derived material container may already exist. This can happen when a material in a
# project file and the a material in Cura have the same ID.
# In the case if a derived material already exists, override that material container because if
# the data in the parent material has been changed, the derived ones should be updated too.
if ContainerRegistry.getInstance().isLoaded(new_material_id):
new_material = ContainerRegistry.getInstance().findContainers(id = new_material_id)[0]
is_new_material = False
else:
Logger.log("d", "Unsupported material setting %s", key)
new_material = XmlMaterialProfile(new_material_id)
is_new_material = True
new_material.setMetaData(copy.deepcopy(self.getMetaData()))
new_material.getMetaData()["id"] = new_material_id
new_material.getMetaData()["name"] = self.getName()
new_material.setDefinition(machine_id)
# Don't use setMetadata, as that overrides it for all materials with same base file
new_material.getMetaData()["compatible"] = machine_compatibility
new_material.getMetaData()["machine_manufacturer"] = machine_manufacturer
new_material.getMetaData()["definition"] = machine_id
new_material.setCachedValues(cached_machine_setting_properties)
new_material._dirty = False
if is_new_material:
containers_to_add.append(new_material)
hotends = machine.iterfind("./um:hotend", self.__namespaces)
for hotend in hotends:
hotend_id = hotend.get("id")
if hotend_id is None:
continue
variant_containers = ContainerRegistry.getInstance().findInstanceContainersMetadata(id = hotend_id)
if not variant_containers:
# It is not really properly defined what "ID" is so also search for variants by name.
variant_containers = ContainerRegistry.getInstance().findInstanceContainersMetadata(definition = machine_id, name = hotend_id)
if not variant_containers:
continue
hotend_compatibility = machine_compatibility
hotend_setting_values = {}
settings = hotend.iterfind("./um:setting", self.__namespaces)
for entry in settings:
key = entry.get("key")
if key in self.__material_settings_setting_map:
hotend_setting_values[self.__material_settings_setting_map[key]] = entry.text
elif key in self.__unmapped_settings:
if key == "hardware compatible":
hotend_compatibility = self._parseCompatibleValue(entry.text)
else:
Logger.log("d", "Unsupported material setting %s", key)
new_hotend_id = self.getId() + "_" + machine_id + "_" + hotend_id.replace(" ", "_")
# Same as machine compatibility, keep the derived material containers consistent with the parent
# material
if ContainerRegistry.getInstance().isLoaded(new_hotend_id):
new_hotend_material = ContainerRegistry.getInstance().findContainers(id = new_hotend_id)[0]
is_new_material = False
else:
new_hotend_material = XmlMaterialProfile(new_hotend_id)
is_new_material = True
new_hotend_material.setMetaData(copy.deepcopy(self.getMetaData()))
new_hotend_material.getMetaData()["id"] = new_hotend_id
new_hotend_material.getMetaData()["name"] = self.getName()
new_hotend_material.getMetaData()["variant"] = variant_containers[0]["id"]
new_hotend_material.setDefinition(machine_id)
# Don't use setMetadata, as that overrides it for all materials with same base file
new_hotend_material.getMetaData()["compatible"] = hotend_compatibility
new_hotend_material.getMetaData()["machine_manufacturer"] = machine_manufacturer
new_hotend_material.getMetaData()["definition"] = machine_id
# Add namespaced Cura-specific settings
settings = hotend.iterfind("./cura:setting", self.__namespaces)
@ -636,38 +646,192 @@ class XmlMaterialProfile(InstanceContainer):
new_hotend_id = self.id + "_" + machine_id + "_" + hotend_id.replace(" ", "_")
# Same as machine compatibility, keep the derived material containers consistent with the parent
# material
found_materials = ContainerRegistry.getInstance().findInstanceContainers(id = new_hotend_id)
is_new_material = False
if found_materials:
new_hotend_material = found_materials[0]
else:
new_hotend_material = XmlMaterialProfile(new_hotend_id)
is_new_material = True
cached_hotend_setting_properties = cached_machine_setting_properties.copy()
cached_hotend_setting_properties.update(hotend_setting_values)
# Update the private directly, as we want to prevent the lookup that is done when using setName
new_hotend_material._name = self.getName()
new_hotend_material.setMetaData(copy.deepcopy(self.getMetaData()))
new_hotend_material.setDefinition(definition)
new_hotend_material.addMetaDataEntry("variant", variant_containers[0].id)
# Don't use setMetadata, as that overrides it for all materials with same base file
new_hotend_material.getMetaData()["compatible"] = hotend_compatibility
new_hotend_material.getMetaData()["machine_manufacturer"] = machine_manufacturer
new_hotend_material.setCachedValues(cached_hotend_setting_properties)
cached_hotend_setting_properties = cached_machine_setting_properties.copy()
cached_hotend_setting_properties.update(hotend_setting_values)
new_hotend_material._dirty = False
new_hotend_material.setCachedValues(cached_hotend_setting_properties)
if is_new_material:
containers_to_add.append(new_hotend_material)
new_hotend_material._dirty = False
if is_new_material:
containers_to_add.append(new_hotend_material)
# there is only one ID for a machine. Once we have reached here, it means we have already found
# a workable ID for that machine, so there is no need to continue
break
for container_to_add in containers_to_add:
ContainerRegistry.getInstance().addContainer(container_to_add)
@classmethod
def deserializeMetadata(cls, serialized: str, container_id: str) -> List[Dict[str, Any]]:
result_metadata = [] #All the metadata that we found except the base (because the base is returned).
#Update the serialized data to the latest version.
serialized = cls._updateSerialized(serialized)
base_metadata = {
"type": "material",
"status": "unknown", #TODO: Add material verification.
"container_type": XmlMaterialProfile,
"id": container_id,
"base_file": container_id
}
try:
data = ET.fromstring(serialized)
except:
Logger.logException("e", "An exception occurred while parsing the material profile")
return []
#TODO: Implement the <inherits> tag. It's unused at the moment though.
if "version" in data.attrib:
base_metadata["setting_version"] = cls.xmlVersionToSettingVersion(data.attrib["version"])
else:
base_metadata["setting_version"] = cls.xmlVersionToSettingVersion("1.2") #1.2 and lower didn't have that version number there yet.
for entry in data.iterfind("./um:metadata/*", cls.__namespaces):
tag_name = _tag_without_namespace(entry)
if tag_name == "name":
brand = entry.find("./um:brand", cls.__namespaces)
material = entry.find("./um:material", cls.__namespaces)
color = entry.find("./um:color", cls.__namespaces)
label = entry.find("./um:label", cls.__namespaces)
if label is not None:
base_metadata["name"] = label.text
else:
base_metadata["name"] = cls._profile_name(material.text, color.text)
base_metadata["brand"] = brand.text
base_metadata["material"] = material.text
base_metadata["color_name"] = color.text
continue
#Setting_version is derived from the "version" tag in the schema earlier, so don't set it here.
if tag_name == "setting_version":
continue
base_metadata[tag_name] = entry.text
if "description" not in base_metadata:
base_metadata["description"] = ""
if "adhesion_info" not in base_metadata:
base_metadata["adhesion_info"] = ""
property_values = {}
properties = data.iterfind("./um:properties/*", cls.__namespaces)
for entry in properties:
tag_name = _tag_without_namespace(entry)
property_values[tag_name] = entry.text
base_metadata["approximate_diameter"] = str(round(float(property_values.get("diameter", 2.85)))) # In mm
base_metadata["properties"] = property_values
base_metadata["definition"] = "fdmprinter"
compatible_entries = data.iterfind("./um:settings/um:setting[@key='hardware compatible']", cls.__namespaces)
try:
common_compatibility = cls._parseCompatibleValue(next(compatible_entries).text)
except StopIteration: #No 'hardware compatible' setting.
common_compatibility = True
base_metadata["compatible"] = common_compatibility
result_metadata.append(base_metadata)
# Map machine human-readable names to IDs
product_id_map = cls.getProductIdMap()
for machine in data.iterfind("./um:settings/um:machine", cls.__namespaces):
machine_compatibility = common_compatibility
for entry in machine.iterfind("./um:setting", cls.__namespaces):
key = entry.get("key")
if key == "hardware compatible":
machine_compatibility = cls._parseCompatibleValue(entry.text)
for identifier in machine.iterfind("./um:machine_identifier", cls.__namespaces):
machine_id_list = product_id_map.get(identifier.get("product"), [])
if not machine_id_list:
machine_id_list = cls.getPossibleDefinitionIDsFromName(identifier.get("product"))
for machine_id in machine_id_list:
definition_metadata = ContainerRegistry.getInstance().findDefinitionContainersMetadata(id = machine_id)
if not definition_metadata:
Logger.log("w", "No definition found for machine ID %s", machine_id)
continue
Logger.log("d", "Found def for machine [%s]", machine_id)
definition_metadata = definition_metadata[0]
machine_manufacturer = identifier.get("manufacturer", definition_metadata.get("manufacturer", "Unknown")) #If the XML material doesn't specify a manufacturer, use the one in the actual printer definition.
if machine_compatibility:
new_material_id = container_id + "_" + machine_id
# The child or derived material container may already exist. This can happen when a material in a
# project file and the a material in Cura have the same ID.
# In the case if a derived material already exists, override that material container because if
# the data in the parent material has been changed, the derived ones should be updated too.
found_materials = ContainerRegistry.getInstance().findInstanceContainersMetadata(id = new_material_id)
if found_materials:
new_material_metadata = found_materials[0]
else:
new_material_metadata = {}
new_material_metadata.update(base_metadata)
new_material_metadata["id"] = new_material_id
new_material_metadata["compatible"] = machine_compatibility
new_material_metadata["machine_manufacturer"] = machine_manufacturer
new_material_metadata["definition"] = machine_id
if len(found_materials) == 0: #This is a new material.
result_metadata.append(new_material_metadata)
for hotend in machine.iterfind("./um:hotend", cls.__namespaces):
hotend_id = hotend.get("id")
if hotend_id is None:
continue
variant_containers = ContainerRegistry.getInstance().findInstanceContainersMetadata(id = hotend_id)
if not variant_containers:
# It is not really properly defined what "ID" is so also search for variants by name.
variant_containers = ContainerRegistry.getInstance().findInstanceContainersMetadata(definition = machine_id, name = hotend_id)
hotend_compatibility = machine_compatibility
for entry in hotend.iterfind("./um:setting", cls.__namespaces):
key = entry.get("key")
if key == "hardware compatible":
hotend_compatibility = cls._parseCompatibleValue(entry.text)
new_hotend_id = container_id + "_" + machine_id + "_" + hotend_id.replace(" ", "_")
# Same as machine compatibility, keep the derived material containers consistent with the parent
# material
found_materials = ContainerRegistry.getInstance().findInstanceContainersMetadata(id = new_hotend_id)
if found_materials:
new_hotend_material_metadata = found_materials[0]
else:
new_hotend_material_metadata = {}
new_hotend_material_metadata.update(base_metadata)
if variant_containers:
new_hotend_material_metadata["variant"] = variant_containers[0]["id"]
else:
new_hotend_material_metadata["variant"] = hotend_id
_with_missing_variants.append(new_hotend_material_metadata)
new_hotend_material_metadata["compatible"] = hotend_compatibility
new_hotend_material_metadata["machine_manufacturer"] = machine_manufacturer
new_hotend_material_metadata["id"] = new_hotend_id
new_hotend_material_metadata["definition"] = machine_id
if len(found_materials) == 0:
result_metadata.append(new_hotend_material_metadata)
# there is only one ID for a machine. Once we have reached here, it means we have already found
# a workable ID for that machine, so there is no need to continue
break
return result_metadata
def _addSettingElement(self, builder, instance):
key = instance.definition.key
if key in self.__material_settings_setting_map.values():
@ -685,16 +849,58 @@ class XmlMaterialProfile(InstanceContainer):
builder.data(str(instance.value))
builder.end(tag_name)
def _profile_name(self, material_name, color_name):
@classmethod
def _profile_name(cls, material_name, color_name):
if color_name != "Generic":
return "%s %s" % (color_name, material_name)
else:
return material_name
@classmethod
def getPossibleDefinitionIDsFromName(cls, name):
name_parts = name.lower().split(" ")
merged_name_parts = []
for part in name_parts:
if len(part) == 0:
continue
if len(merged_name_parts) == 0:
merged_name_parts.append(part)
continue
if part.isdigit():
# for names with digit(s) such as Ultimaker 3 Extended, we generate an ID like
# "ultimaker3_extended", ignoring the space between "Ultimaker" and "3".
merged_name_parts[-1] = merged_name_parts[-1] + part
else:
merged_name_parts.append(part)
id_list = [name.lower().replace(" ", ""), # simply removing all spaces
name.lower().replace(" ", "_"), # simply replacing all spaces with underscores
"_".join(merged_name_parts),
]
return id_list
## Gets a mapping from product names in the XML files to their definition
# IDs.
#
# This loads the mapping from a file.
@classmethod
def getProductIdMap(cls) -> Dict[str, List[str]]:
product_to_id_file = os.path.join(os.path.dirname(sys.modules[cls.__module__].__file__), "product_to_id.json")
with open(product_to_id_file) as f:
product_to_id_map = json.load(f)
product_to_id_map = {key: [value] for key, value in product_to_id_map.items()}
return product_to_id_map
## Parse the value of the "material compatible" property.
def _parseCompatibleValue(self, value: str):
@classmethod
def _parseCompatibleValue(cls, value: str):
return value in {"yes", "unknown"}
## Small string representation for debugging.
def __str__(self):
return "<XmlMaterialProfile '{my_id}' ('{name}') from base file '{base_file}'>".format(my_id = self.getId(), name = self.getName(), base_file = self.getMetaDataEntry("base_file"))
# Map XML file setting names to internal names
__material_settings_setting_map = {
"print temperature": "default_material_print_temperature",
@ -744,4 +950,22 @@ def _indent(elem, level = 0):
# We are only interested in the actual tag name, so discard everything
# before the last }
def _tag_without_namespace(element):
return element.tag[element.tag.rfind("}") + 1:]
return element.tag[element.tag.rfind("}") + 1:]
#While loading XML profiles, some of these profiles don't know what variant
#they belong to. We'd like to search by the machine ID and the variant's
#name, but we don't know the variant's ID. Not all variants have been loaded
#yet so we can't run a filter on the name and machine. The ID is unknown
#so we can't lazily load the variant either. So we have to wait until all
#the rest is loaded properly and then assign the correct variant to the
#material files that were missing it.
_with_missing_variants = []
def _fillMissingVariants():
registry = ContainerRegistry.getInstance()
for variant_metadata in _with_missing_variants:
variants = registry.findContainersMetadata(definition = variant_metadata["definition"], name = variant_metadata["variant"])
if not variants:
Logger.log("w", "Could not find variant for variant-specific material {material_id}.".format(material_id = variant_metadata["id"]))
continue
variant_metadata["variant"] = variants[0]["id"]
ContainerRegistry.allMetadataLoaded.connect(_fillMissingVariants)

View file

@ -0,0 +1,12 @@
{
"Ultimaker 2": "ultimaker2",
"Ultimaker 2 Extended": "ultimaker2_extended",
"Ultimaker 2 Extended+": "ultimaker2_extended_plus",
"Ultimaker 2 Go": "ultimaker2_go",
"Ultimaker 2+": "ultimaker2_plus",
"Ultimaker 3": "ultimaker3",
"Ultimaker 3 Extended": "ultimaker3_extended",
"Ultimaker Original": "ultimaker_original",
"Ultimaker Original+": "ultimaker_original_plus",
"IMADE3D JellyBOX": "imade3d_jellybox"
}