Store painted texture to 3MF file

CURA-12544
Also allows having multiple texture for multiple models while painting
This commit is contained in:
Erwan MATHIEU 2025-06-03 13:27:57 +02:00
parent 5873222c15
commit f0764134cc
8 changed files with 73 additions and 988 deletions

View File

@ -1,12 +1,23 @@
from UM.Scene.SceneNodeDecorator import SceneNodeDecorator from UM.Scene.SceneNodeDecorator import SceneNodeDecorator
from UM.View.GL.OpenGL import OpenGL
# FIXME: When the texture UV-unwrapping is done, these two values will need to be set to a proper value (suggest 4096 for both).
TEXTURE_WIDTH = 512
TEXTURE_HEIGHT = 512
class SliceableObjectDecorator(SceneNodeDecorator): class SliceableObjectDecorator(SceneNodeDecorator):
def __init__(self) -> None: def __init__(self) -> None:
super().__init__() super().__init__()
self._paint_texture = None
def isSliceable(self) -> bool: def isSliceable(self) -> bool:
return True return True
def getPaintTexture(self, create_if_required: bool = True):
if self._paint_texture is None and create_if_required:
self._paint_texture = OpenGL.getInstance().createTexture(TEXTURE_WIDTH, TEXTURE_HEIGHT)
return self._paint_texture
def __deepcopy__(self, memo) -> "SliceableObjectDecorator": def __deepcopy__(self, memo) -> "SliceableObjectDecorator":
return type(self)() return type(self)()

View File

@ -58,6 +58,8 @@ 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"
TEXTURES_PATH = "3D/Textures"
MODEL_RELATIONS_PATH = "3D/_rels/3dmodel.model.rels"
class ThreeMFWriter(MeshWriter): class ThreeMFWriter(MeshWriter):
def __init__(self): def __init__(self):
@ -110,7 +112,10 @@ class ThreeMFWriter(MeshWriter):
transformation = Matrix(), transformation = Matrix(),
exported_settings: Optional[Dict[str, Set[str]]] = None, exported_settings: Optional[Dict[str, Set[str]]] = None,
center_mesh = False, center_mesh = False,
scene: Savitar.Scene = None): scene: Savitar.Scene = None,
archive: zipfile.ZipFile = None,
model_relations_element: ET.Element = None,
content_types_element: ET.Element = 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.
@ -153,9 +158,33 @@ class ThreeMFWriter(MeshWriter):
else: else:
savitar_node.getMeshData().setFacesFromBytes(numpy.arange(mesh_data.getVertices().size / 3, dtype=numpy.int32).tobytes()) savitar_node.getMeshData().setFacesFromBytes(numpy.arange(mesh_data.getVertices().size / 3, dtype=numpy.int32).tobytes())
texture = um_node.callDecoration("getPaintTexture")
uv_coordinates_array = mesh_data.getUVCoordinatesAsByteArray() uv_coordinates_array = mesh_data.getUVCoordinatesAsByteArray()
if uv_coordinates_array is not None and len(uv_coordinates_array) > 0: if texture is not None and archive is not None and uv_coordinates_array is not None and len(uv_coordinates_array) > 0:
savitar_node.getMeshData().setUVCoordinatesPerVertexAsBytes(uv_coordinates_array, scene) texture_image = texture.getImage()
if texture_image is not None:
texture_path = f"{TEXTURES_PATH}/{id(um_node)}.png"
texture_buffer = QBuffer()
texture_buffer.open(QBuffer.OpenModeFlag.ReadWrite)
texture_image.save(texture_buffer, "PNG")
texture_file = zipfile.ZipInfo(texture_path)
# Don't try to compress texture file, because the PNG is pretty much as compact as it will get
archive.writestr(texture_file, texture_buffer.data())
savitar_node.getMeshData().setUVCoordinatesPerVertexAsBytes(uv_coordinates_array, texture_path, scene)
# Add texture relation to model relations file
if model_relations_element is not None:
ET.SubElement(model_relations_element, "Relationship",
Target=texture_path, Id=f"rel{len(model_relations_element)+1}",
Type="http://schemas.microsoft.com/3dmanufacturing/2013/01/3dtexture")
if content_types_element is not None:
ET.SubElement(content_types_element, "Override", PartName=texture_path,
ContentType="application/vnd.ms-package.3dmanufacturing-3dmodeltexture")
# Handle per object settings (if any) # Handle per object settings (if any)
stack = um_node.callDecoration("getStack") stack = um_node.callDecoration("getStack")
@ -193,7 +222,10 @@ class ThreeMFWriter(MeshWriter):
continue continue
savitar_child_node = ThreeMFWriter._convertUMNodeToSavitarNode(child_node, savitar_child_node = ThreeMFWriter._convertUMNodeToSavitarNode(child_node,
exported_settings = exported_settings, exported_settings = exported_settings,
scene = scene) scene = scene,
archive = archive,
model_relations_element = model_relations_element,
content_types_element = content_types_element)
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)
@ -255,6 +287,9 @@ 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()
# Create model relations
model_relations_element = self._makeRelationsTree()
# Let the variant add its specific files # Let the variant add its specific files
variant.add_extra_files(archive, metadata_relations_element) variant.add_extra_files(archive, metadata_relations_element)
@ -327,14 +362,20 @@ class ThreeMFWriter(MeshWriter):
transformation_matrix, transformation_matrix,
exported_model_settings, exported_model_settings,
center_mesh = True, center_mesh = True,
scene = savitar_scene) scene = savitar_scene,
archive = archive,
model_relations_element = model_relations_element,
content_types_element = content_types)
if savitar_node: if savitar_node:
savitar_scene.addSceneNode(savitar_node) savitar_scene.addSceneNode(savitar_node)
else: else:
savitar_node = self._convertUMNodeToSavitarNode(node, savitar_node = self._convertUMNodeToSavitarNode(node,
transformation_matrix, transformation_matrix,
exported_model_settings, exported_model_settings,
scene = savitar_scene) scene = savitar_scene,
archive = archive,
model_relations_element = model_relations_element,
content_types_element = content_types)
if savitar_node: if savitar_node:
savitar_scene.addSceneNode(savitar_node) savitar_scene.addSceneNode(savitar_node)
@ -346,6 +387,8 @@ class ThreeMFWriter(MeshWriter):
self._storeElementTree(archive, "_rels/.rels", relations_element) self._storeElementTree(archive, "_rels/.rels", relations_element)
if len(metadata_relations_element) > 0: if len(metadata_relations_element) > 0:
self._storeElementTree(archive, "Metadata/_rels/model_settings.config.rels", metadata_relations_element) self._storeElementTree(archive, "Metadata/_rels/model_settings.config.rels", metadata_relations_element)
if len(model_relations_element) > 0:
self._storeElementTree(archive, MODEL_RELATIONS_PATH, model_relations_element)
except Exception as error: except Exception as error:
Logger.logException("e", "Error writing zip file") Logger.logException("e", "Error writing zip file")
self.setInformation(str(error)) self.setInformation(str(error))

View File

@ -19,25 +19,21 @@ class PaintView(View):
def __init__(self) -> None: def __init__(self) -> None:
super().__init__() super().__init__()
self._paint_shader = None self._paint_shader = None
self._paint_texture = None self._current_paint_texture = None
# FIXME: When the texture UV-unwrapping is done, these two values will need to be set to a proper value (suggest 4096 for both).
self._tex_width = 512
self._tex_height = 512
def _checkSetup(self): def _checkSetup(self):
if not self._paint_shader: if not self._paint_shader:
shader_filename = os.path.join(PluginRegistry.getInstance().getPluginPath("PaintTool"), "paint.shader") shader_filename = os.path.join(PluginRegistry.getInstance().getPluginPath("PaintTool"), "paint.shader")
self._paint_shader = OpenGL.getInstance().createShaderProgram(shader_filename) self._paint_shader = OpenGL.getInstance().createShaderProgram(shader_filename)
if not self._paint_texture:
self._paint_texture = OpenGL.getInstance().createTexture(self._tex_width, self._tex_height)
self._paint_shader.setTexture(0, self._paint_texture)
def addStroke(self, stroke_image: QImage, start_x: int, start_y: int) -> None: def addStroke(self, stroke_image: QImage, start_x: int, start_y: int) -> None:
self._paint_texture.setSubImage(stroke_image, start_x, start_y) if self._current_paint_texture is not None:
self._current_paint_texture.setSubImage(stroke_image, start_x, start_y)
def getUvTexDimensions(self): def getUvTexDimensions(self):
return self._tex_width, self._tex_height if self._current_paint_texture is not None:
return self._current_paint_texture.getWidth(), self._current_paint_texture.getHeight()
return 0, 0
def beginRendering(self) -> None: def beginRendering(self) -> None:
renderer = self.getRenderer() renderer = self.getRenderer()
@ -48,4 +44,7 @@ class PaintView(View):
node = Selection.getAllSelectedObjects()[0] node = Selection.getAllSelectedObjects()[0]
if node is None: if node is None:
return return
self._current_paint_texture = node.callDecoration("getPaintTexture")
self._paint_shader.setTexture(0, self._current_paint_texture)
paint_batch.addItem(node.getWorldTransformation(copy=False), node.getMeshData(), normal_transformation=node.getCachedNormalMatrix()) paint_batch.addItem(node.getWorldTransformation(copy=False), node.getMeshData(), normal_transformation=node.getCachedNormalMatrix())

View File

@ -1,26 +1 @@
{ {"metadata": {"name": "Colorblind Assist Dark", "inherits": "cura-dark"}, "colors": {"x_axis": [212, 0, 0, 255], "y_axis": [64, 64, 255, 255], "model_overhang": [200, 0, 255, 255], "xray": [26, 26, 62, 255], "xray_error": [255, 0, 0, 255], "layerview_inset_0": [255, 64, 0, 255], "layerview_inset_x": [0, 156, 128, 255], "layerview_skin": [255, 255, 86, 255], "layerview_support": [255, 255, 0, 255], "layerview_infill": [0, 255, 255, 255], "layerview_support_infill": [0, 200, 200, 255], "layerview_move_retraction": [0, 100, 255, 255], "main_window_header_background": [192, 199, 65, 255]}}
"metadata": {
"name": "Colorblind Assist Dark",
"inherits": "cura-dark"
},
"colors": {
"x_axis": [212, 0, 0, 255],
"y_axis": [64, 64, 255, 255],
"model_overhang": [200, 0, 255, 255],
"xray": [26, 26, 62, 255],
"xray_error": [255, 0, 0, 255],
"layerview_inset_0": [255, 64, 0, 255],
"layerview_inset_x": [0, 156, 128, 255],
"layerview_skin": [255, 255, 86, 255],
"layerview_support": [255, 255, 0, 255],
"layerview_infill": [0, 255, 255, 255],
"layerview_support_infill": [0, 200, 200, 255],
"layerview_move_retraction": [0, 100, 255, 255]
}
}

File diff suppressed because one or more lines are too long

View File

@ -1,29 +1 @@
{ {"metadata": {"name": "Colorblind Assist Light", "inherits": "cura-light"}, "colors": {"x_axis": [200, 0, 0, 255], "y_axis": [64, 64, 255, 255], "model_overhang": [200, 0, 255, 255], "model_selection_outline": [12, 169, 227, 255], "xray_error_dark": [255, 0, 0, 255], "xray_error_light": [255, 255, 0, 255], "xray": [26, 26, 62, 255], "xray_error": [255, 0, 0, 255], "layerview_inset_0": [255, 64, 0, 255], "layerview_inset_x": [0, 156, 128, 255], "layerview_skin": [255, 255, 86, 255], "layerview_support": [255, 255, 0, 255], "layerview_infill": [0, 255, 255, 255], "layerview_support_infill": [0, 200, 200, 255], "layerview_move_retraction": [0, 100, 255, 255], "main_window_header_background": [192, 199, 65, 255]}}
"metadata": {
"name": "Colorblind Assist Light",
"inherits": "cura-light"
},
"colors": {
"x_axis": [200, 0, 0, 255],
"y_axis": [64, 64, 255, 255],
"model_overhang": [200, 0, 255, 255],
"model_selection_outline": [12, 169, 227, 255],
"xray_error_dark": [255, 0, 0, 255],
"xray_error_light": [255, 255, 0, 255],
"xray": [26, 26, 62, 255],
"xray_error": [255, 0, 0, 255],
"layerview_inset_0": [255, 64, 0, 255],
"layerview_inset_x": [0, 156, 128, 255],
"layerview_skin": [255, 255, 86, 255],
"layerview_support": [255, 255, 0, 255],
"layerview_infill": [0, 255, 255, 255],
"layerview_support_infill": [0, 200, 200, 255],
"layerview_move_retraction": [0, 100, 255, 255]
}
}

File diff suppressed because one or more lines are too long

View File

@ -1,16 +0,0 @@
[
[ 62, 33, 55, 255],
[126, 196, 193, 255],
[126, 196, 193, 255],
[215, 155, 125, 255],
[228, 148, 58, 255],
[192, 199, 65, 255],
[157, 48, 59, 255],
[140, 143, 174, 255],
[ 23, 67, 75, 255],
[ 23, 67, 75, 255],
[154, 99, 72, 255],
[112, 55, 127, 255],
[100, 125, 52, 255],
[210, 100, 113, 255]
]