mirror of
https://github.com/Ultimaker/Cura.git
synced 2025-07-10 00:07:51 -06:00
Merge branch 'master' into python_type_hinting
This commit is contained in:
commit
d4619da358
132 changed files with 42584 additions and 952 deletions
|
@ -83,20 +83,20 @@ class ThreeMFReader(MeshReader):
|
|||
definition = QualityManager.getInstance().getParentMachineDefinition(global_container_stack.getBottom())
|
||||
node.callDecoration("getStack").getTop().setDefinition(definition)
|
||||
|
||||
setting_container = node.callDecoration("getStack").getTop()
|
||||
for setting in xml_settings:
|
||||
setting_key = setting.get("key")
|
||||
setting_value = setting.text
|
||||
setting_container = node.callDecoration("getStack").getTop()
|
||||
for setting in xml_settings:
|
||||
setting_key = setting.get("key")
|
||||
setting_value = setting.text
|
||||
|
||||
# Extruder_nr is a special case.
|
||||
if setting_key == "extruder_nr":
|
||||
extruder_stack = ExtruderManager.getInstance().getExtruderStack(int(setting_value))
|
||||
if extruder_stack:
|
||||
node.callDecoration("setActiveExtruder", extruder_stack.getId())
|
||||
else:
|
||||
Logger.log("w", "Unable to find extruder in position %s", setting_value)
|
||||
continue
|
||||
setting_container.setProperty(setting_key,"value", setting_value)
|
||||
# Extruder_nr is a special case.
|
||||
if setting_key == "extruder_nr":
|
||||
extruder_stack = ExtruderManager.getInstance().getExtruderStack(int(setting_value))
|
||||
if extruder_stack:
|
||||
node.callDecoration("setActiveExtruder", extruder_stack.getId())
|
||||
else:
|
||||
Logger.log("w", "Unable to find extruder in position %s", setting_value)
|
||||
continue
|
||||
setting_container.setProperty(setting_key,"value", setting_value)
|
||||
|
||||
if len(node.getChildren()) > 0:
|
||||
group_decorator = GroupDecorator()
|
||||
|
@ -184,9 +184,22 @@ class ThreeMFReader(MeshReader):
|
|||
continue
|
||||
|
||||
build_item_node = self._createNodeFromObject(object, self._base_name + "_" + str(id))
|
||||
|
||||
# compensate for original center position, if object(s) is/are not around its zero position
|
||||
transform_matrix = Matrix()
|
||||
mesh_data = build_item_node.getMeshData()
|
||||
if mesh_data is not None:
|
||||
extents = mesh_data.getExtents()
|
||||
center_vector = Vector(extents.center.x, extents.center.y, extents.center.z)
|
||||
transform_matrix.setByTranslation(center_vector)
|
||||
|
||||
# offset with transform from 3mf
|
||||
transform = build_item.get("transform")
|
||||
if transform is not None:
|
||||
build_item_node.setTransformation(self._createMatrixFromTransformationString(transform))
|
||||
transform_matrix.multiply(self._createMatrixFromTransformationString(transform))
|
||||
|
||||
build_item_node.setTransformation(transform_matrix)
|
||||
|
||||
global_container_stack = Application.getInstance().getGlobalContainerStack()
|
||||
|
||||
# Create a transformation Matrix to convert from 3mf worldspace into ours.
|
||||
|
|
474
plugins/3MFReader/ThreeMFWorkspaceReader.py
Normal file
474
plugins/3MFReader/ThreeMFWorkspaceReader.py
Normal file
|
@ -0,0 +1,474 @@
|
|||
from UM.Workspace.WorkspaceReader import WorkspaceReader
|
||||
from UM.Application import Application
|
||||
|
||||
from UM.Logger import Logger
|
||||
from UM.i18n import i18nCatalog
|
||||
from UM.Settings.ContainerStack import ContainerStack
|
||||
from UM.Settings.DefinitionContainer import DefinitionContainer
|
||||
from UM.Settings.InstanceContainer import InstanceContainer
|
||||
from UM.Settings.ContainerRegistry import ContainerRegistry
|
||||
from UM.MimeTypeDatabase import MimeTypeDatabase
|
||||
from UM.Job import Job
|
||||
from UM.Preferences import Preferences
|
||||
from .WorkspaceDialog import WorkspaceDialog
|
||||
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
from cura.Settings.ExtruderManager import ExtruderManager
|
||||
|
||||
import zipfile
|
||||
import io
|
||||
import configparser
|
||||
|
||||
i18n_catalog = i18nCatalog("cura")
|
||||
|
||||
|
||||
## Base implementation for reading 3MF workspace files.
|
||||
class ThreeMFWorkspaceReader(WorkspaceReader):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._supported_extensions = [".3mf"]
|
||||
self._dialog = WorkspaceDialog()
|
||||
self._3mf_mesh_reader = None
|
||||
self._container_registry = ContainerRegistry.getInstance()
|
||||
self._definition_container_suffix = ContainerRegistry.getMimeTypeForContainer(DefinitionContainer).preferredSuffix
|
||||
self._material_container_suffix = None # We have to wait until all other plugins are loaded before we can set it
|
||||
self._instance_container_suffix = ContainerRegistry.getMimeTypeForContainer(InstanceContainer).preferredSuffix
|
||||
self._container_stack_suffix = ContainerRegistry.getMimeTypeForContainer(ContainerStack).preferredSuffix
|
||||
|
||||
self._resolve_strategies = {}
|
||||
|
||||
self._id_mapping = {}
|
||||
|
||||
## Get a unique name based on the old_id. This is different from directly calling the registry in that it caches results.
|
||||
# This has nothing to do with speed, but with getting consistent new naming for instances & objects.
|
||||
def getNewId(self, old_id):
|
||||
if old_id not in self._id_mapping:
|
||||
self._id_mapping[old_id] = self._container_registry.uniqueName(old_id)
|
||||
return self._id_mapping[old_id]
|
||||
|
||||
def preRead(self, file_name):
|
||||
self._3mf_mesh_reader = Application.getInstance().getMeshFileHandler().getReaderForFile(file_name)
|
||||
if self._3mf_mesh_reader and self._3mf_mesh_reader.preRead(file_name) == WorkspaceReader.PreReadResult.accepted:
|
||||
pass
|
||||
else:
|
||||
Logger.log("w", "Could not find reader that was able to read the scene data for 3MF workspace")
|
||||
return WorkspaceReader.PreReadResult.failed
|
||||
machine_name = ""
|
||||
# Check if there are any conflicts, so we can ask the user.
|
||||
archive = zipfile.ZipFile(file_name, "r")
|
||||
cura_file_names = [name for name in archive.namelist() if name.startswith("Cura/")]
|
||||
container_stack_files = [name for name in cura_file_names if name.endswith(self._container_stack_suffix)]
|
||||
self._resolve_strategies = {"machine": None, "quality_changes": None, "material": None}
|
||||
machine_conflict = False
|
||||
quality_changes_conflict = False
|
||||
for container_stack_file in container_stack_files:
|
||||
container_id = self._stripFileToId(container_stack_file)
|
||||
serialized = archive.open(container_stack_file).read().decode("utf-8")
|
||||
if machine_name == "":
|
||||
machine_name = self._getMachineNameFromSerializedStack(serialized)
|
||||
stacks = self._container_registry.findContainerStacks(id=container_id)
|
||||
if stacks:
|
||||
# Check if there are any changes at all in any of the container stacks.
|
||||
id_list = self._getContainerIdListFromSerialized(serialized)
|
||||
for index, container_id in enumerate(id_list):
|
||||
if stacks[0].getContainer(index).getId() != container_id:
|
||||
machine_conflict = True
|
||||
Job.yieldThread()
|
||||
|
||||
material_labels = []
|
||||
material_conflict = False
|
||||
xml_material_profile = self._getXmlProfileClass()
|
||||
if self._material_container_suffix is None:
|
||||
self._material_container_suffix = ContainerRegistry.getMimeTypeForContainer(xml_material_profile).preferredSuffix
|
||||
if xml_material_profile:
|
||||
material_container_files = [name for name in cura_file_names if name.endswith(self._material_container_suffix)]
|
||||
for material_container_file in material_container_files:
|
||||
container_id = self._stripFileToId(material_container_file)
|
||||
materials = self._container_registry.findInstanceContainers(id=container_id)
|
||||
material_labels.append(self._getMaterialLabelFromSerialized(archive.open(material_container_file).read().decode("utf-8")))
|
||||
if materials and not materials[0].isReadOnly(): # Only non readonly materials can be in conflict
|
||||
material_conflict = True
|
||||
Job.yieldThread()
|
||||
# Check if any quality_changes instance container is in conflict.
|
||||
instance_container_files = [name for name in cura_file_names if name.endswith(self._instance_container_suffix)]
|
||||
quality_name = ""
|
||||
quality_type = ""
|
||||
num_settings_overriden_by_quality_changes = 0 # How many settings are changed by the quality changes
|
||||
for instance_container_file in instance_container_files:
|
||||
container_id = self._stripFileToId(instance_container_file)
|
||||
instance_container = InstanceContainer(container_id)
|
||||
|
||||
# Deserialize InstanceContainer by converting read data from bytes to string
|
||||
instance_container.deserialize(archive.open(instance_container_file).read().decode("utf-8"))
|
||||
container_type = instance_container.getMetaDataEntry("type")
|
||||
if container_type == "quality_changes":
|
||||
quality_name = instance_container.getName()
|
||||
num_settings_overriden_by_quality_changes += len(instance_container._instances)
|
||||
# Check if quality changes already exists.
|
||||
quality_changes = self._container_registry.findInstanceContainers(id = container_id)
|
||||
if quality_changes:
|
||||
# Check if there really is a conflict by comparing the values
|
||||
if quality_changes[0] != instance_container:
|
||||
quality_changes_conflict = True
|
||||
elif container_type == "quality":
|
||||
# If the quality name is not set (either by quality or changes, set it now)
|
||||
# Quality changes should always override this (as they are "on top")
|
||||
if quality_name == "":
|
||||
quality_name = instance_container.getName()
|
||||
quality_type = instance_container.getName()
|
||||
Job.yieldThread()
|
||||
num_visible_settings = 0
|
||||
try:
|
||||
temp_preferences = Preferences()
|
||||
temp_preferences.readFromFile(io.TextIOWrapper(archive.open("Cura/preferences.cfg"))) # We need to wrap it, else the archive parser breaks.
|
||||
|
||||
visible_settings_string = temp_preferences.getValue("general/visible_settings")
|
||||
if visible_settings_string is not None:
|
||||
num_visible_settings = len(visible_settings_string.split(";"))
|
||||
active_mode = temp_preferences.getValue("cura/active_mode")
|
||||
if not active_mode:
|
||||
active_mode = Preferences.getInstance().getValue("cura/active_mode")
|
||||
except KeyError:
|
||||
# If there is no preferences file, it's not a workspace, so notify user of failure.
|
||||
Logger.log("w", "File %s is not a valid workspace.", file_name)
|
||||
return WorkspaceReader.PreReadResult.failed
|
||||
|
||||
# Show the dialog, informing the user what is about to happen.
|
||||
self._dialog.setMachineConflict(machine_conflict)
|
||||
self._dialog.setQualityChangesConflict(quality_changes_conflict)
|
||||
self._dialog.setMaterialConflict(material_conflict)
|
||||
self._dialog.setNumVisibleSettings(num_visible_settings)
|
||||
self._dialog.setQualityName(quality_name)
|
||||
self._dialog.setQualityType(quality_type)
|
||||
self._dialog.setNumSettingsOverridenByQualityChanges(num_settings_overriden_by_quality_changes)
|
||||
self._dialog.setActiveMode(active_mode)
|
||||
self._dialog.setMachineName(machine_name)
|
||||
self._dialog.setMaterialLabels(material_labels)
|
||||
self._dialog.setHasObjectsOnPlate(Application.getInstance().getPlatformActivity)
|
||||
self._dialog.show()
|
||||
|
||||
# Block until the dialog is closed.
|
||||
self._dialog.waitForClose()
|
||||
|
||||
if self._dialog.getResult() == {}:
|
||||
return WorkspaceReader.PreReadResult.cancelled
|
||||
|
||||
self._resolve_strategies = self._dialog.getResult()
|
||||
|
||||
return WorkspaceReader.PreReadResult.accepted
|
||||
|
||||
def read(self, file_name):
|
||||
archive = zipfile.ZipFile(file_name, "r")
|
||||
|
||||
cura_file_names = [name for name in archive.namelist() if name.startswith("Cura/")]
|
||||
|
||||
# Create a shadow copy of the preferences (we don't want all of the preferences, but we do want to re-use its
|
||||
# parsing code.
|
||||
temp_preferences = Preferences()
|
||||
temp_preferences.readFromFile(io.TextIOWrapper(archive.open("Cura/preferences.cfg"))) # We need to wrap it, else the archive parser breaks.
|
||||
|
||||
# Copy a number of settings from the temp preferences to the global
|
||||
global_preferences = Preferences.getInstance()
|
||||
|
||||
visible_settings = temp_preferences.getValue("general/visible_settings")
|
||||
if visible_settings is None:
|
||||
Logger.log("w", "Workspace did not contain visible settings. Leaving visibility unchanged")
|
||||
else:
|
||||
global_preferences.setValue("general/visible_settings", visible_settings)
|
||||
|
||||
categories_expanded = temp_preferences.getValue("cura/categories_expanded")
|
||||
if categories_expanded is None:
|
||||
Logger.log("w", "Workspace did not contain expanded categories. Leaving them unchanged")
|
||||
else:
|
||||
global_preferences.setValue("cura/categories_expanded", categories_expanded)
|
||||
|
||||
Application.getInstance().expandedCategoriesChanged.emit() # Notify the GUI of the change
|
||||
|
||||
self._id_mapping = {}
|
||||
|
||||
# We don't add containers right away, but wait right until right before the stack serialization.
|
||||
# We do this so that if something goes wrong, it's easier to clean up.
|
||||
containers_to_add = []
|
||||
|
||||
# TODO: For the moment we use pretty naive existence checking. If the ID is the same, we assume in quite a few
|
||||
# TODO: cases that the container loaded is the same (most notable in materials & definitions).
|
||||
# TODO: It might be possible that we need to add smarter checking in the future.
|
||||
Logger.log("d", "Workspace loading is checking definitions...")
|
||||
# Get all the definition files & check if they exist. If not, add them.
|
||||
definition_container_files = [name for name in cura_file_names if name.endswith(self._definition_container_suffix)]
|
||||
for definition_container_file in definition_container_files:
|
||||
container_id = self._stripFileToId(definition_container_file)
|
||||
definitions = self._container_registry.findDefinitionContainers(id=container_id)
|
||||
if not definitions:
|
||||
definition_container = DefinitionContainer(container_id)
|
||||
definition_container.deserialize(archive.open(definition_container_file).read().decode("utf-8"))
|
||||
self._container_registry.addContainer(definition_container)
|
||||
Job.yieldThread()
|
||||
|
||||
Logger.log("d", "Workspace loading is checking materials...")
|
||||
material_containers = []
|
||||
# Get all the material files and check if they exist. If not, add them.
|
||||
xml_material_profile = self._getXmlProfileClass()
|
||||
if self._material_container_suffix is None:
|
||||
self._material_container_suffix = ContainerRegistry.getMimeTypeForContainer(xml_material_profile).suffixes[0]
|
||||
if xml_material_profile:
|
||||
material_container_files = [name for name in cura_file_names if name.endswith(self._material_container_suffix)]
|
||||
for material_container_file in material_container_files:
|
||||
container_id = self._stripFileToId(material_container_file)
|
||||
materials = self._container_registry.findInstanceContainers(id=container_id)
|
||||
if not materials:
|
||||
material_container = xml_material_profile(container_id)
|
||||
material_container.deserialize(archive.open(material_container_file).read().decode("utf-8"))
|
||||
containers_to_add.append(material_container)
|
||||
else:
|
||||
if not materials[0].isReadOnly(): # Only create new materials if they are not read only.
|
||||
if self._resolve_strategies["material"] == "override":
|
||||
materials[0].deserialize(archive.open(material_container_file).read().decode("utf-8"))
|
||||
elif self._resolve_strategies["material"] == "new":
|
||||
# Note that we *must* deserialize it with a new ID, as multiple containers will be
|
||||
# auto created & added.
|
||||
material_container = xml_material_profile(self.getNewId(container_id))
|
||||
material_container.deserialize(archive.open(material_container_file).read().decode("utf-8"))
|
||||
containers_to_add.append(material_container)
|
||||
material_containers.append(material_container)
|
||||
Job.yieldThread()
|
||||
|
||||
Logger.log("d", "Workspace loading is checking instance containers...")
|
||||
# Get quality_changes and user profiles saved in the workspace
|
||||
instance_container_files = [name for name in cura_file_names if name.endswith(self._instance_container_suffix)]
|
||||
user_instance_containers = []
|
||||
quality_changes_instance_containers = []
|
||||
for instance_container_file in instance_container_files:
|
||||
container_id = self._stripFileToId(instance_container_file)
|
||||
instance_container = InstanceContainer(container_id)
|
||||
|
||||
# Deserialize InstanceContainer by converting read data from bytes to string
|
||||
instance_container.deserialize(archive.open(instance_container_file).read().decode("utf-8"))
|
||||
container_type = instance_container.getMetaDataEntry("type")
|
||||
Job.yieldThread()
|
||||
if container_type == "user":
|
||||
# Check if quality changes already exists.
|
||||
user_containers = self._container_registry.findInstanceContainers(id=container_id)
|
||||
if not user_containers:
|
||||
containers_to_add.append(instance_container)
|
||||
else:
|
||||
if self._resolve_strategies["machine"] == "override" or self._resolve_strategies["machine"] is None:
|
||||
user_containers[0].deserialize(archive.open(instance_container_file).read().decode("utf-8"))
|
||||
elif self._resolve_strategies["machine"] == "new":
|
||||
# The machine is going to get a spiffy new name, so ensure that the id's of user settings match.
|
||||
extruder_id = instance_container.getMetaDataEntry("extruder", None)
|
||||
if extruder_id:
|
||||
new_id = self.getNewId(extruder_id) + "_current_settings"
|
||||
instance_container._id = new_id
|
||||
instance_container.setName(new_id)
|
||||
instance_container.setMetaDataEntry("extruder", self.getNewId(extruder_id))
|
||||
containers_to_add.append(instance_container)
|
||||
|
||||
machine_id = instance_container.getMetaDataEntry("machine", None)
|
||||
if machine_id:
|
||||
new_id = self.getNewId(machine_id) + "_current_settings"
|
||||
instance_container._id = new_id
|
||||
instance_container.setName(new_id)
|
||||
instance_container.setMetaDataEntry("machine", self.getNewId(machine_id))
|
||||
containers_to_add.append(instance_container)
|
||||
user_instance_containers.append(instance_container)
|
||||
elif container_type == "quality_changes":
|
||||
# Check if quality changes already exists.
|
||||
quality_changes = self._container_registry.findInstanceContainers(id = container_id)
|
||||
if not quality_changes:
|
||||
containers_to_add.append(instance_container)
|
||||
else:
|
||||
if self._resolve_strategies["quality_changes"] == "override":
|
||||
quality_changes[0].deserialize(archive.open(instance_container_file).read().decode("utf-8"))
|
||||
elif self._resolve_strategies["quality_changes"] is None:
|
||||
# The ID already exists, but nothing in the values changed, so do nothing.
|
||||
pass
|
||||
quality_changes_instance_containers.append(instance_container)
|
||||
else:
|
||||
continue
|
||||
|
||||
# Add all the containers right before we try to add / serialize the stack
|
||||
for container in containers_to_add:
|
||||
self._container_registry.addContainer(container)
|
||||
container.setDirty(True)
|
||||
|
||||
# Get the stack(s) saved in the workspace.
|
||||
Logger.log("d", "Workspace loading is checking stacks containers...")
|
||||
container_stack_files = [name for name in cura_file_names if name.endswith(self._container_stack_suffix)]
|
||||
global_stack = None
|
||||
extruder_stacks = []
|
||||
container_stacks_added = []
|
||||
try:
|
||||
for container_stack_file in container_stack_files:
|
||||
container_id = self._stripFileToId(container_stack_file)
|
||||
|
||||
# Check if a stack by this ID already exists;
|
||||
container_stacks = self._container_registry.findContainerStacks(id=container_id)
|
||||
if container_stacks:
|
||||
stack = container_stacks[0]
|
||||
if self._resolve_strategies["machine"] == "override":
|
||||
# TODO: HACK
|
||||
# There is a machine, check if it has authenticationd 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(container_stack_file).read().decode("utf-8"))
|
||||
if network_authentication_id:
|
||||
container_stacks[0].addMetaDataEntry("network_authentication_id", network_authentication_id)
|
||||
if network_authentication_key:
|
||||
container_stacks[0].addMetaDataEntry("network_authentication_key", network_authentication_key)
|
||||
elif self._resolve_strategies["machine"] == "new":
|
||||
new_id = self.getNewId(container_id)
|
||||
stack = ContainerStack(new_id)
|
||||
stack.deserialize(archive.open(container_stack_file).read().decode("utf-8"))
|
||||
|
||||
# Ensure a unique ID and name
|
||||
stack._id = new_id
|
||||
|
||||
# Extruder stacks are "bound" to a machine. If we add the machine as a new one, the id of the
|
||||
# bound machine also needs to change.
|
||||
if stack.getMetaDataEntry("machine", None):
|
||||
stack.setMetaDataEntry("machine", self.getNewId(stack.getMetaDataEntry("machine")))
|
||||
|
||||
if stack.getMetaDataEntry("type") != "extruder_train":
|
||||
# Only machines need a new name, stacks may be non-unique
|
||||
stack.setName(self._container_registry.uniqueName(stack.getName()))
|
||||
container_stacks_added.append(stack)
|
||||
self._container_registry.addContainer(stack)
|
||||
else:
|
||||
Logger.log("w", "Resolve strategy of %s for machine is not supported", self._resolve_strategies["machine"])
|
||||
else:
|
||||
stack = ContainerStack(container_id)
|
||||
# Deserialize stack by converting read data from bytes to string
|
||||
stack.deserialize(archive.open(container_stack_file).read().decode("utf-8"))
|
||||
container_stacks_added.append(stack)
|
||||
self._container_registry.addContainer(stack)
|
||||
|
||||
if stack.getMetaDataEntry("type") == "extruder_train":
|
||||
extruder_stacks.append(stack)
|
||||
else:
|
||||
global_stack = stack
|
||||
Job.yieldThread()
|
||||
except:
|
||||
Logger.logException("w", "We failed to serialize the stack. Trying to clean up.")
|
||||
# Something went really wrong. Try to remove any data that we added.
|
||||
for container in containers_to_add:
|
||||
self._container_registry.getInstance().removeContainer(container.getId())
|
||||
|
||||
for container in container_stacks_added:
|
||||
self._container_registry.getInstance().removeContainer(container.getId())
|
||||
|
||||
return None
|
||||
|
||||
if self._resolve_strategies["machine"] == "new":
|
||||
# A new machine was made, but it was serialized with the wrong user container. Fix that now.
|
||||
for container in user_instance_containers:
|
||||
extruder_id = container.getMetaDataEntry("extruder", None)
|
||||
if extruder_id:
|
||||
for extruder in extruder_stacks:
|
||||
if extruder.getId() == extruder_id:
|
||||
extruder.replaceContainer(0, container)
|
||||
continue
|
||||
machine_id = container.getMetaDataEntry("machine", None)
|
||||
if machine_id:
|
||||
if global_stack.getId() == machine_id:
|
||||
global_stack.replaceContainer(0, container)
|
||||
continue
|
||||
|
||||
if self._resolve_strategies["quality_changes"] == "new":
|
||||
# Quality changes needs to get a new ID, added to registry and to the right stacks
|
||||
for container in quality_changes_instance_containers:
|
||||
old_id = container.getId()
|
||||
container.setName(self._container_registry.uniqueName(container.getName()))
|
||||
# We're not really supposed to change the ID in normal cases, but this is an exception.
|
||||
container._id = self.getNewId(container.getId())
|
||||
|
||||
# The container was not added yet, as it didn't have an unique ID. It does now, so add it.
|
||||
self._container_registry.addContainer(container)
|
||||
|
||||
# Replace the quality changes container
|
||||
old_container = global_stack.findContainer({"type": "quality_changes"})
|
||||
if old_container.getId() == old_id:
|
||||
quality_changes_index = global_stack.getContainerIndex(old_container)
|
||||
global_stack.replaceContainer(quality_changes_index, container)
|
||||
continue
|
||||
|
||||
for stack in extruder_stacks:
|
||||
old_container = stack.findContainer({"type": "quality_changes"})
|
||||
if old_container.getId() == old_id:
|
||||
quality_changes_index = stack.getContainerIndex(old_container)
|
||||
stack.replaceContainer(quality_changes_index, container)
|
||||
|
||||
if self._resolve_strategies["material"] == "new":
|
||||
for material in material_containers:
|
||||
old_material = global_stack.findContainer({"type": "material"})
|
||||
if old_material.getId() in self._id_mapping:
|
||||
material_index = global_stack.getContainerIndex(old_material)
|
||||
global_stack.replaceContainer(material_index, material)
|
||||
continue
|
||||
|
||||
for stack in extruder_stacks:
|
||||
old_material = stack.findContainer({"type": "material"})
|
||||
if old_material.getId() in self._id_mapping:
|
||||
material_index = stack.getContainerIndex(old_material)
|
||||
stack.replaceContainer(material_index, material)
|
||||
continue
|
||||
|
||||
for stack in extruder_stacks:
|
||||
ExtruderManager.getInstance().registerExtruder(stack, global_stack.getId())
|
||||
else:
|
||||
# Machine has no extruders, but it needs to be registered with the extruder manager.
|
||||
ExtruderManager.getInstance().registerExtruder(None, global_stack.getId())
|
||||
|
||||
Logger.log("d", "Workspace loading is notifying rest of the code of changes...")
|
||||
|
||||
# Notify everything/one that is to notify about changes.
|
||||
global_stack.containersChanged.emit(global_stack.getTop())
|
||||
|
||||
for stack in extruder_stacks:
|
||||
stack.setNextStack(global_stack)
|
||||
stack.containersChanged.emit(stack.getTop())
|
||||
|
||||
# Actually change the active machine.
|
||||
Application.getInstance().setGlobalContainerStack(global_stack)
|
||||
|
||||
# Load all the nodes / meshdata of the workspace
|
||||
nodes = self._3mf_mesh_reader.read(file_name)
|
||||
if nodes is None:
|
||||
nodes = []
|
||||
return nodes
|
||||
|
||||
def _stripFileToId(self, file):
|
||||
return file.replace("Cura/", "").split(".")[0]
|
||||
|
||||
def _getXmlProfileClass(self):
|
||||
return self._container_registry.getContainerForMimeType(MimeTypeDatabase.getMimeType("application/x-ultimaker-material-profile"))
|
||||
|
||||
## Get the list of ID's of all containers in a container stack by partially parsing it's serialized data.
|
||||
def _getContainerIdListFromSerialized(self, serialized):
|
||||
parser = configparser.ConfigParser(interpolation=None, empty_lines_in_values=False)
|
||||
parser.read_string(serialized)
|
||||
|
||||
container_ids = []
|
||||
if "containers" in parser:
|
||||
for index, container_id in parser.items("containers"):
|
||||
container_ids.append(container_id)
|
||||
elif parser.has_option("general", "containers"):
|
||||
container_string = parser["general"].get("containers", "")
|
||||
container_list = container_string.split(",")
|
||||
container_ids = [container_id for container_id in container_list if container_id != ""]
|
||||
|
||||
return container_ids
|
||||
|
||||
def _getMachineNameFromSerializedStack(self, serialized):
|
||||
parser = configparser.ConfigParser(interpolation=None, empty_lines_in_values=False)
|
||||
parser.read_string(serialized)
|
||||
return parser["general"].get("name", "")
|
||||
|
||||
def _getMaterialLabelFromSerialized(self, serialized):
|
||||
data = ET.fromstring(serialized)
|
||||
metadata = data.iterfind("./um:metadata/um:name/um:label", {"um": "http://www.ultimaker.com/material"})
|
||||
for entry in metadata:
|
||||
return entry.text
|
||||
pass
|
||||
|
228
plugins/3MFReader/WorkspaceDialog.py
Normal file
228
plugins/3MFReader/WorkspaceDialog.py
Normal file
|
@ -0,0 +1,228 @@
|
|||
# Copyright (c) 2016 Ultimaker B.V.
|
||||
# Cura is released under the terms of the AGPLv3 or higher.
|
||||
|
||||
from PyQt5.QtCore import Qt, QUrl, pyqtSignal, pyqtSlot, QObject, pyqtProperty, QCoreApplication
|
||||
from PyQt5.QtQml import QQmlComponent, QQmlContext
|
||||
from UM.PluginRegistry import PluginRegistry
|
||||
from UM.Application import Application
|
||||
from UM.Logger import Logger
|
||||
from UM.i18n import i18nCatalog
|
||||
from UM.Settings.ContainerRegistry import ContainerRegistry
|
||||
|
||||
import os
|
||||
import threading
|
||||
import time
|
||||
i18n_catalog = i18nCatalog("cura")
|
||||
|
||||
|
||||
class WorkspaceDialog(QObject):
|
||||
showDialogSignal = pyqtSignal()
|
||||
|
||||
def __init__(self, parent = None):
|
||||
super().__init__(parent)
|
||||
self._component = None
|
||||
self._context = None
|
||||
self._view = None
|
||||
self._qml_url = "WorkspaceDialog.qml"
|
||||
self._lock = threading.Lock()
|
||||
self._default_strategy = "override"
|
||||
self._result = {"machine": self._default_strategy,
|
||||
"quality_changes": self._default_strategy,
|
||||
"material": self._default_strategy}
|
||||
self._visible = False
|
||||
self.showDialogSignal.connect(self.__show)
|
||||
|
||||
self._has_quality_changes_conflict = False
|
||||
self._has_machine_conflict = False
|
||||
self._has_material_conflict = False
|
||||
self._num_visible_settings = 0
|
||||
self._active_mode = ""
|
||||
self._quality_name = ""
|
||||
self._num_settings_overriden_by_quality_changes = 0
|
||||
self._quality_type = ""
|
||||
self._machine_name = ""
|
||||
self._material_labels = []
|
||||
self._objects_on_plate = False
|
||||
|
||||
machineConflictChanged = pyqtSignal()
|
||||
qualityChangesConflictChanged = pyqtSignal()
|
||||
materialConflictChanged = pyqtSignal()
|
||||
numVisibleSettingsChanged = pyqtSignal()
|
||||
activeModeChanged = pyqtSignal()
|
||||
qualityNameChanged = pyqtSignal()
|
||||
numSettingsOverridenByQualityChangesChanged = pyqtSignal()
|
||||
qualityTypeChanged = pyqtSignal()
|
||||
machineNameChanged = pyqtSignal()
|
||||
materialLabelsChanged = pyqtSignal()
|
||||
objectsOnPlateChanged = pyqtSignal()
|
||||
|
||||
@pyqtProperty(bool, notify=objectsOnPlateChanged)
|
||||
def hasObjectsOnPlate(self):
|
||||
return self._objects_on_plate
|
||||
|
||||
def setHasObjectsOnPlate(self, objects_on_plate):
|
||||
self._objects_on_plate = objects_on_plate
|
||||
self.objectsOnPlateChanged.emit()
|
||||
|
||||
@pyqtProperty("QVariantList", notify = materialLabelsChanged)
|
||||
def materialLabels(self):
|
||||
return self._material_labels
|
||||
|
||||
def setMaterialLabels(self, material_labels):
|
||||
self._material_labels = material_labels
|
||||
self.materialLabelsChanged.emit()
|
||||
|
||||
@pyqtProperty(str, notify = machineNameChanged)
|
||||
def machineName(self):
|
||||
return self._machine_name
|
||||
|
||||
def setMachineName(self, machine_name):
|
||||
self._machine_name = machine_name
|
||||
self.machineNameChanged.emit()
|
||||
|
||||
@pyqtProperty(str, notify=qualityTypeChanged)
|
||||
def qualityType(self):
|
||||
return self._quality_type
|
||||
|
||||
def setQualityType(self, quality_type):
|
||||
self._quality_type = quality_type
|
||||
self.qualityTypeChanged.emit()
|
||||
|
||||
@pyqtProperty(int, notify=numSettingsOverridenByQualityChangesChanged)
|
||||
def numSettingsOverridenByQualityChanges(self):
|
||||
return self._num_settings_overriden_by_quality_changes
|
||||
|
||||
def setNumSettingsOverridenByQualityChanges(self, num_settings_overriden_by_quality_changes):
|
||||
self._num_settings_overriden_by_quality_changes = num_settings_overriden_by_quality_changes
|
||||
self.numSettingsOverridenByQualityChangesChanged.emit()
|
||||
|
||||
@pyqtProperty(str, notify=qualityNameChanged)
|
||||
def qualityName(self):
|
||||
return self._quality_name
|
||||
|
||||
def setQualityName(self, quality_name):
|
||||
self._quality_name = quality_name
|
||||
self.qualityNameChanged.emit()
|
||||
|
||||
@pyqtProperty(str, notify=activeModeChanged)
|
||||
def activeMode(self):
|
||||
return self._active_mode
|
||||
|
||||
def setActiveMode(self, active_mode):
|
||||
if active_mode == 0:
|
||||
self._active_mode = i18n_catalog.i18nc("@title:tab", "Recommended")
|
||||
else:
|
||||
self._active_mode = i18n_catalog.i18nc("@title:tab", "Custom")
|
||||
self.activeModeChanged.emit()
|
||||
|
||||
@pyqtProperty(int, constant = True)
|
||||
def totalNumberOfSettings(self):
|
||||
return len(ContainerRegistry.getInstance().findDefinitionContainers(id="fdmprinter")[0].getAllKeys())
|
||||
|
||||
@pyqtProperty(int, notify = numVisibleSettingsChanged)
|
||||
def numVisibleSettings(self):
|
||||
return self._num_visible_settings
|
||||
|
||||
def setNumVisibleSettings(self, num_visible_settings):
|
||||
self._num_visible_settings = num_visible_settings
|
||||
self.numVisibleSettingsChanged.emit()
|
||||
|
||||
@pyqtProperty(bool, notify = machineConflictChanged)
|
||||
def machineConflict(self):
|
||||
return self._has_machine_conflict
|
||||
|
||||
@pyqtProperty(bool, notify=qualityChangesConflictChanged)
|
||||
def qualityChangesConflict(self):
|
||||
return self._has_quality_changes_conflict
|
||||
|
||||
@pyqtProperty(bool, notify=materialConflictChanged)
|
||||
def materialConflict(self):
|
||||
return self._has_material_conflict
|
||||
|
||||
@pyqtSlot(str, str)
|
||||
def setResolveStrategy(self, key, strategy):
|
||||
if key in self._result:
|
||||
self._result[key] = strategy
|
||||
|
||||
def setMaterialConflict(self, material_conflict):
|
||||
self._has_material_conflict = material_conflict
|
||||
self.materialConflictChanged.emit()
|
||||
|
||||
def setMachineConflict(self, machine_conflict):
|
||||
self._has_machine_conflict = machine_conflict
|
||||
self.machineConflictChanged.emit()
|
||||
|
||||
def setQualityChangesConflict(self, quality_changes_conflict):
|
||||
self._has_quality_changes_conflict = quality_changes_conflict
|
||||
self.qualityChangesConflictChanged.emit()
|
||||
|
||||
def getResult(self):
|
||||
if "machine" in self._result and not self._has_machine_conflict:
|
||||
self._result["machine"] = None
|
||||
if "quality_changes" in self._result and not self._has_quality_changes_conflict:
|
||||
self._result["quality_changes"] = None
|
||||
if "material" in self._result and not self._has_material_conflict:
|
||||
self._result["material"] = None
|
||||
return self._result
|
||||
|
||||
def _createViewFromQML(self):
|
||||
path = QUrl.fromLocalFile(os.path.join(PluginRegistry.getInstance().getPluginPath("3MFReader"), self._qml_url))
|
||||
self._component = QQmlComponent(Application.getInstance()._engine, path)
|
||||
self._context = QQmlContext(Application.getInstance()._engine.rootContext())
|
||||
self._context.setContextProperty("manager", self)
|
||||
self._view = self._component.create(self._context)
|
||||
if self._view is None:
|
||||
Logger.log("c", "QQmlComponent status %s", self._component.status())
|
||||
Logger.log("c", "QQmlComponent error string %s", self._component.errorString())
|
||||
|
||||
def show(self):
|
||||
# Emit signal so the right thread actually shows the view.
|
||||
if threading.current_thread() != threading.main_thread():
|
||||
self._lock.acquire()
|
||||
# Reset the result
|
||||
self._result = {"machine": self._default_strategy,
|
||||
"quality_changes": self._default_strategy,
|
||||
"material": self._default_strategy}
|
||||
self._visible = True
|
||||
self.showDialogSignal.emit()
|
||||
|
||||
@pyqtSlot()
|
||||
## Used to notify the dialog so the lock can be released.
|
||||
def notifyClosed(self):
|
||||
self._result = {}
|
||||
self._visible = False
|
||||
self._lock.release()
|
||||
|
||||
def hide(self):
|
||||
self._visible = False
|
||||
self._lock.release()
|
||||
self._view.hide()
|
||||
|
||||
@pyqtSlot()
|
||||
def onOkButtonClicked(self):
|
||||
self._view.hide()
|
||||
self.hide()
|
||||
|
||||
@pyqtSlot()
|
||||
def onCancelButtonClicked(self):
|
||||
self._view.hide()
|
||||
self.hide()
|
||||
self._result = {}
|
||||
|
||||
## Block thread until the dialog is closed.
|
||||
def waitForClose(self):
|
||||
if self._visible:
|
||||
if threading.current_thread() != threading.main_thread():
|
||||
self._lock.acquire()
|
||||
self._lock.release()
|
||||
else:
|
||||
# If this is not run from a separate thread, we need to ensure that the events are still processed.
|
||||
while self._visible:
|
||||
time.sleep(1 / 50)
|
||||
QCoreApplication.processEvents() # Ensure that the GUI does not freeze.
|
||||
|
||||
def __show(self):
|
||||
if self._view is None:
|
||||
self._createViewFromQML()
|
||||
if self._view:
|
||||
self._view.show()
|
319
plugins/3MFReader/WorkspaceDialog.qml
Normal file
319
plugins/3MFReader/WorkspaceDialog.qml
Normal file
|
@ -0,0 +1,319 @@
|
|||
// Copyright (c) 2016 Ultimaker B.V.
|
||||
// Cura is released under the terms of the AGPLv3 or higher.
|
||||
|
||||
import QtQuick 2.1
|
||||
import QtQuick.Controls 1.1
|
||||
import QtQuick.Layouts 1.1
|
||||
import QtQuick.Window 2.1
|
||||
|
||||
import UM 1.1 as UM
|
||||
|
||||
UM.Dialog
|
||||
{
|
||||
title: catalog.i18nc("@title:window", "Open Project")
|
||||
|
||||
width: 550
|
||||
minimumWidth: 550
|
||||
maximumWidth: 550
|
||||
|
||||
height: 350
|
||||
minimumHeight: 350
|
||||
maximumHeight: 350
|
||||
property int comboboxHeight: 15
|
||||
property int spacerHeight: 10
|
||||
onClosing: manager.notifyClosed()
|
||||
onVisibleChanged:
|
||||
{
|
||||
if(visible)
|
||||
{
|
||||
machineResolveComboBox.currentIndex = 0
|
||||
qualityChangesResolveComboBox.currentIndex = 0
|
||||
materialConflictComboBox.currentIndex = 0
|
||||
}
|
||||
}
|
||||
Item
|
||||
{
|
||||
anchors.fill: parent
|
||||
|
||||
UM.I18nCatalog
|
||||
{
|
||||
id: catalog;
|
||||
name: "cura";
|
||||
}
|
||||
|
||||
ListModel
|
||||
{
|
||||
id: resolveStrategiesModel
|
||||
// Instead of directly adding the list elements, we add them afterwards.
|
||||
// This is because it's impossible to use setting function results to be bound to listElement properties directly.
|
||||
// See http://stackoverflow.com/questions/7659442/listelement-fields-as-properties
|
||||
Component.onCompleted:
|
||||
{
|
||||
append({"key": "override", "label": catalog.i18nc("@action:ComboBox option", "Update existing")});
|
||||
append({"key": "new", "label": catalog.i18nc("@action:ComboBox option", "Create new")});
|
||||
}
|
||||
}
|
||||
|
||||
Column
|
||||
{
|
||||
anchors.fill: parent
|
||||
spacing: 2
|
||||
Label
|
||||
{
|
||||
id: titleLabel
|
||||
text: catalog.i18nc("@action:title", "Summary - Cura Project")
|
||||
font.pixelSize: 22
|
||||
}
|
||||
Rectangle
|
||||
{
|
||||
id: separator
|
||||
color: "black"
|
||||
width: parent.width
|
||||
height: 1
|
||||
}
|
||||
Item // Spacer
|
||||
{
|
||||
height: spacerHeight
|
||||
width: height
|
||||
}
|
||||
|
||||
Label
|
||||
{
|
||||
text: catalog.i18nc("@action:label", "Printer settings")
|
||||
font.bold: true
|
||||
}
|
||||
|
||||
Row
|
||||
{
|
||||
width: parent.width
|
||||
height: childrenRect.height
|
||||
Label
|
||||
{
|
||||
text: catalog.i18nc("@action:label", "Name")
|
||||
width: parent.width / 3
|
||||
}
|
||||
Label
|
||||
{
|
||||
text: manager.machineName
|
||||
width: parent.width / 3
|
||||
}
|
||||
|
||||
UM.TooltipArea
|
||||
{
|
||||
id: machineResolveTooltip
|
||||
width: parent.width / 3
|
||||
height: visible ? comboboxHeight : 0
|
||||
visible: manager.machineConflict
|
||||
text: catalog.i18nc("@info:tooltip", "How should the conflict in the machine be resolved?")
|
||||
ComboBox
|
||||
{
|
||||
model: resolveStrategiesModel
|
||||
textRole: "label"
|
||||
id: machineResolveComboBox
|
||||
width: parent.width
|
||||
onActivated:
|
||||
{
|
||||
manager.setResolveStrategy("machine", resolveStrategiesModel.get(index).key)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Item // Spacer
|
||||
{
|
||||
height: spacerHeight
|
||||
width: height
|
||||
}
|
||||
|
||||
Label
|
||||
{
|
||||
text: catalog.i18nc("@action:label", "Profile settings")
|
||||
font.bold: true
|
||||
}
|
||||
|
||||
Row
|
||||
{
|
||||
width: parent.width
|
||||
height: childrenRect.height
|
||||
Label
|
||||
{
|
||||
text: catalog.i18nc("@action:label", "Name")
|
||||
width: parent.width / 3
|
||||
}
|
||||
Label
|
||||
{
|
||||
text: manager.qualityName
|
||||
width: parent.width / 3
|
||||
}
|
||||
|
||||
UM.TooltipArea
|
||||
{
|
||||
id: qualityChangesResolveTooltip
|
||||
width: parent.width / 3
|
||||
height: visible ? comboboxHeight : 0
|
||||
visible: manager.qualityChangesConflict
|
||||
text: catalog.i18nc("@info:tooltip", "How should the conflict in the profile be resolved?")
|
||||
ComboBox
|
||||
{
|
||||
model: resolveStrategiesModel
|
||||
textRole: "label"
|
||||
id: qualityChangesResolveComboBox
|
||||
width: parent.width
|
||||
onActivated:
|
||||
{
|
||||
manager.setResolveStrategy("quality_changes", resolveStrategiesModel.get(index).key)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Row
|
||||
{
|
||||
width: parent.width
|
||||
height: childrenRect.height
|
||||
Label
|
||||
{
|
||||
text: catalog.i18nc("@action:label", "Derivative from")
|
||||
width: parent.width / 3
|
||||
}
|
||||
Label
|
||||
{
|
||||
text: catalog.i18nc("@action:label", "%1, %2 override(s)" ).arg(manager.qualityType).arg(manager.numSettingsOverridenByQualityChanges)
|
||||
width: parent.width / 3
|
||||
}
|
||||
visible: manager.numSettingsOverridenByQualityChanges != 0
|
||||
}
|
||||
Item // Spacer
|
||||
{
|
||||
height: spacerHeight
|
||||
width: height
|
||||
}
|
||||
|
||||
Label
|
||||
{
|
||||
text: catalog.i18nc("@action:label", "Material settings")
|
||||
font.bold: true
|
||||
}
|
||||
|
||||
Repeater
|
||||
{
|
||||
model: manager.materialLabels
|
||||
delegate: Row
|
||||
{
|
||||
width: parent.width
|
||||
height: childrenRect.height
|
||||
Label
|
||||
{
|
||||
text: catalog.i18nc("@action:label", "Name")
|
||||
width: parent.width / 3
|
||||
}
|
||||
Label
|
||||
{
|
||||
text: modelData
|
||||
width: parent.width / 3
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Row
|
||||
{
|
||||
width: parent.width
|
||||
height: childrenRect.height
|
||||
visible: manager.materialConflict
|
||||
Item
|
||||
{
|
||||
width: parent.width / 3 * 2
|
||||
height: comboboxHeight
|
||||
}
|
||||
|
||||
UM.TooltipArea
|
||||
{
|
||||
id: materialResolveTooltip
|
||||
width: parent.width / 3
|
||||
height: visible ? comboboxHeight : 0
|
||||
|
||||
text: catalog.i18nc("@info:tooltip", "How should the conflict in the material be resolved?")
|
||||
ComboBox
|
||||
{
|
||||
model: resolveStrategiesModel
|
||||
textRole: "label"
|
||||
id: materialResolveComboBox
|
||||
width: parent.width
|
||||
onActivated:
|
||||
{
|
||||
manager.setResolveStrategy("material", resolveStrategiesModel.get(index).key)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Item // Spacer
|
||||
{
|
||||
height: spacerHeight
|
||||
width: height
|
||||
}
|
||||
|
||||
Label
|
||||
{
|
||||
text: catalog.i18nc("@action:label", "Setting visibility")
|
||||
font.bold: true
|
||||
}
|
||||
Row
|
||||
{
|
||||
width: parent.width
|
||||
height: childrenRect.height
|
||||
Label
|
||||
{
|
||||
text: catalog.i18nc("@action:label", "Mode")
|
||||
width: parent.width / 3
|
||||
}
|
||||
Label
|
||||
{
|
||||
text: manager.activeMode
|
||||
width: parent.width / 3
|
||||
}
|
||||
}
|
||||
Row
|
||||
{
|
||||
width: parent.width
|
||||
height: childrenRect.height
|
||||
Label
|
||||
{
|
||||
text: catalog.i18nc("@action:label", "Visible settings:")
|
||||
width: parent.width / 3
|
||||
}
|
||||
Label
|
||||
{
|
||||
text: catalog.i18nc("@action:label", "%1 out of %2" ).arg(manager.numVisibleSettings).arg(manager.totalNumberOfSettings)
|
||||
width: parent.width / 3
|
||||
}
|
||||
}
|
||||
Item // Spacer
|
||||
{
|
||||
height: spacerHeight
|
||||
width: height
|
||||
}
|
||||
Label
|
||||
{
|
||||
text: catalog.i18nc("@action:warning", "Loading a project will clear all models on the buildplate")
|
||||
visible: manager.hasObjectsOnPlate
|
||||
color: "red"
|
||||
width: parent.width
|
||||
wrapMode: Text.Wrap
|
||||
}
|
||||
}
|
||||
}
|
||||
rightButtons: [
|
||||
Button
|
||||
{
|
||||
id: ok_button
|
||||
text: catalog.i18nc("@action:button","OK");
|
||||
onClicked: { manager.onOkButtonClicked() }
|
||||
enabled: true
|
||||
},
|
||||
Button
|
||||
{
|
||||
id: cancel_button
|
||||
text: catalog.i18nc("@action:button","Cancel");
|
||||
onClicked: { manager.onCancelButtonClicked() }
|
||||
enabled: true
|
||||
}
|
||||
]
|
||||
}
|
|
@ -2,10 +2,11 @@
|
|||
# Cura is released under the terms of the AGPLv3 or higher.
|
||||
|
||||
from . import ThreeMFReader
|
||||
|
||||
from . import ThreeMFWorkspaceReader
|
||||
from UM.i18n import i18nCatalog
|
||||
catalog = i18nCatalog("cura")
|
||||
|
||||
|
||||
def getMetaData():
|
||||
return {
|
||||
"plugin": {
|
||||
|
@ -20,8 +21,17 @@ def getMetaData():
|
|||
"extension": "3mf",
|
||||
"description": catalog.i18nc("@item:inlistbox", "3MF File")
|
||||
}
|
||||
],
|
||||
"workspace_reader":
|
||||
[
|
||||
{
|
||||
"extension": "curaproject.3mf",
|
||||
"description": catalog.i18nc("@item:inlistbox", "3MF File")
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
def register(app):
|
||||
return { "mesh_reader": ThreeMFReader.ThreeMFReader() }
|
||||
return {"mesh_reader": ThreeMFReader.ThreeMFReader(),
|
||||
"workspace_reader": ThreeMFWorkspaceReader.ThreeMFWorkspaceReader()}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue