Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
a0d2d6f
Add setting for retract/unretract during travel move
wawanbreton Jan 28, 2025
86d096b
Merge remote-tracking branch 'origin/main' into CURA-11978_retract-an…
wawanbreton Feb 5, 2025
8502284
Disable retract during travel for printers that handle retraction
wawanbreton Feb 5, 2025
5670419
Merge branch 'main' into CURA-11978_retract-and-unretract-in-a-travel
HellAholic Feb 12, 2025
cf362f4
Merge branch 'main' into CURA-11978_retract-and-unretract-in-a-travel
HellAholic Feb 14, 2025
3adf94c
move settings to experimental category
HellAholic Feb 14, 2025
c5a8a21
Merge branch 'main' into CURA-11978_retract-and-unretract-in-a-travel
wawanbreton Feb 19, 2025
d9b5069
Merge branch 'main' into CURA-11978_retract-and-unretract-in-a-travel
wawanbreton Feb 20, 2025
86777ac
Add new travel types and display z-hops
wawanbreton May 26, 2025
e1d579c
Display legend tooltip for travel types
wawanbreton May 26, 2025
b298fa6
Merge remote-tracking branch 'origin/main' into CURA-12544_saving-and…
wawanbreton May 28, 2025
96d2caf
Load UV coordinates from 3MF file
wawanbreton May 28, 2025
50ea216
Store UV coordinates to 3MF file
wawanbreton May 28, 2025
5873222
Merge remote-tracking branch 'origin/CURA-12543_painting_ux' into CUR…
wawanbreton May 28, 2025
f076413
Store painted texture to 3MF file
wawanbreton Jun 3, 2025
57f811a
Load painted texture from 3MF file
wawanbreton Jun 3, 2025
5a4b5bf
Allow properly duplicating painted models
wawanbreton Jun 3, 2025
21443fa
Merge remote-tracking branch 'origin/CURA-12543_painting_ux' into CUR…
wawanbreton Jun 3, 2025
b358b93
Use proper English word for "plane"
wawanbreton Jun 10, 2025
4ff78c8
Merge branch 'main' into CURA-12544_saving-and-loading-painted-files-…
wawanbreton Jun 16, 2025
2390067
Restore basic themes
wawanbreton Jun 17, 2025
864ccbc
Merge remote-tracking branch 'origin/CURA-12543_painting_ux' into CUR…
wawanbreton Jun 17, 2025
851d472
Restore original themes
wawanbreton Jun 17, 2025
4caba52
Basically working multipurpose painting
wawanbreton Jun 18, 2025
960d2a2
Optimized application of stroke
wawanbreton Jun 19, 2025
810638a
Merge remote-tracking branch 'origin/CURA-12543_painting_ux' into CUR…
wawanbreton Jun 19, 2025
7cca040
Merge remote-tracking branch 'origin/CURA-12544_saving-and-loading-pa…
wawanbreton Jun 19, 2025
2f2ae62
Merge pull request #20711 from Ultimaker/CURA-12566_save-proper-data-…
wawanbreton Jun 19, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions cura/LayerDataBuilder.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,9 +80,13 @@ def build(self, material_color_map, line_type_brightness = 1.0):
material_colors = numpy.zeros((line_dimensions.shape[0], 4), dtype=numpy.float32)
for extruder_nr in range(material_color_map.shape[0]):
material_colors[extruders == extruder_nr] = material_color_map[extruder_nr]
# Set material_colors with indices where line_types (also numpy array) == MoveCombingType
material_colors[line_types == LayerPolygon.MoveCombingType] = colors[line_types == LayerPolygon.MoveCombingType]
material_colors[line_types == LayerPolygon.MoveRetractionType] = colors[line_types == LayerPolygon.MoveRetractionType]
# Set material_colors with indices where line_types (also numpy array) == MoveUnretractedType
material_colors[line_types == LayerPolygon.MoveUnretractedType] = colors[line_types == LayerPolygon.MoveUnretractedType]
material_colors[line_types == LayerPolygon.MoveRetractedType] = colors[line_types == LayerPolygon.MoveRetractedType]
material_colors[line_types == LayerPolygon.MoveWhileRetractingType] = colors[
line_types == LayerPolygon.MoveWhileRetractingType]
material_colors[line_types == LayerPolygon.MoveWhileUnretractingType] = colors[
line_types == LayerPolygon.MoveWhileUnretractingType]

attributes = {
"line_dimensions": {
Expand Down
28 changes: 18 additions & 10 deletions cura/LayerPolygon.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,21 @@ class LayerPolygon:
SkirtType = 5
InfillType = 6
SupportInfillType = 7
MoveCombingType = 8
MoveRetractionType = 9
MoveUnretractedType = 8
MoveRetractedType = 9
SupportInterfaceType = 10
PrimeTowerType = 11
__number_of_types = 12

__jump_map = numpy.logical_or(numpy.logical_or(numpy.arange(__number_of_types) == NoneType,
numpy.arange(__number_of_types) == MoveCombingType),
numpy.arange(__number_of_types) == MoveRetractionType)
MoveWhileRetractingType = 12
MoveWhileUnretractingType = 13
__number_of_types = 14

__jump_map = numpy.logical_or(numpy.logical_or(numpy.logical_or(
numpy.arange(__number_of_types) == NoneType,
numpy.arange(__number_of_types) == MoveUnretractedType),
numpy.logical_or(
numpy.arange(__number_of_types) == MoveRetractedType,
numpy.arange(__number_of_types) == MoveWhileRetractingType)),
numpy.arange(__number_of_types) == MoveWhileUnretractingType)

def __init__(self, extruder: int, line_types: numpy.ndarray, data: numpy.ndarray,
line_widths: numpy.ndarray, line_thicknesses: numpy.ndarray, line_feedrates: numpy.ndarray) -> None:
Expand Down Expand Up @@ -269,10 +275,12 @@ def getColorMap(cls) -> numpy.ndarray:
theme.getColor("layerview_skirt").getRgbF(), # SkirtType
theme.getColor("layerview_infill").getRgbF(), # InfillType
theme.getColor("layerview_support_infill").getRgbF(), # SupportInfillType
theme.getColor("layerview_move_combing").getRgbF(), # MoveCombingType
theme.getColor("layerview_move_retraction").getRgbF(), # MoveRetractionType
theme.getColor("layerview_move_combing").getRgbF(), # MoveUnretractedType
theme.getColor("layerview_move_retraction").getRgbF(), # MoveRetractedType
theme.getColor("layerview_support_interface").getRgbF(), # SupportInterfaceType
theme.getColor("layerview_prime_tower").getRgbF() # PrimeTowerType
theme.getColor("layerview_prime_tower").getRgbF(), # PrimeTowerType
theme.getColor("layerview_move_while_retracting").getRgbF(), # MoveWhileRetracting
theme.getColor("layerview_move_while_unretracting").getRgbF(), # MoveWhileUnretracting
])

return cls.__color_map
37 changes: 36 additions & 1 deletion cura/Scene/SliceableObjectDecorator.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,47 @@
import copy

from typing import Optional, Dict

from PyQt6.QtGui import QImage

import UM.View.GL.Texture
from UM.Scene.SceneNodeDecorator import SceneNodeDecorator
from UM.View.GL.OpenGL import OpenGL
from UM.View.GL.Texture import Texture


# 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
Comment thread
wawanbreton marked this conversation as resolved.

class SliceableObjectDecorator(SceneNodeDecorator):
def __init__(self) -> None:
super().__init__()
self._paint_texture = None
self._texture_data_mapping: Dict[str, tuple[int, int]] = {}

def isSliceable(self) -> bool:
return True

def getPaintTexture(self, create_if_required: bool = True) -> Optional[UM.View.GL.Texture.Texture]:
if self._paint_texture is None and create_if_required:
self._paint_texture = OpenGL.getInstance().createTexture(TEXTURE_WIDTH, TEXTURE_HEIGHT)
image = QImage(TEXTURE_WIDTH, TEXTURE_HEIGHT, QImage.Format.Format_RGB32)
image.fill(0)
self._paint_texture.setImage(image)
return self._paint_texture

def setPaintTexture(self, texture: UM.View.GL.Texture) -> None:
self._paint_texture = texture

def getTextureDataMapping(self) -> Dict[str, tuple[int, int]]:
return self._texture_data_mapping

def setTextureDataMapping(self, mapping: Dict[str, tuple[int, int]]) -> None:
self._texture_data_mapping = mapping

def __deepcopy__(self, memo) -> "SliceableObjectDecorator":
return type(self)()
copied_decorator = SliceableObjectDecorator()
copied_decorator.setPaintTexture(copy.deepcopy(self.getPaintTexture()))
copied_decorator.setTextureDataMapping(copy.deepcopy(self.getTextureDataMapping()))
return copied_decorator
32 changes: 25 additions & 7 deletions plugins/3MFReader/ThreeMFReader.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

import pySavitar as Savitar
import numpy
from PyQt6.QtGui import QImage

from UM.Logger import Logger
from UM.Math.Matrix import Matrix
Expand All @@ -18,6 +19,8 @@
from UM.Scene.SceneNode import SceneNode # For typing.
from UM.Scene.SceneNodeSettings import SceneNodeSettings
from UM.Util import parseBool
from UM.View.GL.OpenGL import OpenGL
from UM.View.GL.Texture import Texture
from cura.CuraApplication import CuraApplication
from cura.Machines.ContainerTree import ContainerTree
from cura.Scene.BuildPlateDecorator import BuildPlateDecorator
Expand Down Expand Up @@ -94,14 +97,14 @@ def _createMatrixFromTransformationString(transformation: str) -> Matrix:
return temp_mat

@staticmethod
def _convertSavitarNodeToUMNode(savitar_node: Savitar.SceneNode, file_name: str = "", archive: zipfile.ZipFile = None) -> Optional[SceneNode]:
def _convertSavitarNodeToUMNode(savitar_node: Savitar.SceneNode, file_name: str = "", archive: zipfile.ZipFile = None, scene: Savitar.Scene = None) -> Optional[SceneNode]:
"""Convenience function that converts a SceneNode object (as obtained from libSavitar) to a scene node.

:returns: Scene node.
"""
try:
node_name = savitar_node.getName()
node_id = savitar_node.getId()
node_id = str(savitar_node.getId())
except AttributeError:
Logger.log("e", "Outdated version of libSavitar detected! Please update to the newest version!")
node_name = ""
Expand Down Expand Up @@ -131,12 +134,19 @@ def _convertSavitarNodeToUMNode(savitar_node: Savitar.SceneNode, file_name: str
um_node.setTransformation(transformation)
mesh_builder = MeshBuilder()

data = numpy.fromstring(savitar_node.getMeshData().getFlatVerticesAsBytes(), dtype=numpy.float32)
mesh_data = savitar_node.getMeshData()

vertices_data = numpy.fromstring(mesh_data.getFlatVerticesAsBytes(), dtype=numpy.float32)
vertices = numpy.resize(vertices_data, (int(vertices_data.size / 3), 3))

texture_path = mesh_data.getTexturePath(scene)
uv_data = numpy.fromstring(mesh_data.getUVCoordinatesPerVertexAsBytes(scene), dtype=numpy.float32)
uv_coordinates = numpy.resize(uv_data, (int(uv_data.size / 2), 2))

vertices = numpy.resize(data, (int(data.size / 3), 3))
mesh_builder.setVertices(vertices)
mesh_builder.calculateNormals(fast=True)
mesh_builder.setMeshId(node_id)
mesh_builder.setUVCoordinates(uv_coordinates)
if file_name:
# The filename is used to give the user the option to reload the file if it is changed on disk
# It is only set for the root node of the 3mf file
Expand All @@ -147,7 +157,7 @@ def _convertSavitarNodeToUMNode(savitar_node: Savitar.SceneNode, file_name: str
um_node.setMeshData(mesh_data)

for child in savitar_node.getChildren():
child_node = ThreeMFReader._convertSavitarNodeToUMNode(child, archive=archive)
child_node = ThreeMFReader._convertSavitarNodeToUMNode(child, archive=archive, scene=scene)
if child_node:
um_node.addChild(child_node)

Expand Down Expand Up @@ -219,6 +229,14 @@ def _convertSavitarNodeToUMNode(savitar_node: Savitar.SceneNode, file_name: str
# affects (auto) slicing
sliceable_decorator = SliceableObjectDecorator()
um_node.addDecorator(sliceable_decorator)

if texture_path != "" and archive is not None:
texture_data = archive.open(texture_path).read()
texture_image = QImage.fromData(texture_data, "PNG")
texture = Texture(OpenGL.getInstance())
texture.setImage(texture_image)
sliceable_decorator.setPaintTexture(texture)

return um_node

def _read(self, file_name: str) -> Union[SceneNode, List[SceneNode]]:
Expand All @@ -236,7 +254,7 @@ def _read(self, file_name: str) -> Union[SceneNode, List[SceneNode]]:
CuraApplication.getInstance().getController().getScene().setMetaDataEntry(key, value)

for node in scene_3mf.getSceneNodes():
um_node = ThreeMFReader._convertSavitarNodeToUMNode(node, file_name, archive)
um_node = ThreeMFReader._convertSavitarNodeToUMNode(node, file_name, archive, scene_3mf)
if um_node is None:
continue

Expand Down Expand Up @@ -336,7 +354,7 @@ def stringToSceneNodes(scene_string: str) -> List[SceneNode]:
# Convert the scene to scene nodes
nodes = []
for savitar_node in scene.getSceneNodes():
scene_node = ThreeMFReader._convertSavitarNodeToUMNode(savitar_node, "file_name")
scene_node = ThreeMFReader._convertSavitarNodeToUMNode(savitar_node, "file_name", scene=scene)
if scene_node is None:
continue
nodes.append(scene_node)
Expand Down
63 changes: 57 additions & 6 deletions plugins/3MFWriter/ThreeMFWriter.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@

MODEL_PATH = "3D/3dmodel.model"
PACKAGE_METADATA_PATH = "Cura/packages.json"
TEXTURES_PATH = "3D/Textures"
MODEL_RELATIONS_PATH = "3D/_rels/3dmodel.model.rels"

class ThreeMFWriter(MeshWriter):
def __init__(self):
Expand Down Expand Up @@ -109,7 +111,11 @@ def setStoreArchive(self, store_archive):
def _convertUMNodeToSavitarNode(um_node,
transformation = Matrix(),
exported_settings: Optional[Dict[str, Set[str]]] = None,
center_mesh = False):
center_mesh = False,
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

:returns: Uranium Scene node.
Expand Down Expand Up @@ -150,7 +156,35 @@ def _convertUMNodeToSavitarNode(um_node,
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())
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()
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_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)
stack = um_node.callDecoration("getStack")
Expand Down Expand Up @@ -187,7 +221,11 @@ def _convertUMNodeToSavitarNode(um_node,
if child_node.callDecoration("getBuildPlateNumber") != active_build_plate_nr:
continue
savitar_child_node = ThreeMFWriter._convertUMNodeToSavitarNode(child_node,
exported_settings = exported_settings)
exported_settings = exported_settings,
scene = scene,
archive = archive,
model_relations_element = model_relations_element,
content_types_element = content_types_element)
if savitar_child_node is not None:
savitar_node.addChild(savitar_child_node)

Expand Down Expand Up @@ -249,6 +287,9 @@ def write(self, stream, nodes, mode = MeshWriter.OutputMode.BinaryMode, export_s
# Create Metadata/_rels/model_settings.config.rels
metadata_relations_element = self._makeRelationsTree()

# Create model relations
model_relations_element = self._makeRelationsTree()

# Let the variant add its specific files
variant.add_extra_files(archive, metadata_relations_element)

Expand Down Expand Up @@ -320,13 +361,21 @@ def write(self, stream, nodes, mode = MeshWriter.OutputMode.BinaryMode, export_s
savitar_node = ThreeMFWriter._convertUMNodeToSavitarNode(root_child,
transformation_matrix,
exported_model_settings,
center_mesh = True)
center_mesh = True,
scene = savitar_scene,
archive = archive,
model_relations_element = model_relations_element,
content_types_element = content_types)
if savitar_node:
savitar_scene.addSceneNode(savitar_node)
else:
savitar_node = self._convertUMNodeToSavitarNode(node,
transformation_matrix,
exported_model_settings)
exported_model_settings,
scene = savitar_scene,
archive = archive,
model_relations_element = model_relations_element,
content_types_element = content_types)
if savitar_node:
savitar_scene.addSceneNode(savitar_node)

Expand All @@ -338,6 +387,8 @@ def write(self, stream, nodes, mode = MeshWriter.OutputMode.BinaryMode, export_s
self._storeElementTree(archive, "_rels/.rels", relations_element)
if len(metadata_relations_element) > 0:
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:
Logger.logException("e", "Error writing zip file")
self.setInformation(str(error))
Expand Down Expand Up @@ -500,7 +551,7 @@ def _createSnapshot(self):
def sceneNodesToString(scene_nodes: [SceneNode]) -> str:
savitar_scene = Savitar.Scene()
for scene_node in scene_nodes:
savitar_node = ThreeMFWriter._convertUMNodeToSavitarNode(scene_node, center_mesh = True)
savitar_node = ThreeMFWriter._convertUMNodeToSavitarNode(scene_node, center_mesh = True, scene = savitar_scene)
savitar_scene.addSceneNode(savitar_node)
parser = Savitar.ThreeMFParser()
scene_string = parser.sceneToString(savitar_scene)
Expand Down
4 changes: 2 additions & 2 deletions plugins/CuraEngineBackend/Cura.proto
Original file line number Diff line number Diff line change
Expand Up @@ -78,8 +78,8 @@ message Polygon {
SkirtType = 5;
InfillType = 6;
SupportInfillType = 7;
MoveCombingType = 8;
MoveRetractionType = 9;
MoveUnretractedType = 8;
MoveRetractedType = 9;
SupportInterfaceType = 10;
PrimeTowerType = 11;
}
Expand Down
Loading
Loading