mirror of
https://github.com/Ultimaker/Cura.git
synced 2025-12-11 16:00:47 -07:00
Merge branch 'master' into feature_xmlmaterials_cura_settings
# Conflicts: # plugins/XmlMaterialProfile/XmlMaterialProfile.py
This commit is contained in:
commit
266be74569
312 changed files with 46856 additions and 17766 deletions
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
||||
|
|
|
|||
|
|
@ -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})
|
||||
|
|
|
|||
|
|
@ -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
|
||||
We’re 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
|
||||
We’ve 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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
462
plugins/GCodeReader/FlavorParser.py
Normal file
462
plugins/GCodeReader/FlavorParser.py
Normal 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
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
10
plugins/GCodeReader/MarlinFlavorParser.py
Normal file
10
plugins/GCodeReader/MarlinFlavorParser.py
Normal 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__()
|
||||
32
plugins/GCodeReader/RepRapFlavorParser.py
Normal file
32
plugins/GCodeReader/RepRapFlavorParser.py
Normal 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
|
||||
|
|
@ -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"))
|
||||
|
||||
|
|
|
|||
|
|
@ -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) }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
@ -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() }
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
44
plugins/MonitorStage/MonitorMainView.qml
Normal file
44
plugins/MonitorStage/MonitorMainView.qml
Normal 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
|
||||
}
|
||||
}
|
||||
73
plugins/MonitorStage/MonitorStage.py
Normal file
73
plugins/MonitorStage/MonitorStage.py
Normal 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"
|
||||
20
plugins/MonitorStage/__init__.py
Normal file
20
plugins/MonitorStage/__init__.py
Normal 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()
|
||||
}
|
||||
8
plugins/MonitorStage/plugin.json
Normal file
8
plugins/MonitorStage/plugin.json
Normal 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"
|
||||
}
|
||||
|
|
@ -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
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
18
plugins/PrepareStage/PrepareStage.py
Normal file
18
plugins/PrepareStage/PrepareStage.py
Normal 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)
|
||||
20
plugins/PrepareStage/__init__.py
Normal file
20
plugins/PrepareStage/__init__.py
Normal 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()
|
||||
}
|
||||
8
plugins/PrepareStage/plugin.json
Normal file
8
plugins/PrepareStage/plugin.json
Normal 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"
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
49
plugins/SimulationView/NozzleNode.py
Normal file
49
plugins/SimulationView/NozzleNode.py
Normal 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
|
||||
161
plugins/SimulationView/PathSlider.qml
Normal file
161
plugins/SimulationView/PathSlider.qml
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
190
plugins/SimulationView/SimulationPass.py
Normal file
190
plugins/SimulationView/SimulationPass.py
Normal 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()
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
217
plugins/LayerView/LayerView.py → plugins/SimulationView/SimulationView.py
Executable file → Normal file
217
plugins/LayerView/LayerView.py → plugins/SimulationView/SimulationView.py
Executable file → Normal 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:
|
||||
707
plugins/SimulationView/SimulationView.qml
Normal file
707
plugins/SimulationView/SimulationView.qml
Normal 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")
|
||||
}
|
||||
}
|
||||
259
plugins/SimulationView/SimulationViewProxy.py
Normal file
259
plugins/SimulationView/SimulationViewProxy.py
Normal 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)
|
||||
26
plugins/SimulationView/__init__.py
Normal file
26
plugins/SimulationView/__init__.py
Normal 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()}
|
||||
0
plugins/LayerView/layers.shader → plugins/SimulationView/layers.shader
Executable file → Normal file
0
plugins/LayerView/layers.shader → plugins/SimulationView/layers.shader
Executable file → Normal file
61
plugins/LayerView/layers3d.shader → plugins/SimulationView/layers3d.shader
Executable file → Normal file
61
plugins/LayerView/layers3d.shader → plugins/SimulationView/layers3d.shader
Executable file → Normal 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
|
||||
256
plugins/SimulationView/layers3d_shadow.shader
Normal file
256
plugins/SimulationView/layers3d_shadow.shader
Normal 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
|
||||
156
plugins/SimulationView/layers_shadow.shader
Normal file
156
plugins/SimulationView/layers_shadow.shader
Normal 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
|
||||
|
|
@ -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"
|
||||
}
|
||||
BIN
plugins/SimulationView/resources/nozzle.stl
Normal file
BIN
plugins/SimulationView/resources/nozzle.stl
Normal file
Binary file not shown.
79
plugins/SimulationView/resources/simulation_pause.svg
Normal file
79
plugins/SimulationView/resources/simulation_pause.svg
Normal 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 |
78
plugins/SimulationView/resources/simulation_resume.svg
Normal file
78
plugins/SimulationView/resources/simulation_resume.svg
Normal 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 |
|
|
@ -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")
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"))
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 )
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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})
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
12
plugins/XmlMaterialProfile/product_to_id.json
Normal file
12
plugins/XmlMaterialProfile/product_to_id.json
Normal 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"
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue