mirror of
https://github.com/Ultimaker/Cura.git
synced 2025-07-07 06:57:28 -06:00
Now using ThreeMFWriter to save PCB files
CURA-11561
This commit is contained in:
parent
38b67f8015
commit
b931029f1c
14 changed files with 207 additions and 545 deletions
|
@ -8,7 +8,6 @@ import QtQuick.Window 2.2
|
||||||
|
|
||||||
import UM 1.5 as UM
|
import UM 1.5 as UM
|
||||||
import Cura 1.1 as Cura
|
import Cura 1.1 as Cura
|
||||||
import PCBWriter 1.0 as PCBWriter
|
|
||||||
|
|
||||||
UM.Dialog
|
UM.Dialog
|
||||||
{
|
{
|
|
@ -32,7 +32,7 @@ RowLayout
|
||||||
UM.I18nCatalog { id: catalog; name: "cura" }
|
UM.I18nCatalog { id: catalog; name: "cura" }
|
||||||
|
|
||||||
text: catalog.i18nc("@tooltip",
|
text: catalog.i18nc("@tooltip",
|
||||||
"This setting can't be exported because it depends too much on the used printer capacities")
|
"This setting can't be exported because it depends on the used printer capacities")
|
||||||
visible: !modelData.selectable
|
visible: !modelData.selectable
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -8,7 +8,7 @@ import QtQuick.Window 2.2
|
||||||
|
|
||||||
import UM 1.5 as UM
|
import UM 1.5 as UM
|
||||||
import Cura 1.1 as Cura
|
import Cura 1.1 as Cura
|
||||||
import PCBWriter 1.0 as PCBWriter
|
import ThreeMFWriter 1.0 as ThreeMFWriter
|
||||||
|
|
||||||
ColumnLayout
|
ColumnLayout
|
||||||
{
|
{
|
||||||
|
@ -34,9 +34,9 @@ ColumnLayout
|
||||||
{
|
{
|
||||||
switch(modelData.category)
|
switch(modelData.category)
|
||||||
{
|
{
|
||||||
case PCBWriter.SettingsExportGroup.Global:
|
case ThreeMFWriter.SettingsExportGroup.Global:
|
||||||
return UM.Theme.getIcon("Sliders")
|
return UM.Theme.getIcon("Sliders")
|
||||||
case PCBWriter.SettingsExportGroup.Model:
|
case ThreeMFWriter.SettingsExportGroup.Model:
|
||||||
return UM.Theme.getIcon("View3D")
|
return UM.Theme.getIcon("View3D")
|
||||||
default:
|
default:
|
||||||
return ""
|
return ""
|
||||||
|
@ -50,7 +50,7 @@ ColumnLayout
|
||||||
{
|
{
|
||||||
id: settingsExtruderIcon
|
id: settingsExtruderIcon
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
visible: modelData.category === PCBWriter.SettingsExportGroup.Extruder
|
visible: modelData.category === ThreeMFWriter.SettingsExportGroup.Extruder
|
||||||
text: (modelData.extruder_index + 1).toString()
|
text: (modelData.extruder_index + 1).toString()
|
||||||
font: UM.Theme.getFont("tiny_emphasis")
|
font: UM.Theme.getFont("tiny_emphasis")
|
||||||
materialColor: modelData.extruder_color
|
materialColor: modelData.extruder_color
|
|
@ -3,7 +3,9 @@
|
||||||
|
|
||||||
import configparser
|
import configparser
|
||||||
from io import StringIO
|
from io import StringIO
|
||||||
|
from threading import Lock
|
||||||
import zipfile
|
import zipfile
|
||||||
|
from typing import Dict, Any
|
||||||
|
|
||||||
from UM.Application import Application
|
from UM.Application import Application
|
||||||
from UM.Logger import Logger
|
from UM.Logger import Logger
|
||||||
|
@ -13,15 +15,50 @@ from UM.Workspace.WorkspaceWriter import WorkspaceWriter
|
||||||
from UM.i18n import i18nCatalog
|
from UM.i18n import i18nCatalog
|
||||||
catalog = i18nCatalog("cura")
|
catalog = i18nCatalog("cura")
|
||||||
|
|
||||||
from cura.Utils.Threading import call_on_qt_thread
|
from .PCBDialog import PCBDialog
|
||||||
|
from .ThreeMFWriter import ThreeMFWriter
|
||||||
|
from .SettingsExportModel import SettingsExportModel
|
||||||
|
from .SettingsExportGroup import SettingsExportGroup
|
||||||
|
|
||||||
|
USER_SETTINGS_PATH = "Cura/user-settings.json"
|
||||||
|
|
||||||
|
|
||||||
class ThreeMFWorkspaceWriter(WorkspaceWriter):
|
class ThreeMFWorkspaceWriter(WorkspaceWriter):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
self._main_thread_lock = Lock()
|
||||||
|
self._success = False
|
||||||
|
self._export_model = None
|
||||||
|
self._stream = None
|
||||||
|
self._nodes = None
|
||||||
|
self._mode = None
|
||||||
|
self._config_dialog = None
|
||||||
|
|
||||||
@call_on_qt_thread
|
def _preWrite(self):
|
||||||
def write(self, stream, nodes, mode=WorkspaceWriter.OutputMode.BinaryMode):
|
is_pcb = False
|
||||||
|
if hasattr(self._stream, 'name'):
|
||||||
|
# This only works with local file, but we don't want remote PCB files yet
|
||||||
|
is_pcb = self._stream.name.endswith('.pcb')
|
||||||
|
|
||||||
|
if is_pcb:
|
||||||
|
self._config_dialog = PCBDialog()
|
||||||
|
self._config_dialog.finished.connect(self._onPCBConfigFinished)
|
||||||
|
self._config_dialog.show()
|
||||||
|
else:
|
||||||
|
self._doWrite()
|
||||||
|
|
||||||
|
def _onPCBConfigFinished(self, accepted: bool):
|
||||||
|
if accepted:
|
||||||
|
self._export_model = self._config_dialog.getModel()
|
||||||
|
self._doWrite()
|
||||||
|
else:
|
||||||
|
self._main_thread_lock.release()
|
||||||
|
|
||||||
|
def _doWrite(self):
|
||||||
|
self._write()
|
||||||
|
self._main_thread_lock.release()
|
||||||
|
|
||||||
|
def _write(self):
|
||||||
application = Application.getInstance()
|
application = Application.getInstance()
|
||||||
machine_manager = application.getMachineManager()
|
machine_manager = application.getMachineManager()
|
||||||
|
|
||||||
|
@ -30,24 +67,24 @@ class ThreeMFWorkspaceWriter(WorkspaceWriter):
|
||||||
if not mesh_writer: # We need to have the 3mf mesh writer, otherwise we can't save the entire workspace
|
if not mesh_writer: # We need to have the 3mf mesh writer, otherwise we can't save the entire workspace
|
||||||
self.setInformation(catalog.i18nc("@error:zip", "3MF Writer plug-in is corrupt."))
|
self.setInformation(catalog.i18nc("@error:zip", "3MF Writer plug-in is corrupt."))
|
||||||
Logger.error("3MF Writer class is unavailable. Can't write workspace.")
|
Logger.error("3MF Writer class is unavailable. Can't write workspace.")
|
||||||
return False
|
return
|
||||||
|
|
||||||
global_stack = machine_manager.activeMachine
|
global_stack = machine_manager.activeMachine
|
||||||
if global_stack is None:
|
if global_stack is None:
|
||||||
self.setInformation(catalog.i18nc("@error", "There is no workspace yet to write. Please add a printer first."))
|
self.setInformation(
|
||||||
|
catalog.i18nc("@error", "There is no workspace yet to write. Please add a printer first."))
|
||||||
Logger.error("Tried to write a 3MF workspace before there was a global stack.")
|
Logger.error("Tried to write a 3MF workspace before there was a global stack.")
|
||||||
return False
|
return
|
||||||
|
|
||||||
# Indicate that the 3mf mesh writer should not close the archive just yet (we still need to add stuff to it).
|
# Indicate that the 3mf mesh writer should not close the archive just yet (we still need to add stuff to it).
|
||||||
mesh_writer.setStoreArchive(True)
|
mesh_writer.setStoreArchive(True)
|
||||||
if not mesh_writer.write(stream, nodes, mode):
|
if not mesh_writer.write(self._stream, self._nodes, self._mode, self._export_model):
|
||||||
self.setInformation(mesh_writer.getInformation())
|
self.setInformation(mesh_writer.getInformation())
|
||||||
return False
|
return
|
||||||
|
|
||||||
archive = mesh_writer.getArchive()
|
archive = mesh_writer.getArchive()
|
||||||
if archive is None: # This happens if there was no mesh data to write.
|
if archive is None: # This happens if there was no mesh data to write.
|
||||||
archive = zipfile.ZipFile(stream, "w", compression = zipfile.ZIP_DEFLATED)
|
archive = zipfile.ZipFile(self._stream, "w", compression=zipfile.ZIP_DEFLATED)
|
||||||
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Add global container stack data to the archive.
|
# Add global container stack data to the archive.
|
||||||
|
@ -62,15 +99,21 @@ class ThreeMFWorkspaceWriter(WorkspaceWriter):
|
||||||
self._writeContainerToArchive(extruder_stack, archive)
|
self._writeContainerToArchive(extruder_stack, archive)
|
||||||
for container in extruder_stack.getContainers():
|
for container in extruder_stack.getContainers():
|
||||||
self._writeContainerToArchive(container, archive)
|
self._writeContainerToArchive(container, archive)
|
||||||
|
|
||||||
|
# Write user settings data
|
||||||
|
if self._export_model is not None:
|
||||||
|
user_settings_data = self._getUserSettings(self._export_model)
|
||||||
|
ThreeMFWriter._storeMetadataJson(user_settings_data, archive, USER_SETTINGS_PATH)
|
||||||
except PermissionError:
|
except PermissionError:
|
||||||
self.setInformation(catalog.i18nc("@error:zip", "No permission to write the workspace here."))
|
self.setInformation(catalog.i18nc("@error:zip", "No permission to write the workspace here."))
|
||||||
Logger.error("No permission to write workspace to this stream.")
|
Logger.error("No permission to write workspace to this stream.")
|
||||||
return False
|
return
|
||||||
|
|
||||||
# Write preferences to archive
|
# Write preferences to archive
|
||||||
original_preferences = Application.getInstance().getPreferences() # Copy only the preferences that we use to the workspace.
|
original_preferences = Application.getInstance().getPreferences() # Copy only the preferences that we use to the workspace.
|
||||||
temp_preferences = Preferences()
|
temp_preferences = Preferences()
|
||||||
for preference in {"general/visible_settings", "cura/active_mode", "cura/categories_expanded", "metadata/setting_version"}:
|
for preference in {"general/visible_settings", "cura/active_mode", "cura/categories_expanded",
|
||||||
|
"metadata/setting_version"}:
|
||||||
temp_preferences.addPreference(preference, None)
|
temp_preferences.addPreference(preference, None)
|
||||||
temp_preferences.setValue(preference, original_preferences.getValue(preference))
|
temp_preferences.setValue(preference, original_preferences.getValue(preference))
|
||||||
preferences_string = StringIO()
|
preferences_string = StringIO()
|
||||||
|
@ -98,13 +141,37 @@ class ThreeMFWorkspaceWriter(WorkspaceWriter):
|
||||||
except PermissionError:
|
except PermissionError:
|
||||||
self.setInformation(catalog.i18nc("@error:zip", "No permission to write the workspace here."))
|
self.setInformation(catalog.i18nc("@error:zip", "No permission to write the workspace here."))
|
||||||
Logger.error("No permission to write workspace to this stream.")
|
Logger.error("No permission to write workspace to this stream.")
|
||||||
return False
|
return
|
||||||
except EnvironmentError as e:
|
except EnvironmentError as e:
|
||||||
self.setInformation(catalog.i18nc("@error:zip", str(e)))
|
self.setInformation(catalog.i18nc("@error:zip", str(e)))
|
||||||
Logger.error("EnvironmentError when writing workspace to this stream: {err}".format(err=str(e)))
|
Logger.error("EnvironmentError when writing workspace to this stream: {err}".format(err=str(e)))
|
||||||
return False
|
return
|
||||||
mesh_writer.setStoreArchive(False)
|
mesh_writer.setStoreArchive(False)
|
||||||
return True
|
|
||||||
|
self._success = True
|
||||||
|
|
||||||
|
def write(self, stream, nodes, mode=WorkspaceWriter.OutputMode.BinaryMode):
|
||||||
|
self._success = False
|
||||||
|
self._export_model = None
|
||||||
|
self._stream = stream
|
||||||
|
self._nodes = nodes
|
||||||
|
self._mode = mode
|
||||||
|
self._config_dialog = None
|
||||||
|
|
||||||
|
self._main_thread_lock.acquire()
|
||||||
|
# Export is done in main thread because it may require a few asynchronous configuration steps
|
||||||
|
Application.getInstance().callLater(self._preWrite)
|
||||||
|
self._main_thread_lock.acquire() # Block until lock has been released, meaning the config+write is over
|
||||||
|
|
||||||
|
self._main_thread_lock.release()
|
||||||
|
|
||||||
|
self._export_model = None
|
||||||
|
self._stream = None
|
||||||
|
self._nodes = None
|
||||||
|
self._mode = None
|
||||||
|
self._config_dialog = None
|
||||||
|
|
||||||
|
return self._success
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _writePluginMetadataToArchive(archive: zipfile.ZipFile) -> None:
|
def _writePluginMetadataToArchive(archive: zipfile.ZipFile) -> None:
|
||||||
|
@ -166,3 +233,26 @@ class ThreeMFWorkspaceWriter(WorkspaceWriter):
|
||||||
except (FileNotFoundError, EnvironmentError):
|
except (FileNotFoundError, EnvironmentError):
|
||||||
Logger.error("File became inaccessible while writing to it: {archive_filename}".format(archive_filename = archive.fp.name))
|
Logger.error("File became inaccessible while writing to it: {archive_filename}".format(archive_filename = archive.fp.name))
|
||||||
return
|
return
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _getUserSettings(model: SettingsExportModel) -> Dict[str, Dict[str, Any]]:
|
||||||
|
user_settings = {}
|
||||||
|
|
||||||
|
for group in model.settingsGroups:
|
||||||
|
category = ''
|
||||||
|
if group.category == SettingsExportGroup.Category.Global:
|
||||||
|
category = 'global'
|
||||||
|
elif group.category == SettingsExportGroup.Category.Extruder:
|
||||||
|
category = f"extruder_{group.extruder_index}"
|
||||||
|
|
||||||
|
if len(category) > 0:
|
||||||
|
settings_values = {}
|
||||||
|
stack = group.stack
|
||||||
|
|
||||||
|
for setting in group.settings:
|
||||||
|
if setting.selected:
|
||||||
|
settings_values[setting.id] = stack.getProperty(setting.id, "value")
|
||||||
|
|
||||||
|
user_settings[category] = settings_values
|
||||||
|
|
||||||
|
return user_settings
|
|
@ -40,6 +40,9 @@ except ImportError:
|
||||||
import zipfile
|
import zipfile
|
||||||
import UM.Application
|
import UM.Application
|
||||||
|
|
||||||
|
from .SettingsExportModel import SettingsExportModel
|
||||||
|
from .SettingsExportGroup import SettingsExportGroup
|
||||||
|
|
||||||
from UM.i18n import i18nCatalog
|
from UM.i18n import i18nCatalog
|
||||||
catalog = i18nCatalog("cura")
|
catalog = i18nCatalog("cura")
|
||||||
|
|
||||||
|
@ -87,7 +90,9 @@ class ThreeMFWriter(MeshWriter):
|
||||||
self._store_archive = store_archive
|
self._store_archive = store_archive
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _convertUMNodeToSavitarNode(um_node, transformation=Matrix()):
|
def _convertUMNodeToSavitarNode(um_node,
|
||||||
|
transformation = Matrix(),
|
||||||
|
exported_settings: Optional[Dict[str, Set[str]]] = None):
|
||||||
"""Convenience function that converts an Uranium SceneNode object to a SavitarSceneNode
|
"""Convenience function that converts an Uranium SceneNode object to a SavitarSceneNode
|
||||||
|
|
||||||
:returns: Uranium Scene node.
|
:returns: Uranium Scene node.
|
||||||
|
@ -129,6 +134,7 @@ class ThreeMFWriter(MeshWriter):
|
||||||
if stack is not None:
|
if stack is not None:
|
||||||
changed_setting_keys = stack.getTop().getAllKeys()
|
changed_setting_keys = stack.getTop().getAllKeys()
|
||||||
|
|
||||||
|
if exported_settings is None:
|
||||||
# Ensure that we save the extruder used for this object in a multi-extrusion setup
|
# Ensure that we save the extruder used for this object in a multi-extrusion setup
|
||||||
if stack.getProperty("machine_extruder_count", "value") > 1:
|
if stack.getProperty("machine_extruder_count", "value") > 1:
|
||||||
changed_setting_keys.add("extruder_nr")
|
changed_setting_keys.add("extruder_nr")
|
||||||
|
@ -136,6 +142,14 @@ class ThreeMFWriter(MeshWriter):
|
||||||
# Get values for all changed settings & save them.
|
# Get values for all changed settings & save them.
|
||||||
for key in changed_setting_keys:
|
for key in changed_setting_keys:
|
||||||
savitar_node.setSetting("cura:" + key, str(stack.getProperty(key, "value")))
|
savitar_node.setSetting("cura:" + key, str(stack.getProperty(key, "value")))
|
||||||
|
else:
|
||||||
|
# We want to export only the specified settings
|
||||||
|
if um_node.getName() in exported_settings:
|
||||||
|
model_exported_settings = exported_settings[um_node.getName()]
|
||||||
|
|
||||||
|
# Get values for all exported settings & save them.
|
||||||
|
for key in model_exported_settings:
|
||||||
|
savitar_node.setSetting("cura:" + key, str(stack.getProperty(key, "value")))
|
||||||
|
|
||||||
# Store the metadata.
|
# Store the metadata.
|
||||||
for key, value in um_node.metadata.items():
|
for key, value in um_node.metadata.items():
|
||||||
|
@ -145,7 +159,8 @@ class ThreeMFWriter(MeshWriter):
|
||||||
# only save the nodes on the active build plate
|
# only save the nodes on the active build plate
|
||||||
if child_node.callDecoration("getBuildPlateNumber") != active_build_plate_nr:
|
if child_node.callDecoration("getBuildPlateNumber") != active_build_plate_nr:
|
||||||
continue
|
continue
|
||||||
savitar_child_node = ThreeMFWriter._convertUMNodeToSavitarNode(child_node)
|
savitar_child_node = ThreeMFWriter._convertUMNodeToSavitarNode(child_node,
|
||||||
|
exported_settings = exported_settings)
|
||||||
if savitar_child_node is not None:
|
if savitar_child_node is not None:
|
||||||
savitar_node.addChild(savitar_child_node)
|
savitar_node.addChild(savitar_child_node)
|
||||||
|
|
||||||
|
@ -154,7 +169,7 @@ class ThreeMFWriter(MeshWriter):
|
||||||
def getArchive(self):
|
def getArchive(self):
|
||||||
return self._archive
|
return self._archive
|
||||||
|
|
||||||
def write(self, stream, nodes, mode = MeshWriter.OutputMode.BinaryMode) -> bool:
|
def write(self, stream, nodes, mode = MeshWriter.OutputMode.BinaryMode, export_settings_model = None) -> bool:
|
||||||
self._archive = None # Reset archive
|
self._archive = None # Reset archive
|
||||||
archive = zipfile.ZipFile(stream, "w", compression = zipfile.ZIP_DEFLATED)
|
archive = zipfile.ZipFile(stream, "w", compression = zipfile.ZIP_DEFLATED)
|
||||||
try:
|
try:
|
||||||
|
@ -232,14 +247,19 @@ class ThreeMFWriter(MeshWriter):
|
||||||
transformation_matrix.preMultiply(translation_matrix)
|
transformation_matrix.preMultiply(translation_matrix)
|
||||||
|
|
||||||
root_node = UM.Application.Application.getInstance().getController().getScene().getRoot()
|
root_node = UM.Application.Application.getInstance().getController().getScene().getRoot()
|
||||||
|
exported_model_settings = ThreeMFWriter._extractModelExportedSettings(export_settings_model)
|
||||||
for node in nodes:
|
for node in nodes:
|
||||||
if node == root_node:
|
if node == root_node:
|
||||||
for root_child in node.getChildren():
|
for root_child in node.getChildren():
|
||||||
savitar_node = ThreeMFWriter._convertUMNodeToSavitarNode(root_child, transformation_matrix)
|
savitar_node = ThreeMFWriter._convertUMNodeToSavitarNode(root_child,
|
||||||
|
transformation_matrix,
|
||||||
|
exported_model_settings)
|
||||||
if savitar_node:
|
if savitar_node:
|
||||||
savitar_scene.addSceneNode(savitar_node)
|
savitar_scene.addSceneNode(savitar_node)
|
||||||
else:
|
else:
|
||||||
savitar_node = self._convertUMNodeToSavitarNode(node, transformation_matrix)
|
savitar_node = self._convertUMNodeToSavitarNode(node,
|
||||||
|
transformation_matrix,
|
||||||
|
exported_model_settings)
|
||||||
if savitar_node:
|
if savitar_node:
|
||||||
savitar_scene.addSceneNode(savitar_node)
|
savitar_scene.addSceneNode(savitar_node)
|
||||||
|
|
||||||
|
@ -395,3 +415,20 @@ class ThreeMFWriter(MeshWriter):
|
||||||
parser = Savitar.ThreeMFParser()
|
parser = Savitar.ThreeMFParser()
|
||||||
scene_string = parser.sceneToString(savitar_scene)
|
scene_string = parser.sceneToString(savitar_scene)
|
||||||
return scene_string
|
return scene_string
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _extractModelExportedSettings(model: Optional[SettingsExportModel]) -> Dict[str, Set[str]]:
|
||||||
|
extra_settings = {}
|
||||||
|
|
||||||
|
if model is not None:
|
||||||
|
for group in model.settingsGroups:
|
||||||
|
if group.category == SettingsExportGroup.Category.Model:
|
||||||
|
exported_model_settings = set()
|
||||||
|
|
||||||
|
for exported_setting in group.settings:
|
||||||
|
if exported_setting.selected:
|
||||||
|
exported_model_settings.add(exported_setting.id)
|
||||||
|
|
||||||
|
extra_settings[group.category_details] = exported_model_settings
|
||||||
|
|
||||||
|
return extra_settings
|
||||||
|
|
|
@ -2,9 +2,12 @@
|
||||||
# Uranium is released under the terms of the LGPLv3 or higher.
|
# Uranium is released under the terms of the LGPLv3 or higher.
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
|
from PyQt6.QtQml import qmlRegisterType
|
||||||
|
|
||||||
from UM.Logger import Logger
|
from UM.Logger import Logger
|
||||||
try:
|
try:
|
||||||
from . import ThreeMFWriter
|
from . import ThreeMFWriter
|
||||||
|
from .SettingsExportGroup import SettingsExportGroup
|
||||||
threemf_writer_was_imported = True
|
threemf_writer_was_imported = True
|
||||||
except ImportError:
|
except ImportError:
|
||||||
Logger.log("w", "Could not import ThreeMFWriter; libSavitar may be missing")
|
Logger.log("w", "Could not import ThreeMFWriter; libSavitar may be missing")
|
||||||
|
@ -23,20 +26,36 @@ def getMetaData():
|
||||||
|
|
||||||
if threemf_writer_was_imported:
|
if threemf_writer_was_imported:
|
||||||
metaData["mesh_writer"] = {
|
metaData["mesh_writer"] = {
|
||||||
"output": [{
|
"output": [
|
||||||
|
{
|
||||||
"extension": "3mf",
|
"extension": "3mf",
|
||||||
"description": i18n_catalog.i18nc("@item:inlistbox", "3MF file"),
|
"description": i18n_catalog.i18nc("@item:inlistbox", "3MF file"),
|
||||||
"mime_type": "application/vnd.ms-package.3dmanufacturing-3dmodel+xml",
|
"mime_type": "application/vnd.ms-package.3dmanufacturing-3dmodel+xml",
|
||||||
"mode": ThreeMFWriter.ThreeMFWriter.OutputMode.BinaryMode
|
"mode": ThreeMFWriter.ThreeMFWriter.OutputMode.BinaryMode
|
||||||
}]
|
},
|
||||||
|
{
|
||||||
|
"extension": "pcb",
|
||||||
|
"description": i18n_catalog.i18nc("@item:inlistbox", "PCB file"),
|
||||||
|
"mime_type": "application/x-pcb",
|
||||||
|
"mode": ThreeMFWriter.ThreeMFWriter.OutputMode.BinaryMode
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
metaData["workspace_writer"] = {
|
metaData["workspace_writer"] = {
|
||||||
"output": [{
|
"output": [
|
||||||
|
{
|
||||||
"extension": workspace_extension,
|
"extension": workspace_extension,
|
||||||
"description": i18n_catalog.i18nc("@item:inlistbox", "Cura Project 3MF file"),
|
"description": i18n_catalog.i18nc("@item:inlistbox", "Cura Project 3MF file"),
|
||||||
"mime_type": "application/vnd.ms-package.3dmanufacturing-3dmodel+xml",
|
"mime_type": "application/vnd.ms-package.3dmanufacturing-3dmodel+xml",
|
||||||
"mode": ThreeMFWorkspaceWriter.ThreeMFWorkspaceWriter.OutputMode.BinaryMode
|
"mode": ThreeMFWorkspaceWriter.ThreeMFWorkspaceWriter.OutputMode.BinaryMode
|
||||||
}]
|
},
|
||||||
|
{
|
||||||
|
"extension": "pcb",
|
||||||
|
"description": i18n_catalog.i18nc("@item:inlistbox", "Pre-Configured Batch file"),
|
||||||
|
"mime_type": "application/x-pcb",
|
||||||
|
"mode": ThreeMFWorkspaceWriter.ThreeMFWorkspaceWriter.OutputMode.BinaryMode
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
return metaData
|
return metaData
|
||||||
|
@ -44,6 +63,8 @@ def getMetaData():
|
||||||
|
|
||||||
def register(app):
|
def register(app):
|
||||||
if "3MFWriter.ThreeMFWriter" in sys.modules:
|
if "3MFWriter.ThreeMFWriter" in sys.modules:
|
||||||
|
qmlRegisterType(SettingsExportGroup, "ThreeMFWriter", 1, 0, "SettingsExportGroup")
|
||||||
|
|
||||||
return {"mesh_writer": ThreeMFWriter.ThreeMFWriter(),
|
return {"mesh_writer": ThreeMFWriter.ThreeMFWriter(),
|
||||||
"workspace_writer": ThreeMFWorkspaceWriter.ThreeMFWorkspaceWriter()}
|
"workspace_writer": ThreeMFWorkspaceWriter.ThreeMFWorkspaceWriter()}
|
||||||
else:
|
else:
|
||||||
|
|
|
@ -1,460 +0,0 @@
|
||||||
# Copyright (c) 2024 Ultimaker B.V.
|
|
||||||
# Cura is released under the terms of the LGPLv3 or higher.
|
|
||||||
import json
|
|
||||||
import zipfile
|
|
||||||
import datetime
|
|
||||||
import numpy
|
|
||||||
import re
|
|
||||||
from dataclasses import asdict
|
|
||||||
from typing import Optional, cast, List, Dict, Pattern, Set, Union, Mapping, Any
|
|
||||||
from threading import Lock
|
|
||||||
from io import StringIO # For converting g-code to bytes.
|
|
||||||
|
|
||||||
import pySavitar as Savitar
|
|
||||||
|
|
||||||
from PyQt6.QtCore import QBuffer
|
|
||||||
|
|
||||||
from UM.Mesh.MeshWriter import MeshWriter
|
|
||||||
from UM.Logger import Logger
|
|
||||||
from UM.Scene.SceneNode import SceneNode
|
|
||||||
from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
|
|
||||||
from UM.i18n import i18nCatalog
|
|
||||||
from UM.Settings.InstanceContainer import InstanceContainer
|
|
||||||
from UM.Settings.SettingFunction import SettingFunction
|
|
||||||
from UM.Settings.ContainerRegistry import ContainerRegistry
|
|
||||||
from UM.Math.Matrix import Matrix
|
|
||||||
from UM.Math.Vector import Vector
|
|
||||||
|
|
||||||
from cura.CuraApplication import CuraApplication
|
|
||||||
from cura.CuraPackageManager import CuraPackageManager
|
|
||||||
from cura.Settings import CuraContainerStack
|
|
||||||
from cura.Settings.GlobalStack import GlobalStack
|
|
||||||
from cura.Utils.Threading import call_on_qt_thread
|
|
||||||
from cura.Snapshot import Snapshot
|
|
||||||
|
|
||||||
from .PCBDialog import PCBDialog
|
|
||||||
from .SettingsExportModel import SettingsExportModel
|
|
||||||
from .SettingsExportGroup import SettingsExportGroup
|
|
||||||
|
|
||||||
MYPY = False
|
|
||||||
try:
|
|
||||||
if not MYPY:
|
|
||||||
import xml.etree.cElementTree as ET
|
|
||||||
except ImportError:
|
|
||||||
Logger.log("w", "Unable to load cElementTree, switching to slower version")
|
|
||||||
import xml.etree.ElementTree as ET
|
|
||||||
|
|
||||||
catalog = i18nCatalog("cura")
|
|
||||||
|
|
||||||
THUMBNAIL_PATH = "Metadata/thumbnail.png"
|
|
||||||
MODEL_PATH = "3D/3dmodel.model"
|
|
||||||
PACKAGE_METADATA_PATH = "Cura/packages.json"
|
|
||||||
USER_SETTINGS_PATH = "Cura/user-settings.json"
|
|
||||||
|
|
||||||
class PCBWriter(MeshWriter):
|
|
||||||
def __init__(self):
|
|
||||||
super().__init__()
|
|
||||||
self._namespaces = {
|
|
||||||
"3mf": "http://schemas.microsoft.com/3dmanufacturing/core/2015/02",
|
|
||||||
"content-types": "http://schemas.openxmlformats.org/package/2006/content-types",
|
|
||||||
"relationships": "http://schemas.openxmlformats.org/package/2006/relationships",
|
|
||||||
"cura": "http://software.ultimaker.com/xml/cura/3mf/2015/10"
|
|
||||||
}
|
|
||||||
|
|
||||||
self._config_dialog = None
|
|
||||||
self._main_thread_lock = Lock()
|
|
||||||
self._success = False
|
|
||||||
self._export_model = None
|
|
||||||
|
|
||||||
def write(self, stream, nodes, mode = MeshWriter.OutputMode.BinaryMode) -> bool:
|
|
||||||
self._success = False
|
|
||||||
self._export_model = None
|
|
||||||
|
|
||||||
self._main_thread_lock.acquire()
|
|
||||||
# Start configuration window in main application thread
|
|
||||||
CuraApplication.getInstance().callLater(self._write, stream, nodes, mode)
|
|
||||||
self._main_thread_lock.acquire() # Block until lock has been released, meaning the config is over
|
|
||||||
|
|
||||||
self._main_thread_lock.release()
|
|
||||||
|
|
||||||
if self._export_model is not None:
|
|
||||||
archive = zipfile.ZipFile(stream, "w", compression=zipfile.ZIP_DEFLATED)
|
|
||||||
try:
|
|
||||||
model_file = zipfile.ZipInfo(MODEL_PATH)
|
|
||||||
# Because zipfile is stupid and ignores archive-level compression settings when writing with ZipInfo.
|
|
||||||
model_file.compress_type = zipfile.ZIP_DEFLATED
|
|
||||||
|
|
||||||
# Create content types file
|
|
||||||
content_types_file = zipfile.ZipInfo("[Content_Types].xml")
|
|
||||||
content_types_file.compress_type = zipfile.ZIP_DEFLATED
|
|
||||||
content_types = ET.Element("Types", xmlns=self._namespaces["content-types"])
|
|
||||||
rels_type = ET.SubElement(content_types, "Default", Extension="rels",
|
|
||||||
ContentType="application/vnd.openxmlformats-package.relationships+xml")
|
|
||||||
model_type = ET.SubElement(content_types, "Default", Extension="model",
|
|
||||||
ContentType="application/vnd.ms-package.3dmanufacturing-3dmodel+xml")
|
|
||||||
|
|
||||||
# Create _rels/.rels file
|
|
||||||
relations_file = zipfile.ZipInfo("_rels/.rels")
|
|
||||||
relations_file.compress_type = zipfile.ZIP_DEFLATED
|
|
||||||
relations_element = ET.Element("Relationships", xmlns=self._namespaces["relationships"])
|
|
||||||
model_relation_element = ET.SubElement(relations_element, "Relationship", Target="/" + MODEL_PATH,
|
|
||||||
Id="rel0",
|
|
||||||
Type="http://schemas.microsoft.com/3dmanufacturing/2013/01/3dmodel")
|
|
||||||
|
|
||||||
# Attempt to add a thumbnail
|
|
||||||
snapshot = self._createSnapshot()
|
|
||||||
if snapshot:
|
|
||||||
thumbnail_buffer = QBuffer()
|
|
||||||
thumbnail_buffer.open(QBuffer.OpenModeFlag.ReadWrite)
|
|
||||||
snapshot.save(thumbnail_buffer, "PNG")
|
|
||||||
|
|
||||||
thumbnail_file = zipfile.ZipInfo(THUMBNAIL_PATH)
|
|
||||||
# Don't try to compress snapshot file, because the PNG is pretty much as compact as it will get
|
|
||||||
archive.writestr(thumbnail_file, thumbnail_buffer.data())
|
|
||||||
|
|
||||||
# Add PNG to content types file
|
|
||||||
thumbnail_type = ET.SubElement(content_types, "Default", Extension="png", ContentType="image/png")
|
|
||||||
# Add thumbnail relation to _rels/.rels file
|
|
||||||
thumbnail_relation_element = ET.SubElement(relations_element, "Relationship",
|
|
||||||
Target="/" + THUMBNAIL_PATH, Id="rel1",
|
|
||||||
Type="http://schemas.openxmlformats.org/package/2006/relationships/metadata/thumbnail")
|
|
||||||
|
|
||||||
# Write material metadata
|
|
||||||
packages_metadata = self._getMaterialPackageMetadata() + self._getPluginPackageMetadata()
|
|
||||||
self._storeMetadataJson({"packages": packages_metadata}, archive, PACKAGE_METADATA_PATH)
|
|
||||||
|
|
||||||
# Write user settings data
|
|
||||||
user_settings_data = self._getUserSettings(self._export_model)
|
|
||||||
self._storeMetadataJson(user_settings_data, archive, USER_SETTINGS_PATH)
|
|
||||||
|
|
||||||
savitar_scene = Savitar.Scene()
|
|
||||||
|
|
||||||
scene_metadata = CuraApplication.getInstance().getController().getScene().getMetaData()
|
|
||||||
|
|
||||||
for key, value in scene_metadata.items():
|
|
||||||
savitar_scene.setMetaDataEntry(key, value)
|
|
||||||
|
|
||||||
current_time_string = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
||||||
if "Application" not in scene_metadata:
|
|
||||||
# This might sound a bit strange, but this field should store the original application that created
|
|
||||||
# the 3mf. So if it was already set, leave it to whatever it was.
|
|
||||||
savitar_scene.setMetaDataEntry("Application",
|
|
||||||
CuraApplication.getInstance().getApplicationDisplayName())
|
|
||||||
if "CreationDate" not in scene_metadata:
|
|
||||||
savitar_scene.setMetaDataEntry("CreationDate", current_time_string)
|
|
||||||
|
|
||||||
savitar_scene.setMetaDataEntry("ModificationDate", current_time_string)
|
|
||||||
|
|
||||||
transformation_matrix = Matrix()
|
|
||||||
transformation_matrix._data[1, 1] = 0
|
|
||||||
transformation_matrix._data[1, 2] = -1
|
|
||||||
transformation_matrix._data[2, 1] = 1
|
|
||||||
transformation_matrix._data[2, 2] = 0
|
|
||||||
|
|
||||||
global_container_stack = CuraApplication.getInstance().getGlobalContainerStack()
|
|
||||||
# Second step: 3MF defines the left corner of the machine as center, whereas cura uses the center of the
|
|
||||||
# build volume.
|
|
||||||
if global_container_stack:
|
|
||||||
translation_vector = Vector(x=global_container_stack.getProperty("machine_width", "value") / 2,
|
|
||||||
y=global_container_stack.getProperty("machine_depth", "value") / 2,
|
|
||||||
z=0)
|
|
||||||
translation_matrix = Matrix()
|
|
||||||
translation_matrix.setByTranslation(translation_vector)
|
|
||||||
transformation_matrix.preMultiply(translation_matrix)
|
|
||||||
|
|
||||||
root_node = CuraApplication.getInstance().getController().getScene().getRoot()
|
|
||||||
exported_model_settings = PCBWriter._extractModelExportedSettings(self._export_model)
|
|
||||||
for node in nodes:
|
|
||||||
if node == root_node:
|
|
||||||
for root_child in node.getChildren():
|
|
||||||
savitar_node = PCBWriter._convertUMNodeToSavitarNode(root_child,
|
|
||||||
transformation_matrix,
|
|
||||||
exported_model_settings)
|
|
||||||
if savitar_node:
|
|
||||||
savitar_scene.addSceneNode(savitar_node)
|
|
||||||
else:
|
|
||||||
savitar_node = self._convertUMNodeToSavitarNode(node,
|
|
||||||
transformation_matrix,
|
|
||||||
exported_model_settings)
|
|
||||||
if savitar_node:
|
|
||||||
savitar_scene.addSceneNode(savitar_node)
|
|
||||||
|
|
||||||
parser = Savitar.ThreeMFParser()
|
|
||||||
scene_string = parser.sceneToString(savitar_scene)
|
|
||||||
|
|
||||||
archive.writestr(model_file, scene_string)
|
|
||||||
archive.writestr(content_types_file,
|
|
||||||
b'<?xml version="1.0" encoding="UTF-8"?> \n' + ET.tostring(content_types))
|
|
||||||
archive.writestr(relations_file,
|
|
||||||
b'<?xml version="1.0" encoding="UTF-8"?> \n' + ET.tostring(relations_element))
|
|
||||||
except Exception as error:
|
|
||||||
Logger.logException("e", "Error writing zip file")
|
|
||||||
self.setInformation(str(error))
|
|
||||||
return False
|
|
||||||
finally:
|
|
||||||
archive.close()
|
|
||||||
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
return False
|
|
||||||
|
|
||||||
def _write(self, stream, nodes, mode):
|
|
||||||
self._config_dialog = PCBDialog()
|
|
||||||
self._config_dialog.finished.connect(self._onDialogFinished)
|
|
||||||
self._config_dialog.show()
|
|
||||||
|
|
||||||
def _onDialogFinished(self, accepted: bool):
|
|
||||||
if accepted:
|
|
||||||
self._export_model = self._config_dialog.getModel()
|
|
||||||
|
|
||||||
self._main_thread_lock.release()
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _extractModelExportedSettings(model: SettingsExportModel) -> Mapping[str, Set[str]]:
|
|
||||||
extra_settings = {}
|
|
||||||
|
|
||||||
for group in model.settingsGroups:
|
|
||||||
if group.category == SettingsExportGroup.Category.Model:
|
|
||||||
exported_model_settings = set()
|
|
||||||
|
|
||||||
for exported_setting in group.settings:
|
|
||||||
if exported_setting.selected:
|
|
||||||
exported_model_settings.add(exported_setting.id)
|
|
||||||
|
|
||||||
extra_settings[group.category_details] = exported_model_settings
|
|
||||||
|
|
||||||
return extra_settings
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _convertUMNodeToSavitarNode(um_node,
|
|
||||||
transformation: Matrix = Matrix(),
|
|
||||||
exported_settings: Mapping[str, Set[str]] = None):
|
|
||||||
"""Convenience function that converts an Uranium SceneNode object to a SavitarSceneNode
|
|
||||||
|
|
||||||
:returns: Uranium Scene node.
|
|
||||||
"""
|
|
||||||
if not isinstance(um_node, SceneNode):
|
|
||||||
return None
|
|
||||||
|
|
||||||
active_build_plate_nr = CuraApplication.getInstance().getMultiBuildPlateModel().activeBuildPlate
|
|
||||||
if um_node.callDecoration("getBuildPlateNumber") != active_build_plate_nr:
|
|
||||||
return
|
|
||||||
|
|
||||||
savitar_node = Savitar.SceneNode()
|
|
||||||
savitar_node.setName(um_node.getName())
|
|
||||||
|
|
||||||
node_matrix = Matrix()
|
|
||||||
mesh_data = um_node.getMeshData()
|
|
||||||
# compensate for original center position, if object(s) is/are not around its zero position
|
|
||||||
if mesh_data is not None:
|
|
||||||
extents = mesh_data.getExtents()
|
|
||||||
if extents is not None:
|
|
||||||
# We use a different coordinate space while writing, so flip Z and Y
|
|
||||||
center_vector = Vector(extents.center.x, extents.center.z, extents.center.y)
|
|
||||||
node_matrix.setByTranslation(center_vector)
|
|
||||||
node_matrix.multiply(um_node.getLocalTransformation())
|
|
||||||
|
|
||||||
matrix_string = PCBWriter._convertMatrixToString(node_matrix.preMultiply(transformation))
|
|
||||||
|
|
||||||
savitar_node.setTransformation(matrix_string)
|
|
||||||
if mesh_data is not None:
|
|
||||||
savitar_node.getMeshData().setVerticesFromBytes(mesh_data.getVerticesAsByteArray())
|
|
||||||
indices_array = mesh_data.getIndicesAsByteArray()
|
|
||||||
if indices_array is not None:
|
|
||||||
savitar_node.getMeshData().setFacesFromBytes(indices_array)
|
|
||||||
else:
|
|
||||||
savitar_node.getMeshData().setFacesFromBytes(
|
|
||||||
numpy.arange(mesh_data.getVertices().size / 3, dtype=numpy.int32).tostring())
|
|
||||||
|
|
||||||
# Handle per object settings (if any)
|
|
||||||
stack = um_node.callDecoration("getStack")
|
|
||||||
if stack is not None:
|
|
||||||
if um_node.getName() in exported_settings:
|
|
||||||
model_exported_settings = exported_settings[um_node.getName()]
|
|
||||||
|
|
||||||
# Get values for all exported settings & save them.
|
|
||||||
for key in model_exported_settings:
|
|
||||||
savitar_node.setSetting("cura:" + key, str(stack.getProperty(key, "value")))
|
|
||||||
|
|
||||||
# Store the metadata.
|
|
||||||
for key, value in um_node.metadata.items():
|
|
||||||
savitar_node.setSetting(key, value)
|
|
||||||
|
|
||||||
for child_node in um_node.getChildren():
|
|
||||||
# only save the nodes on the active build plate
|
|
||||||
if child_node.callDecoration("getBuildPlateNumber") != active_build_plate_nr:
|
|
||||||
continue
|
|
||||||
savitar_child_node = PCBWriter._convertUMNodeToSavitarNode(child_node,
|
|
||||||
exported_settings = exported_settings)
|
|
||||||
if savitar_child_node is not None:
|
|
||||||
savitar_node.addChild(savitar_child_node)
|
|
||||||
|
|
||||||
return savitar_node
|
|
||||||
|
|
||||||
@call_on_qt_thread # must be called from the main thread because of OpenGL
|
|
||||||
def _createSnapshot(self):
|
|
||||||
Logger.log("d", "Creating thumbnail image...")
|
|
||||||
if not CuraApplication.getInstance().isVisible:
|
|
||||||
Logger.log("w", "Can't create snapshot when renderer not initialized.")
|
|
||||||
return None
|
|
||||||
try:
|
|
||||||
snapshot = Snapshot.snapshot(width=300, height=300)
|
|
||||||
except:
|
|
||||||
Logger.logException("w", "Failed to create snapshot image")
|
|
||||||
return None
|
|
||||||
|
|
||||||
return snapshot
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _storeMetadataJson(metadata: Union[Dict[str, List[Dict[str, str]]], Dict[str, Dict[str, Any]]],
|
|
||||||
archive: zipfile.ZipFile, path
|
|
||||||
: str) -> None:
|
|
||||||
"""Stores metadata inside archive path as json file"""
|
|
||||||
metadata_file = zipfile.ZipInfo(path)
|
|
||||||
# We have to set the compress type of each file as well (it doesn't keep the type of the entire archive)
|
|
||||||
metadata_file.compress_type = zipfile.ZIP_DEFLATED
|
|
||||||
archive.writestr(metadata_file,
|
|
||||||
json.dumps(metadata, separators=(", ", ": "), indent=4, skipkeys=True, ensure_ascii=False))
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _getUserSettings(model: SettingsExportModel) -> Dict[str, Dict[str, Any]]:
|
|
||||||
user_settings = {}
|
|
||||||
|
|
||||||
for group in model.settingsGroups:
|
|
||||||
category = ''
|
|
||||||
if group.category == SettingsExportGroup.Category.Global:
|
|
||||||
category = 'global'
|
|
||||||
elif group.category == SettingsExportGroup.Category.Extruder:
|
|
||||||
category = f"extruder_{group.extruder_index}"
|
|
||||||
|
|
||||||
if len(category) > 0:
|
|
||||||
settings_values = {}
|
|
||||||
stack = group.stack
|
|
||||||
|
|
||||||
for setting in group.settings:
|
|
||||||
if setting.selected:
|
|
||||||
settings_values[setting.id] = stack.getProperty(setting.id, "value")
|
|
||||||
|
|
||||||
user_settings[category] = settings_values
|
|
||||||
|
|
||||||
return user_settings
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _getPluginPackageMetadata() -> List[Dict[str, str]]:
|
|
||||||
"""Get metadata for all backend plugins that are used in the project.
|
|
||||||
|
|
||||||
:return: List of material metadata dictionaries.
|
|
||||||
"""
|
|
||||||
|
|
||||||
backend_plugin_enum_value_regex = re.compile(
|
|
||||||
r"PLUGIN::(?P<plugin_id>\w+)@(?P<version>\d+.\d+.\d+)::(?P<value>\w+)")
|
|
||||||
# This regex parses enum values to find if they contain custom
|
|
||||||
# backend engine values. These custom enum values are in the format
|
|
||||||
# PLUGIN::<plugin_id>@<version>::<value>
|
|
||||||
# where
|
|
||||||
# - plugin_id is the id of the plugin
|
|
||||||
# - version is in the semver format
|
|
||||||
# - value is the value of the enum
|
|
||||||
|
|
||||||
plugin_ids = set()
|
|
||||||
|
|
||||||
def addPluginIdsInStack(stack: CuraContainerStack) -> None:
|
|
||||||
for key in stack.getAllKeys():
|
|
||||||
value = str(stack.getProperty(key, "value"))
|
|
||||||
for plugin_id, _version, _value in backend_plugin_enum_value_regex.findall(value):
|
|
||||||
plugin_ids.add(plugin_id)
|
|
||||||
|
|
||||||
# Go through all stacks and find all the plugin id contained in the project
|
|
||||||
global_stack = CuraApplication.getInstance().getMachineManager().activeMachine
|
|
||||||
addPluginIdsInStack(global_stack)
|
|
||||||
|
|
||||||
for container in global_stack.getContainers():
|
|
||||||
addPluginIdsInStack(container)
|
|
||||||
|
|
||||||
for extruder_stack in global_stack.extruderList:
|
|
||||||
addPluginIdsInStack(extruder_stack)
|
|
||||||
|
|
||||||
for container in extruder_stack.getContainers():
|
|
||||||
addPluginIdsInStack(container)
|
|
||||||
|
|
||||||
metadata = {}
|
|
||||||
|
|
||||||
package_manager = cast(CuraPackageManager, CuraApplication.getInstance().getPackageManager())
|
|
||||||
for plugin_id in plugin_ids:
|
|
||||||
package_data = package_manager.getInstalledPackageInfo(plugin_id)
|
|
||||||
|
|
||||||
metadata[plugin_id] = {
|
|
||||||
"id": plugin_id,
|
|
||||||
"display_name": package_data.get("display_name") if package_data.get("display_name") else "",
|
|
||||||
"package_version": package_data.get("package_version") if package_data.get("package_version") else "",
|
|
||||||
"sdk_version_semver": package_data.get("sdk_version_semver") if package_data.get(
|
|
||||||
"sdk_version_semver") else "",
|
|
||||||
"type": "plugin",
|
|
||||||
}
|
|
||||||
|
|
||||||
# Storing in a dict and fetching values to avoid duplicates
|
|
||||||
return list(metadata.values())
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _getMaterialPackageMetadata() -> List[Dict[str, str]]:
|
|
||||||
"""Get metadata for installed materials in active extruder stack, this does not include bundled materials.
|
|
||||||
|
|
||||||
:return: List of material metadata dictionaries.
|
|
||||||
"""
|
|
||||||
metadata = {}
|
|
||||||
|
|
||||||
package_manager = cast(CuraPackageManager, CuraApplication.getInstance().getPackageManager())
|
|
||||||
|
|
||||||
for extruder in CuraApplication.getInstance().getExtruderManager().getActiveExtruderStacks():
|
|
||||||
if not extruder.isEnabled:
|
|
||||||
# Don't export materials not in use
|
|
||||||
continue
|
|
||||||
|
|
||||||
if isinstance(extruder.material, type(ContainerRegistry.getInstance().getEmptyInstanceContainer())):
|
|
||||||
# This is an empty material container, no material to export
|
|
||||||
continue
|
|
||||||
|
|
||||||
if package_manager.isMaterialBundled(extruder.material.getFileName(),
|
|
||||||
extruder.material.getMetaDataEntry("GUID")):
|
|
||||||
# Don't export bundled materials
|
|
||||||
continue
|
|
||||||
|
|
||||||
package_id = package_manager.getMaterialFilePackageId(extruder.material.getFileName(),
|
|
||||||
extruder.material.getMetaDataEntry("GUID"))
|
|
||||||
package_data = package_manager.getInstalledPackageInfo(package_id)
|
|
||||||
|
|
||||||
# We failed to find the package for this material
|
|
||||||
if not package_data:
|
|
||||||
Logger.info(f"Could not find package for material in extruder {extruder.id}, skipping.")
|
|
||||||
continue
|
|
||||||
|
|
||||||
material_metadata = {
|
|
||||||
"id": package_id,
|
|
||||||
"display_name": package_data.get("display_name") if package_data.get("display_name") else "",
|
|
||||||
"package_version": package_data.get("package_version") if package_data.get("package_version") else "",
|
|
||||||
"sdk_version_semver": package_data.get("sdk_version_semver") if package_data.get(
|
|
||||||
"sdk_version_semver") else "",
|
|
||||||
"type": "material",
|
|
||||||
}
|
|
||||||
|
|
||||||
metadata[package_id] = material_metadata
|
|
||||||
|
|
||||||
# Storing in a dict and fetching values to avoid duplicates
|
|
||||||
return list(metadata.values())
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _convertMatrixToString(matrix):
|
|
||||||
result = ""
|
|
||||||
result += str(matrix._data[0, 0]) + " "
|
|
||||||
result += str(matrix._data[1, 0]) + " "
|
|
||||||
result += str(matrix._data[2, 0]) + " "
|
|
||||||
result += str(matrix._data[0, 1]) + " "
|
|
||||||
result += str(matrix._data[1, 1]) + " "
|
|
||||||
result += str(matrix._data[2, 1]) + " "
|
|
||||||
result += str(matrix._data[0, 2]) + " "
|
|
||||||
result += str(matrix._data[1, 2]) + " "
|
|
||||||
result += str(matrix._data[2, 2]) + " "
|
|
||||||
result += str(matrix._data[0, 3]) + " "
|
|
||||||
result += str(matrix._data[1, 3]) + " "
|
|
||||||
result += str(matrix._data[2, 3])
|
|
||||||
return result
|
|
|
@ -1,29 +0,0 @@
|
||||||
# Copyright (c) 2024 Ultimaker B.V.
|
|
||||||
# Cura is released under the terms of the LGPLv3 or higher.
|
|
||||||
import sys
|
|
||||||
|
|
||||||
from PyQt6.QtQml import qmlRegisterType
|
|
||||||
|
|
||||||
from UM.i18n import i18nCatalog
|
|
||||||
|
|
||||||
from . import PCBWriter
|
|
||||||
from .SettingsExportModel import SettingsExportModel
|
|
||||||
from .SettingsExportGroup import SettingsExportGroup
|
|
||||||
|
|
||||||
i18n_catalog = i18nCatalog("cura")
|
|
||||||
|
|
||||||
def getMetaData():
|
|
||||||
return {"mesh_writer": {
|
|
||||||
"output": [{
|
|
||||||
"extension": "pcb",
|
|
||||||
"description": i18n_catalog.i18nc("@item:inlistbox", "Pre-Configured Batch file"),
|
|
||||||
"mime_type": "application/x-pcb",
|
|
||||||
"mode": PCBWriter.PCBWriter.OutputMode.BinaryMode
|
|
||||||
}]
|
|
||||||
}}
|
|
||||||
|
|
||||||
def register(app):
|
|
||||||
qmlRegisterType(SettingsExportModel, "PCBWriter", 1, 0, "SettingsExportModel")
|
|
||||||
qmlRegisterType(SettingsExportGroup, "PCBWriter", 1, 0, "SettingsExportGroup")
|
|
||||||
|
|
||||||
return {"mesh_writer": PCBWriter.PCBWriter() }
|
|
|
@ -1,8 +0,0 @@
|
||||||
{
|
|
||||||
"name": "Pre-Configured Batch Writer",
|
|
||||||
"author": "Ultimaker B.V.",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"description": "Provides support for writing Pre-Configured Batch files.",
|
|
||||||
"api": 8,
|
|
||||||
"i18n-catalog": "cura"
|
|
||||||
}
|
|
|
@ -70,6 +70,18 @@ Cura.Menu
|
||||||
enabled: UM.WorkspaceFileHandler.enabled
|
enabled: UM.WorkspaceFileHandler.enabled
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Cura.MenuItem
|
||||||
|
{
|
||||||
|
id: savePCBMenu
|
||||||
|
text: catalog.i18nc("@title:menu menubar:file", "&Save PCB Project...")
|
||||||
|
enabled: UM.WorkspaceFileHandler.enabled
|
||||||
|
onTriggered:
|
||||||
|
{
|
||||||
|
var args = { "filter_by_machine": false, "file_type": "workspace", "preferred_mimetypes": "application/x-pcb" };
|
||||||
|
UM.OutputDeviceManager.requestWriteToDevice("local_file", PrintInformation.jobName, args)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Cura.MenuSeparator { }
|
Cura.MenuSeparator { }
|
||||||
|
|
||||||
UM.MeshWritersModel { id: meshWritersModel }
|
UM.MeshWritersModel { id: meshWritersModel }
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue