mirror of
https://github.com/Ultimaker/Cura.git
synced 2025-07-07 06:57:28 -06:00
Split-out bambu-specific elements to their own 3MF 'variant'.
part of CURA-12099
This commit is contained in:
parent
17ab7a4890
commit
254087cb45
4 changed files with 308 additions and 134 deletions
168
plugins/3MFWriter/BambuLabVariant.py
Normal file
168
plugins/3MFWriter/BambuLabVariant.py
Normal file
|
@ -0,0 +1,168 @@
|
||||||
|
# Copyright (c) 2025 UltiMaker
|
||||||
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
from io import StringIO
|
||||||
|
import xml.etree.ElementTree as ET
|
||||||
|
import zipfile
|
||||||
|
|
||||||
|
from PyQt6.QtCore import Qt, QBuffer
|
||||||
|
from PyQt6.QtGui import QImage
|
||||||
|
|
||||||
|
from UM.Application import Application
|
||||||
|
from UM.Logger import Logger
|
||||||
|
from UM.Mesh.MeshWriter import MeshWriter
|
||||||
|
from UM.PluginRegistry import PluginRegistry
|
||||||
|
from typing import cast
|
||||||
|
|
||||||
|
from cura.CuraApplication import CuraApplication
|
||||||
|
|
||||||
|
from .ThreeMFVariant import ThreeMFVariant
|
||||||
|
from UM.i18n import i18nCatalog
|
||||||
|
catalog = i18nCatalog("cura")
|
||||||
|
|
||||||
|
# Path constants
|
||||||
|
METADATA_PATH = "Metadata"
|
||||||
|
THUMBNAIL_PATH_MULTIPLATE = f"{METADATA_PATH}/plate_1.png"
|
||||||
|
THUMBNAIL_PATH_MULTIPLATE_SMALL = f"{METADATA_PATH}/plate_1_small.png"
|
||||||
|
GCODE_PATH = f"{METADATA_PATH}/plate_1.gcode"
|
||||||
|
GCODE_MD5_PATH = f"{GCODE_PATH}.md5"
|
||||||
|
MODEL_SETTINGS_PATH = f"{METADATA_PATH}/model_settings.config"
|
||||||
|
PLATE_DESC_PATH = f"{METADATA_PATH}/plate_1.json"
|
||||||
|
SLICE_INFO_PATH = f"{METADATA_PATH}/slice_info.config"
|
||||||
|
|
||||||
|
class BambuLabVariant(ThreeMFVariant):
|
||||||
|
"""BambuLab specific implementation of the 3MF format."""
|
||||||
|
|
||||||
|
@property
|
||||||
|
def mime_type(self) -> str:
|
||||||
|
return "application/vnd.bambulab-package.3dmanufacturing-3dmodel+xml"
|
||||||
|
|
||||||
|
def process_thumbnail(self, snapshot: QImage, thumbnail_buffer: QBuffer,
|
||||||
|
archive: zipfile.ZipFile, relations_element: ET.Element) -> None:
|
||||||
|
"""Process the thumbnail for BambuLab variant."""
|
||||||
|
# Write thumbnail
|
||||||
|
archive.writestr(zipfile.ZipInfo(THUMBNAIL_PATH_MULTIPLATE), thumbnail_buffer.data())
|
||||||
|
|
||||||
|
# Add relations elements for thumbnails
|
||||||
|
ET.SubElement(relations_element, "Relationship",
|
||||||
|
Target="/" + THUMBNAIL_PATH_MULTIPLATE, Id="rel-2",
|
||||||
|
Type="http://schemas.openxmlformats.org/package/2006/relationships/metadata/thumbnail")
|
||||||
|
|
||||||
|
ET.SubElement(relations_element, "Relationship",
|
||||||
|
Target="/" + THUMBNAIL_PATH_MULTIPLATE, Id="rel-4",
|
||||||
|
Type="http://schemas.bambulab.com/package/2021/cover-thumbnail-middle")
|
||||||
|
|
||||||
|
# Create and save small thumbnail
|
||||||
|
small_snapshot = snapshot.scaled(128, 128, transformMode=Qt.TransformationMode.SmoothTransformation)
|
||||||
|
small_thumbnail_buffer = QBuffer()
|
||||||
|
small_thumbnail_buffer.open(QBuffer.OpenModeFlag.ReadWrite)
|
||||||
|
small_snapshot.save(small_thumbnail_buffer, "PNG")
|
||||||
|
|
||||||
|
# Write small thumbnail
|
||||||
|
archive.writestr(zipfile.ZipInfo(THUMBNAIL_PATH_MULTIPLATE_SMALL), small_thumbnail_buffer.data())
|
||||||
|
|
||||||
|
# Add relation for small thumbnail
|
||||||
|
ET.SubElement(relations_element, "Relationship",
|
||||||
|
Target="/" + THUMBNAIL_PATH_MULTIPLATE_SMALL, Id="rel-5",
|
||||||
|
Type="http://schemas.bambulab.com/package/2021/cover-thumbnail-small")
|
||||||
|
|
||||||
|
def add_extra_files(self, archive: zipfile.ZipFile, metadata_relations_element: ET.Element) -> None:
|
||||||
|
"""Add BambuLab specific files to the archive."""
|
||||||
|
self._storeGCode(archive, metadata_relations_element)
|
||||||
|
self._storeModelSettings(archive)
|
||||||
|
self._storePlateDesc(archive)
|
||||||
|
self._storeSliceInfo(archive)
|
||||||
|
|
||||||
|
def _storeGCode(self, archive: zipfile.ZipFile, metadata_relations_element: ET.Element):
|
||||||
|
"""Store GCode data in the archive."""
|
||||||
|
gcode_textio = StringIO()
|
||||||
|
gcode_writer = cast(MeshWriter, PluginRegistry.getInstance().getPluginObject("GCodeWriter"))
|
||||||
|
success = gcode_writer.write(gcode_textio, None)
|
||||||
|
|
||||||
|
if not success:
|
||||||
|
error_msg = catalog.i18nc("@info:error", "Can't write GCode to 3MF file")
|
||||||
|
self._writer.setInformation(error_msg)
|
||||||
|
Logger.error(error_msg)
|
||||||
|
raise Exception(error_msg)
|
||||||
|
|
||||||
|
gcode_data = gcode_textio.getvalue().encode("UTF-8")
|
||||||
|
archive.writestr(zipfile.ZipInfo(GCODE_PATH), gcode_data)
|
||||||
|
|
||||||
|
gcode_relation_element = ET.SubElement(metadata_relations_element, "Relationship",
|
||||||
|
Target=f"/{GCODE_PATH}", Id="rel-1",
|
||||||
|
Type="http://schemas.bambulab.com/package/2021/gcode")
|
||||||
|
|
||||||
|
# Calculate and store the MD5 sum of the gcode data
|
||||||
|
md5_hash = hashlib.md5(gcode_data).hexdigest()
|
||||||
|
archive.writestr(zipfile.ZipInfo(GCODE_MD5_PATH), md5_hash.encode("UTF-8"))
|
||||||
|
|
||||||
|
def _storeModelSettings(self, archive: zipfile.ZipFile):
|
||||||
|
"""Store model settings in the archive."""
|
||||||
|
config = ET.Element("config")
|
||||||
|
plate = ET.SubElement(config, "plate")
|
||||||
|
ET.SubElement(plate, "metadata", key="plater_id", value="1")
|
||||||
|
ET.SubElement(plate, "metadata", key="plater_name", value="")
|
||||||
|
ET.SubElement(plate, "metadata", key="locked", value="false")
|
||||||
|
ET.SubElement(plate, "metadata", key="filament_map_mode", value="Auto For Flush")
|
||||||
|
extruders_count = len(CuraApplication.getInstance().getExtruderManager().extruderIds)
|
||||||
|
ET.SubElement(plate, "metadata", key="filament_maps", value=" ".join("1" for _ in range(extruders_count)))
|
||||||
|
ET.SubElement(plate, "metadata", key="gcode_file", value=GCODE_PATH)
|
||||||
|
ET.SubElement(plate, "metadata", key="thumbnail_file", value=THUMBNAIL_PATH_MULTIPLATE)
|
||||||
|
ET.SubElement(plate, "metadata", key="pattern_bbox_file", value=PLATE_DESC_PATH)
|
||||||
|
|
||||||
|
self._writer._storeElementTree(archive, MODEL_SETTINGS_PATH, config)
|
||||||
|
|
||||||
|
def _storePlateDesc(self, archive: zipfile.ZipFile):
|
||||||
|
"""Store plate description in the archive."""
|
||||||
|
plate_desc = {}
|
||||||
|
|
||||||
|
filament_ids = []
|
||||||
|
filament_colors = []
|
||||||
|
|
||||||
|
for extruder in CuraApplication.getInstance().getExtruderManager().getUsedExtruderStacks():
|
||||||
|
filament_ids.append(extruder.getValue("extruder_nr"))
|
||||||
|
filament_colors.append(self._writer._getMaterialColor(extruder))
|
||||||
|
|
||||||
|
plate_desc["filament_ids"] = filament_ids
|
||||||
|
plate_desc["filament_colors"] = filament_colors
|
||||||
|
plate_desc["first_extruder"] = CuraApplication.getInstance().getExtruderManager().getInitialExtruderNr()
|
||||||
|
plate_desc["is_seq_print"] = Application.getInstance().getGlobalContainerStack().getValue("print_sequence") == "one_at_a_time"
|
||||||
|
plate_desc["nozzle_diameter"] = CuraApplication.getInstance().getExtruderManager().getActiveExtruderStack().getValue("machine_nozzle_size")
|
||||||
|
plate_desc["version"] = 2
|
||||||
|
|
||||||
|
file = zipfile.ZipInfo(PLATE_DESC_PATH)
|
||||||
|
file.compress_type = zipfile.ZIP_DEFLATED
|
||||||
|
archive.writestr(file, json.dumps(plate_desc).encode("UTF-8"))
|
||||||
|
|
||||||
|
def _storeSliceInfo(self, archive: zipfile.ZipFile):
|
||||||
|
"""Store slice information in the archive."""
|
||||||
|
config = ET.Element("config")
|
||||||
|
|
||||||
|
header = ET.SubElement(config, "header")
|
||||||
|
ET.SubElement(header, "header_item", key="X-BBL-Client-Type", value="slicer")
|
||||||
|
ET.SubElement(header, "header_item", key="X-BBL-Client-Version", value="02.00.01.50")
|
||||||
|
|
||||||
|
plate = ET.SubElement(config, "plate")
|
||||||
|
ET.SubElement(plate, "metadata", key="index", value="1")
|
||||||
|
ET.SubElement(plate,
|
||||||
|
"metadata",
|
||||||
|
key="nozzle_diameters",
|
||||||
|
value=str(CuraApplication.getInstance().getExtruderManager().getActiveExtruderStack().getValue("machine_nozzle_size")))
|
||||||
|
|
||||||
|
print_information = CuraApplication.getInstance().getPrintInformation()
|
||||||
|
for index, extruder in enumerate(Application.getInstance().getGlobalContainerStack().extruderList):
|
||||||
|
used_m = print_information.materialLengths[index]
|
||||||
|
used_g = print_information.materialWeights[index]
|
||||||
|
if used_m > 0.0 and used_g > 0.0:
|
||||||
|
ET.SubElement(plate,
|
||||||
|
"filament",
|
||||||
|
id=str(extruder.getValue("extruder_nr") + 1),
|
||||||
|
tray_info_idx="GFA00",
|
||||||
|
type=extruder.material.getMetaDataEntry("material", ""),
|
||||||
|
color=self._writer._getMaterialColor(extruder),
|
||||||
|
used_m=str(used_m),
|
||||||
|
used_g=str(used_g))
|
||||||
|
|
||||||
|
self._writer._storeElementTree(archive, SLICE_INFO_PATH, config)
|
33
plugins/3MFWriter/DefaultVariant.py
Normal file
33
plugins/3MFWriter/DefaultVariant.py
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
# Copyright (c) 2025 UltiMaker
|
||||||
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
|
import xml.etree.ElementTree as ET
|
||||||
|
import zipfile
|
||||||
|
|
||||||
|
from PyQt6.QtCore import QBuffer
|
||||||
|
from PyQt6.QtGui import QImage
|
||||||
|
|
||||||
|
from .ThreeMFVariant import ThreeMFVariant
|
||||||
|
|
||||||
|
# Standard 3MF paths
|
||||||
|
METADATA_PATH = "Metadata"
|
||||||
|
THUMBNAIL_PATH = f"{METADATA_PATH}/thumbnail.png"
|
||||||
|
|
||||||
|
class DefaultVariant(ThreeMFVariant):
|
||||||
|
"""Default implementation of the 3MF format."""
|
||||||
|
|
||||||
|
@property
|
||||||
|
def mime_type(self) -> str:
|
||||||
|
return "application/vnd.ms-package.3dmanufacturing-3dmodel+xml"
|
||||||
|
|
||||||
|
def process_thumbnail(self, snapshot: QImage, thumbnail_buffer: QBuffer,
|
||||||
|
archive: zipfile.ZipFile, relations_element: ET.Element) -> None:
|
||||||
|
"""Process the thumbnail for default 3MF variant."""
|
||||||
|
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 thumbnail relation to _rels/.rels file
|
||||||
|
ET.SubElement(relations_element, "Relationship",
|
||||||
|
Target="/" + THUMBNAIL_PATH, Id="rel1",
|
||||||
|
Type="http://schemas.openxmlformats.org/package/2006/relationships/metadata/thumbnail")
|
74
plugins/3MFWriter/ThreeMFVariant.py
Normal file
74
plugins/3MFWriter/ThreeMFVariant.py
Normal file
|
@ -0,0 +1,74 @@
|
||||||
|
# Copyright (c) 2025 UltiMaker
|
||||||
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
|
from abc import ABC, abstractmethod
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
import xml.etree.ElementTree as ET
|
||||||
|
import zipfile
|
||||||
|
|
||||||
|
from PyQt6.QtGui import QImage
|
||||||
|
from PyQt6.QtCore import QBuffer
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from .ThreeMFWriter import ThreeMFWriter
|
||||||
|
|
||||||
|
class ThreeMFVariant(ABC):
|
||||||
|
"""Base class for 3MF format variants.
|
||||||
|
|
||||||
|
Different vendors may have their own extensions to the 3MF format,
|
||||||
|
such as BambuLab's 3MF variant. This class provides an interface
|
||||||
|
for implementing these variants.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, writer: 'ThreeMFWriter'):
|
||||||
|
"""
|
||||||
|
:param writer: The ThreeMFWriter instance that will use this variant
|
||||||
|
"""
|
||||||
|
self._writer = writer
|
||||||
|
|
||||||
|
@property
|
||||||
|
@abstractmethod
|
||||||
|
def mime_type(self) -> str:
|
||||||
|
"""The MIME type for this 3MF variant."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
def handles_mime_type(self, mime_type: str) -> bool:
|
||||||
|
"""Check if this variant handles the given MIME type.
|
||||||
|
|
||||||
|
:param mime_type: The MIME type to check
|
||||||
|
:return: True if this variant handles the MIME type, False otherwise
|
||||||
|
"""
|
||||||
|
return mime_type == self.mime_type
|
||||||
|
|
||||||
|
def prepare_content_types(self, content_types: ET.Element) -> None:
|
||||||
|
"""Prepare the content types XML element for this variant.
|
||||||
|
|
||||||
|
:param content_types: The content types XML element
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
def prepare_relations(self, relations_element: ET.Element) -> None:
|
||||||
|
"""Prepare the relations XML element for this variant.
|
||||||
|
|
||||||
|
:param relations_element: The relations XML element
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
def process_thumbnail(self, snapshot: QImage, thumbnail_buffer: QBuffer,
|
||||||
|
archive: zipfile.ZipFile, relations_element: ET.Element) -> None:
|
||||||
|
"""Process the thumbnail for this variant.
|
||||||
|
|
||||||
|
:param snapshot: The snapshot image
|
||||||
|
:param thumbnail_buffer: Buffer containing the thumbnail data
|
||||||
|
:param archive: The zip archive to write to
|
||||||
|
:param relations_element: The relations XML element
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
def add_extra_files(self, archive: zipfile.ZipFile, metadata_relations_element: ET.Element) -> None:
|
||||||
|
"""Add any extra files required by this variant to the archive.
|
||||||
|
|
||||||
|
:param archive: The zip archive to write to
|
||||||
|
:param metadata_relations_element: The metadata relations XML element
|
||||||
|
"""
|
||||||
|
pass
|
|
@ -1,14 +1,11 @@
|
||||||
# Copyright (c) 2015-2022 Ultimaker B.V.
|
# Copyright (c) 2015-2025 UltiMaker
|
||||||
# Cura is released under the terms of the LGPLv3 or higher.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
import hashlib
|
|
||||||
|
|
||||||
from io import StringIO
|
|
||||||
|
|
||||||
import json
|
import json
|
||||||
import re
|
import re
|
||||||
import threading
|
import threading
|
||||||
|
|
||||||
from typing import Optional, cast, List, Dict, Pattern, Set
|
from typing import Optional, cast, List, Dict, Set
|
||||||
|
|
||||||
from UM.PluginRegistry import PluginRegistry
|
from UM.PluginRegistry import PluginRegistry
|
||||||
from UM.Mesh.MeshWriter import MeshWriter
|
from UM.Mesh.MeshWriter import MeshWriter
|
||||||
|
@ -52,21 +49,15 @@ import UM.Application
|
||||||
|
|
||||||
from .SettingsExportModel import SettingsExportModel
|
from .SettingsExportModel import SettingsExportModel
|
||||||
from .SettingsExportGroup import SettingsExportGroup
|
from .SettingsExportGroup import SettingsExportGroup
|
||||||
|
from .ThreeMFVariant import ThreeMFVariant
|
||||||
|
from .DefaultVariant import DefaultVariant
|
||||||
|
from .BambuLabVariant import BambuLabVariant
|
||||||
|
|
||||||
from UM.i18n import i18nCatalog
|
from UM.i18n import i18nCatalog
|
||||||
catalog = i18nCatalog("cura")
|
catalog = i18nCatalog("cura")
|
||||||
|
|
||||||
MODEL_PATH = "3D/3dmodel.model"
|
MODEL_PATH = "3D/3dmodel.model"
|
||||||
PACKAGE_METADATA_PATH = "Cura/packages.json"
|
PACKAGE_METADATA_PATH = "Cura/packages.json"
|
||||||
METADATA_PATH = "Metadata"
|
|
||||||
THUMBNAIL_PATH = f"{METADATA_PATH}/thumbnail.png"
|
|
||||||
THUMBNAIL_PATH_MULTIPLATE = f"{METADATA_PATH}/plate_1.png"
|
|
||||||
THUMBNAIL_PATH_MULTIPLATE_SMALL = f"{METADATA_PATH}/plate_1_small.png"
|
|
||||||
GCODE_PATH = f"{METADATA_PATH}/plate_1.gcode"
|
|
||||||
GCODE_MD5_PATH = f"{GCODE_PATH}.md5"
|
|
||||||
MODEL_SETTINGS_PATH = f"{METADATA_PATH}/model_settings.config"
|
|
||||||
PLATE_DESC_PATH = f"{METADATA_PATH}/plate_1.json"
|
|
||||||
SLICE_INFO_PATH = f"{METADATA_PATH}/slice_info.config"
|
|
||||||
|
|
||||||
class ThreeMFWriter(MeshWriter):
|
class ThreeMFWriter(MeshWriter):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
@ -83,6 +74,12 @@ class ThreeMFWriter(MeshWriter):
|
||||||
self._store_archive = False
|
self._store_archive = False
|
||||||
self._lock = threading.Lock()
|
self._lock = threading.Lock()
|
||||||
|
|
||||||
|
# Register available variants
|
||||||
|
self._variants = {
|
||||||
|
DefaultVariant(self).mime_type: DefaultVariant,
|
||||||
|
BambuLabVariant(self).mime_type: BambuLabVariant
|
||||||
|
}
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _convertMatrixToString(matrix):
|
def _convertMatrixToString(matrix):
|
||||||
result = ""
|
result = ""
|
||||||
|
@ -216,10 +213,23 @@ class ThreeMFWriter(MeshWriter):
|
||||||
|
|
||||||
painter.end()
|
painter.end()
|
||||||
|
|
||||||
|
def _getVariant(self, mime_type: str) -> ThreeMFVariant:
|
||||||
|
"""Get the appropriate variant for the given MIME type.
|
||||||
|
|
||||||
|
:param mime_type: The MIME type to get the variant for
|
||||||
|
:return: An instance of the variant for the given MIME type
|
||||||
|
"""
|
||||||
|
variant_class = self._variants.get(mime_type, DefaultVariant)
|
||||||
|
return variant_class(self)
|
||||||
|
|
||||||
def write(self, stream, nodes, mode = MeshWriter.OutputMode.BinaryMode, export_settings_model = None, **kwargs) -> bool:
|
def write(self, stream, nodes, mode = MeshWriter.OutputMode.BinaryMode, export_settings_model = None, **kwargs) -> 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)
|
||||||
add_extra_data = kwargs.get("mime_type", "") == "application/vnd.bambulab-package.3dmanufacturing-3dmodel+xml"
|
|
||||||
|
# Determine which variant to use based on mime type in kwargs
|
||||||
|
mime_type = kwargs.get("mime_type", DefaultVariant(self).mime_type)
|
||||||
|
variant = self._getVariant(mime_type)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
model_file = zipfile.ZipInfo(MODEL_PATH)
|
model_file = zipfile.ZipInfo(MODEL_PATH)
|
||||||
# Because zipfile is stupid and ignores archive-level compression settings when writing with ZipInfo.
|
# Because zipfile is stupid and ignores archive-level compression settings when writing with ZipInfo.
|
||||||
|
@ -239,11 +249,12 @@ class ThreeMFWriter(MeshWriter):
|
||||||
# Create Metadata/_rels/model_settings.config.rels
|
# Create Metadata/_rels/model_settings.config.rels
|
||||||
metadata_relations_element = self._makeRelationsTree()
|
metadata_relations_element = self._makeRelationsTree()
|
||||||
|
|
||||||
if add_extra_data:
|
# Let the variant add its specific files
|
||||||
self._storeGCode(archive, metadata_relations_element)
|
variant.add_extra_files(archive, metadata_relations_element)
|
||||||
self._storeModelSettings(archive)
|
|
||||||
self._storePlateDesc(archive)
|
# Let the variant prepare content types and relations
|
||||||
self._storeSliceInfo(archive)
|
variant.prepare_content_types(content_types)
|
||||||
|
variant.prepare_relations(relations_element)
|
||||||
|
|
||||||
# Attempt to add a thumbnail
|
# Attempt to add a thumbnail
|
||||||
snapshot = self._createSnapshot()
|
snapshot = self._createSnapshot()
|
||||||
|
@ -259,32 +270,8 @@ class ThreeMFWriter(MeshWriter):
|
||||||
# Add PNG to content types file
|
# Add PNG to content types file
|
||||||
thumbnail_type = ET.SubElement(content_types, "Default", Extension="png", ContentType="image/png")
|
thumbnail_type = ET.SubElement(content_types, "Default", Extension="png", ContentType="image/png")
|
||||||
|
|
||||||
if add_extra_data:
|
# Let the variant process the thumbnail
|
||||||
archive.writestr(zipfile.ZipInfo(THUMBNAIL_PATH_MULTIPLATE), thumbnail_buffer.data())
|
variant.process_thumbnail(snapshot, thumbnail_buffer, archive, relations_element)
|
||||||
extra_thumbnail_relation_element = ET.SubElement(relations_element, "Relationship",
|
|
||||||
Target="/" + THUMBNAIL_PATH_MULTIPLATE, Id="rel-2",
|
|
||||||
Type="http://schemas.openxmlformats.org/package/2006/relationships/metadata/thumbnail")
|
|
||||||
extra_thumbnail_relation_element_duplicate = ET.SubElement(relations_element, "Relationship",
|
|
||||||
Target="/" + THUMBNAIL_PATH_MULTIPLATE, Id="rel-4",
|
|
||||||
Type="http://schemas.bambulab.com/package/2021/cover-thumbnail-middle")
|
|
||||||
|
|
||||||
small_snapshot = snapshot.scaled(128, 128, transformMode = Qt.TransformationMode.SmoothTransformation)
|
|
||||||
small_thumbnail_buffer = QBuffer()
|
|
||||||
small_thumbnail_buffer.open(QBuffer.OpenModeFlag.ReadWrite)
|
|
||||||
small_snapshot.save(small_thumbnail_buffer, "PNG")
|
|
||||||
archive.writestr(zipfile.ZipInfo(THUMBNAIL_PATH_MULTIPLATE_SMALL), small_thumbnail_buffer.data())
|
|
||||||
thumbnail_small_relation_element = ET.SubElement(relations_element, "Relationship",
|
|
||||||
Target="/" + THUMBNAIL_PATH_MULTIPLATE_SMALL, Id="rel-5",
|
|
||||||
Type="http://schemas.bambulab.com/package/2021/cover-thumbnail-small")
|
|
||||||
else:
|
|
||||||
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 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
|
# Write material metadata
|
||||||
packages_metadata = self._getMaterialPackageMetadata() + self._getPluginPackageMetadata()
|
packages_metadata = self._getMaterialPackageMetadata() + self._getPluginPackageMetadata()
|
||||||
|
@ -369,94 +356,6 @@ class ThreeMFWriter(MeshWriter):
|
||||||
file.compress_type = zipfile.ZIP_DEFLATED
|
file.compress_type = zipfile.ZIP_DEFLATED
|
||||||
archive.writestr(file, b'<?xml version="1.0" encoding="UTF-8"?> \n' + ET.tostring(root_element))
|
archive.writestr(file, b'<?xml version="1.0" encoding="UTF-8"?> \n' + ET.tostring(root_element))
|
||||||
|
|
||||||
def _storeGCode(self, archive: zipfile.ZipFile, metadata_relations_element: ET.Element):
|
|
||||||
gcode_textio = StringIO() # We have to convert the g-code into bytes.
|
|
||||||
gcode_writer = cast(MeshWriter, PluginRegistry.getInstance().getPluginObject("GCodeWriter"))
|
|
||||||
success = gcode_writer.write(gcode_textio, None)
|
|
||||||
|
|
||||||
if not success:
|
|
||||||
error_msg = catalog.i18nc("@info:error", "Can't write GCode to 3MF file")
|
|
||||||
self.setInformation(error_msg)
|
|
||||||
Logger.error(error_msg)
|
|
||||||
raise Exception(error_msg)
|
|
||||||
|
|
||||||
gcode_data = gcode_textio.getvalue().encode("UTF-8")
|
|
||||||
archive.writestr(zipfile.ZipInfo(GCODE_PATH), gcode_data)
|
|
||||||
|
|
||||||
gcode_relation_element = ET.SubElement(metadata_relations_element, "Relationship",
|
|
||||||
Target=f"/{GCODE_PATH}", Id="rel-1",
|
|
||||||
Type="http://schemas.bambulab.com/package/2021/gcode")
|
|
||||||
|
|
||||||
# Calculate and store the MD5 sum of the gcode data
|
|
||||||
md5_hash = hashlib.md5(gcode_data).hexdigest()
|
|
||||||
archive.writestr(zipfile.ZipInfo(GCODE_MD5_PATH), md5_hash.encode("UTF-8"))
|
|
||||||
|
|
||||||
def _storeModelSettings(self, archive: zipfile.ZipFile):
|
|
||||||
config = ET.Element("config")
|
|
||||||
plate = ET.SubElement(config, "plate")
|
|
||||||
plater_id = ET.SubElement(plate, "metadata", key="plater_id", value="1")
|
|
||||||
plater_id = ET.SubElement(plate, "metadata", key="plater_name", value="")
|
|
||||||
plater_id = ET.SubElement(plate, "metadata", key="locked", value="false")
|
|
||||||
plater_id = ET.SubElement(plate, "metadata", key="filament_map_mode", value="Auto For Flush")
|
|
||||||
extruders_count = len(CuraApplication.getInstance().getExtruderManager().extruderIds)
|
|
||||||
plater_id = ET.SubElement(plate, "metadata", key="filament_maps", value=" ".join("1" for _ in range(extruders_count)))
|
|
||||||
plater_id = ET.SubElement(plate, "metadata", key="gcode_file", value=GCODE_PATH)
|
|
||||||
plater_id = ET.SubElement(plate, "metadata", key="thumbnail_file", value=THUMBNAIL_PATH_MULTIPLATE)
|
|
||||||
plater_id = ET.SubElement(plate, "metadata", key="pattern_bbox_file", value=PLATE_DESC_PATH)
|
|
||||||
|
|
||||||
self._storeElementTree(archive, MODEL_SETTINGS_PATH, config)
|
|
||||||
|
|
||||||
def _storePlateDesc(self, archive: zipfile.ZipFile):
|
|
||||||
plate_desc = {}
|
|
||||||
|
|
||||||
filament_ids = []
|
|
||||||
filament_colors = []
|
|
||||||
|
|
||||||
for extruder in CuraApplication.getInstance().getExtruderManager().getUsedExtruderStacks():
|
|
||||||
filament_ids.append(extruder.getValue("extruder_nr"))
|
|
||||||
filament_colors.append(self._getMaterialColor(extruder))
|
|
||||||
|
|
||||||
plate_desc["filament_ids"] = filament_ids
|
|
||||||
plate_desc["filament_colors"] = filament_colors
|
|
||||||
plate_desc["first_extruder"] = CuraApplication.getInstance().getExtruderManager().getInitialExtruderNr()
|
|
||||||
plate_desc["is_seq_print"] = Application.getInstance().getGlobalContainerStack().getValue("print_sequence") == "one_at_a_time"
|
|
||||||
plate_desc["nozzle_diameter"] = CuraApplication.getInstance().getExtruderManager().getActiveExtruderStack().getValue("machine_nozzle_size")
|
|
||||||
plate_desc["version"] = 2
|
|
||||||
|
|
||||||
file = zipfile.ZipInfo(PLATE_DESC_PATH)
|
|
||||||
file.compress_type = zipfile.ZIP_DEFLATED
|
|
||||||
archive.writestr(file, json.dumps(plate_desc).encode("UTF-8"))
|
|
||||||
|
|
||||||
def _storeSliceInfo(self, archive: zipfile.ZipFile):
|
|
||||||
config = ET.Element("config")
|
|
||||||
|
|
||||||
header = ET.SubElement(config, "header")
|
|
||||||
header_type = ET.SubElement(header, "header_item", key="X-BBL-Client-Type", value="slicer")
|
|
||||||
header_version = ET.SubElement(header, "header_item", key="X-BBL-Client-Version", value="02.00.01.50")
|
|
||||||
|
|
||||||
plate = ET.SubElement(config, "plate")
|
|
||||||
index = ET.SubElement(plate, "metadata", key="index", value="1")
|
|
||||||
nozzle_diameter = ET.SubElement(plate,
|
|
||||||
"metadata",
|
|
||||||
key="nozzle_diameters",
|
|
||||||
value=str(CuraApplication.getInstance().getExtruderManager().getActiveExtruderStack().getValue("machine_nozzle_size")))
|
|
||||||
|
|
||||||
print_information = CuraApplication.getInstance().getPrintInformation()
|
|
||||||
for index, extruder in enumerate(Application.getInstance().getGlobalContainerStack().extruderList):
|
|
||||||
used_m = print_information.materialLengths[index]
|
|
||||||
used_g = print_information.materialWeights[index]
|
|
||||||
if used_m > 0.0 and used_g > 0.0:
|
|
||||||
filament = ET.SubElement(plate,
|
|
||||||
"filament",
|
|
||||||
id=str(extruder.getValue("extruder_nr") + 1),
|
|
||||||
tray_info_idx="GFA00",
|
|
||||||
type=extruder.material.getMetaDataEntry("material", ""),
|
|
||||||
color=self._getMaterialColor(extruder),
|
|
||||||
used_m=str(used_m),
|
|
||||||
used_g=str(used_g))
|
|
||||||
|
|
||||||
self._storeElementTree(archive, SLICE_INFO_PATH, config)
|
|
||||||
|
|
||||||
def _makeRelationsTree(self):
|
def _makeRelationsTree(self):
|
||||||
return ET.Element("Relationships", xmlns=self._namespaces["relationships"])
|
return ET.Element("Relationships", xmlns=self._namespaces["relationships"])
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue