From be14fc7dd63e9c8e3137c0288104a4eae4f081a0 Mon Sep 17 00:00:00 2001 From: Erwan MATHIEU Date: Tue, 24 Jun 2025 13:36:49 +0200 Subject: [PATCH] Send texture data to the engine CURA-12574 --- cura/Scene/SliceableObjectDecorator.py | 20 +++++++++- plugins/3MFWriter/ThreeMFWriter.py | 43 ++++++++-------------- plugins/CuraEngineBackend/Cura.proto | 2 + plugins/CuraEngineBackend/StartSliceJob.py | 8 ++++ 4 files changed, 45 insertions(+), 28 deletions(-) diff --git a/cura/Scene/SliceableObjectDecorator.py b/cura/Scene/SliceableObjectDecorator.py index dee244b81c..4278705e2e 100644 --- a/cura/Scene/SliceableObjectDecorator.py +++ b/cura/Scene/SliceableObjectDecorator.py @@ -1,8 +1,10 @@ import copy +import json from typing import Optional, Dict -from PyQt6.QtGui import QImage +from PyQt6.QtCore import QBuffer +from PyQt6.QtGui import QImage, QImageWriter import UM.View.GL.Texture from UM.Scene.SceneNodeDecorator import SceneNodeDecorator @@ -40,6 +42,22 @@ class SliceableObjectDecorator(SceneNodeDecorator): def setTextureDataMapping(self, mapping: Dict[str, tuple[int, int]]) -> None: self._texture_data_mapping = mapping + def packTexture(self) -> Optional[bytearray]: + if self._paint_texture is None: + return None + + texture_image = self._paint_texture.getImage() + if texture_image is None: + return None + + texture_buffer = QBuffer() + texture_buffer.open(QBuffer.OpenModeFlag.ReadWrite) + image_writer = QImageWriter(texture_buffer, b"png") + image_writer.setText("Description", json.dumps(self._texture_data_mapping)) + image_writer.write(texture_image) + + return texture_buffer.data() + def __deepcopy__(self, memo) -> "SliceableObjectDecorator": copied_decorator = SliceableObjectDecorator() copied_decorator.setPaintTexture(copy.deepcopy(self.getPaintTexture())) diff --git a/plugins/3MFWriter/ThreeMFWriter.py b/plugins/3MFWriter/ThreeMFWriter.py index 9d35f88e6d..37345b16b0 100644 --- a/plugins/3MFWriter/ThreeMFWriter.py +++ b/plugins/3MFWriter/ThreeMFWriter.py @@ -29,7 +29,7 @@ from cura.Scene.CuraSceneNode import CuraSceneNode from cura.Snapshot import Snapshot from PyQt6.QtCore import Qt, QBuffer -from PyQt6.QtGui import QImage, QPainter, QImageWriter +from PyQt6.QtGui import QImage, QPainter import pySavitar as Savitar from .UCPDialog import UCPDialog @@ -158,36 +158,25 @@ class ThreeMFWriter(MeshWriter): else: savitar_node.getMeshData().setFacesFromBytes(numpy.arange(mesh_data.getVertices().size / 3, dtype=numpy.int32).tobytes()) - texture = um_node.callDecoration("getPaintTexture") + packed_texture = um_node.callDecoration("packTexture") uv_coordinates_array = mesh_data.getUVCoordinatesAsByteArray() - if texture is not None and archive is not None and uv_coordinates_array is not None and len(uv_coordinates_array) > 0: - texture_image = texture.getImage() - if texture_image is not None: - texture_buffer = QBuffer() - texture_buffer.open(QBuffer.OpenModeFlag.ReadWrite) + if packed_texture is not None and archive is not None and uv_coordinates_array is not None and len(uv_coordinates_array) > 0: + texture_path = f"{TEXTURES_PATH}/{id(um_node)}.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, packed_texture) - image_writer = QImageWriter(texture_buffer, b"png") - texture_data_mapping = um_node.callDecoration("getTextureDataMapping") - if texture_data_mapping is not None: - image_writer.setText("Description", json.dumps(texture_data_mapping)) - image_writer.write(texture_image) + savitar_node.getMeshData().setUVCoordinatesPerVertexAsBytes(uv_coordinates_array, texture_path, scene) - texture_path = f"{TEXTURES_PATH}/{id(um_node)}.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()) + # 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") - 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") + 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) diff --git a/plugins/CuraEngineBackend/Cura.proto b/plugins/CuraEngineBackend/Cura.proto index 8018c9186f..f492718d03 100644 --- a/plugins/CuraEngineBackend/Cura.proto +++ b/plugins/CuraEngineBackend/Cura.proto @@ -53,6 +53,8 @@ message Object bytes indices = 4; //An array of ints. repeated Setting settings = 5; // Setting override per object, overruling the global settings. string name = 6; //Mesh name + bytes uv_coordinates = 7; //An array of 2 floats. + bytes texture = 8; //PNG-encoded texture data } message Progress diff --git a/plugins/CuraEngineBackend/StartSliceJob.py b/plugins/CuraEngineBackend/StartSliceJob.py index b276469d09..8b27a0319a 100644 --- a/plugins/CuraEngineBackend/StartSliceJob.py +++ b/plugins/CuraEngineBackend/StartSliceJob.py @@ -509,6 +509,14 @@ class StartSliceJob(Job): obj.vertices = flat_verts + uv_coordinates = mesh_data.getUVCoordinates() + if uv_coordinates is not None: + obj.uv_coordinates = uv_coordinates.flatten() + + packed_texture = object.callDecoration("packTexture") + if packed_texture is not None: + obj.texture = packed_texture + self._handlePerObjectSettings(cast(CuraSceneNode, object), obj) Job.yieldThread()