mirror of
https://github.com/Ultimaker/Cura.git
synced 2026-01-01 04:00:36 -07:00
Merge pull request #21001 from Ultimaker/CURA-12752_multimat_paint_prime_tower
[CURA-12752] Fix prime-tower for multi-material painting
This commit is contained in:
commit
be88d745ed
10 changed files with 107 additions and 45 deletions
|
|
@ -116,7 +116,8 @@ class BuildVolume(SceneNode):
|
|||
self._application.engineCreatedSignal.connect(self._onEngineCreated)
|
||||
|
||||
self._has_errors = False
|
||||
self._application.getController().getScene().sceneChanged.connect(self._onSceneChanged)
|
||||
scene = self._application.getController().getScene()
|
||||
scene.sceneChanged.connect(self._onSceneChanged)
|
||||
|
||||
# Objects loaded at the moment. We are connected to the property changed events of these objects.
|
||||
self._scene_objects = set() # type: Set[SceneNode]
|
||||
|
|
@ -655,7 +656,7 @@ class BuildVolume(SceneNode):
|
|||
extra_z = retraction_hop
|
||||
return extra_z
|
||||
|
||||
def _onStackChanged(self):
|
||||
def _onStackChanged(self, *args) -> None:
|
||||
self._stack_change_timer.start()
|
||||
|
||||
def _onStackChangeTimerFinished(self) -> None:
|
||||
|
|
|
|||
|
|
@ -1,9 +1,12 @@
|
|||
# Copyright (c) 2025 UltiMaker
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
import copy
|
||||
import json
|
||||
import numpy
|
||||
|
||||
from typing import Optional, Dict
|
||||
from typing import Optional, Dict, List
|
||||
|
||||
from PyQt6.QtCore import QBuffer
|
||||
from PyQt6.QtCore import QBuffer, QTimer
|
||||
from PyQt6.QtGui import QImage, QImageWriter
|
||||
|
||||
from UM.Scene.SceneNodeDecorator import SceneNodeDecorator
|
||||
|
|
@ -18,8 +21,15 @@ class SliceableObjectDecorator(SceneNodeDecorator):
|
|||
self._paint_texture = None
|
||||
self._texture_data_mapping: Dict[str, tuple[int, int]] = {}
|
||||
|
||||
self._painted_extruders: Optional[List[int]] = None
|
||||
|
||||
self.paintTextureChanged = Signal()
|
||||
|
||||
self._texture_change_timer = QTimer()
|
||||
self._texture_change_timer.setInterval(500) # Long interval to avoid triggering during painting
|
||||
self._texture_change_timer.setSingleShot(True)
|
||||
self._texture_change_timer.timeout.connect(self._onTextureChangeTimerFinished)
|
||||
|
||||
def isSliceable(self) -> bool:
|
||||
return True
|
||||
|
||||
|
|
@ -29,6 +39,32 @@ class SliceableObjectDecorator(SceneNodeDecorator):
|
|||
def getPaintTextureChangedSignal(self) -> Signal:
|
||||
return self.paintTextureChanged
|
||||
|
||||
def setPaintedExtrudersCountDirty(self) -> None:
|
||||
self._texture_change_timer.start()
|
||||
|
||||
def _onTextureChangeTimerFinished(self) -> None:
|
||||
self._painted_extruders = None
|
||||
|
||||
if (self._paint_texture is None or self._paint_texture.getImage() is None or
|
||||
"extruder" not in self._texture_data_mapping):
|
||||
return
|
||||
|
||||
image = self._paint_texture.getImage()
|
||||
image_bits = image.constBits()
|
||||
image_bits.setsize(image.sizeInBytes())
|
||||
image_array = numpy.frombuffer(image_bits, dtype=numpy.uint32)
|
||||
|
||||
bit_range_start, bit_range_end = self._texture_data_mapping["extruder"]
|
||||
full_int32 = 0xffffffff
|
||||
bit_mask = (((full_int32 << (32 - 1 - (bit_range_end - bit_range_start))) & full_int32) >> (
|
||||
32 - 1 - bit_range_end))
|
||||
|
||||
texel_counts = numpy.bincount((image_array & bit_mask) >> bit_range_start)
|
||||
self._painted_extruders = [extruder_nr for extruder_nr, count in enumerate(texel_counts) if count > 0]
|
||||
|
||||
from cura.CuraApplication import CuraApplication
|
||||
CuraApplication.getInstance().globalContainerStackChanged.emit()
|
||||
|
||||
def setPaintTexture(self, texture: Texture) -> None:
|
||||
self._paint_texture = texture
|
||||
self.paintTextureChanged.emit()
|
||||
|
|
@ -63,6 +99,9 @@ class SliceableObjectDecorator(SceneNodeDecorator):
|
|||
|
||||
return texture_buffer.data()
|
||||
|
||||
def getPaintedExtruders(self) -> Optional[List[int]]:
|
||||
return self._painted_extruders
|
||||
|
||||
def __deepcopy__(self, memo) -> "SliceableObjectDecorator":
|
||||
copied_decorator = SliceableObjectDecorator()
|
||||
copied_decorator.setPaintTexture(copy.deepcopy(self.getPaintTexture()))
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
# Copyright (c) 2022 Ultimaker B.V.
|
||||
# Copyright (c) 2025 UltiMaker
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
from PyQt6.QtCore import pyqtSignal, pyqtProperty, QObject, QVariant # For communicating data and events to Qt.
|
||||
|
|
@ -254,6 +254,11 @@ class ExtruderManager(QObject):
|
|||
if not support_roof_enabled:
|
||||
support_roof_enabled |= stack_to_use.getProperty("support_roof_enable", "value")
|
||||
|
||||
painted_extruders = node.callDecoration("getPaintedExtruders")
|
||||
if painted_extruders is not None:
|
||||
for extruder_nr in painted_extruders:
|
||||
used_extruder_stack_ids.add(self.extruderIds[str(extruder_nr)])
|
||||
|
||||
# Check limit to extruders
|
||||
limit_to_extruder_feature_list = ["wall_0_extruder_nr",
|
||||
"wall_x_extruder_nr",
|
||||
|
|
|
|||
|
|
@ -416,7 +416,7 @@ class StartSliceJob(Job):
|
|||
# Only check if the printing extruder is enabled for printing meshes
|
||||
is_non_printing_mesh = node.callDecoration("evaluateIsNonPrintingMesh")
|
||||
if not is_non_printing_mesh:
|
||||
for used_extruder in StartSliceJob._getUsedExtruders(node):
|
||||
for used_extruder in StartSliceJob._getMainExtruders(node):
|
||||
if not extruders_enabled[used_extruder]:
|
||||
skip_group = True
|
||||
has_model_with_disabled_extruders = True
|
||||
|
|
@ -763,28 +763,11 @@ class StartSliceJob(Job):
|
|||
self._addRelations(relations_set, relation.target.relations)
|
||||
|
||||
@staticmethod
|
||||
def _getUsedExtruders(node: SceneNode) -> List[int]:
|
||||
used_extruders = []
|
||||
|
||||
# Look at extruders used in painted texture
|
||||
node_texture = node.callDecoration("getPaintTexture")
|
||||
texture_data_mapping = node.callDecoration("getTextureDataMapping")
|
||||
if node_texture is not None and texture_data_mapping is not None and "extruder" in texture_data_mapping:
|
||||
texture_image = node_texture.getImage()
|
||||
image_ptr = texture_image.constBits()
|
||||
image_ptr.setsize(texture_image.sizeInBytes())
|
||||
image_size = texture_image.height(), texture_image.width()
|
||||
image_array = numpy.frombuffer(image_ptr, dtype=numpy.uint32).reshape(image_size)
|
||||
|
||||
bit_range_start, bit_range_end = texture_data_mapping["extruder"]
|
||||
full_int32 = 0xffffffff
|
||||
bit_mask = (((full_int32 << (32 - 1 - (bit_range_end - bit_range_start))) & full_int32) >> (
|
||||
32 - 1 - bit_range_end))
|
||||
|
||||
used_extruders = (numpy.unique(image_array & bit_mask) >> bit_range_start).tolist()
|
||||
def _getMainExtruders(node: SceneNode) -> List[int]:
|
||||
used_extruders = node.callDecoration("getPaintedExtruders")
|
||||
|
||||
# There is no relevant painting data, just take the extruder associated to the model
|
||||
if not used_extruders:
|
||||
used_extruders = [int(node.callDecoration("getActiveExtruderPosition"))]
|
||||
|
||||
return used_extruders
|
||||
return used_extruders
|
||||
|
|
|
|||
|
|
@ -109,4 +109,6 @@ class MultiMaterialExtruderConverter:
|
|||
|
||||
texture.updateImagePart(image.rect())
|
||||
|
||||
node.callDecoration("setPaintedExtrudersCountDirty")
|
||||
|
||||
self.mainExtruderChanged.emit(node)
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ from typing import Optional
|
|||
|
||||
from PyQt6.QtGui import QUndoCommand, QImage, QPainter, QBrush
|
||||
|
||||
from cura.Scene.SliceableObjectDecorator import SliceableObjectDecorator
|
||||
from UM.View.GL.Texture import Texture
|
||||
|
||||
from .PaintCommand import PaintCommand
|
||||
|
|
@ -13,8 +14,12 @@ from .PaintCommand import PaintCommand
|
|||
class PaintClearCommand(PaintCommand):
|
||||
"""Provides the command that clears all the painting for the current mode"""
|
||||
|
||||
def __init__(self, texture: Texture, bit_range: tuple[int, int], set_value: int) -> None:
|
||||
super().__init__(texture, bit_range)
|
||||
def __init__(self,
|
||||
texture: Texture,
|
||||
bit_range: tuple[int, int],
|
||||
set_value: int,
|
||||
sliceable_object_decorator: Optional[SliceableObjectDecorator] = None) -> None:
|
||||
super().__init__(texture, bit_range, sliceable_object_decorator=sliceable_object_decorator)
|
||||
self._set_value = set_value
|
||||
|
||||
def id(self) -> int:
|
||||
|
|
@ -22,13 +27,12 @@ class PaintClearCommand(PaintCommand):
|
|||
|
||||
def redo(self) -> None:
|
||||
painter = self._makeClearedTexture()
|
||||
|
||||
if self._set_value > 0:
|
||||
painter.setCompositionMode(QPainter.CompositionMode.RasterOp_SourceOrDestination)
|
||||
painter.fillRect(self._texture.getImage().rect(), QBrush(self._set_value))
|
||||
|
||||
painter.end()
|
||||
|
||||
self._setPaintedExtrudersCountDirty()
|
||||
self._texture.updateImagePart(self._bounding_rect)
|
||||
|
||||
def mergeWith(self, command: QUndoCommand) -> bool:
|
||||
|
|
@ -38,6 +42,6 @@ class PaintClearCommand(PaintCommand):
|
|||
# There is actually nothing more to do here, both clear commands already have the same original texture
|
||||
return True
|
||||
|
||||
def _clearTextureBits(self, painter: QPainter):
|
||||
def _clearTextureBits(self, painter: QPainter, extended = False):
|
||||
painter.setCompositionMode(QPainter.CompositionMode.RasterOp_NotSourceAndDestination)
|
||||
painter.fillRect(self._texture.getImage().rect(), QBrush(self._getBitRangeMask()))
|
||||
|
|
@ -1,12 +1,14 @@
|
|||
# Copyright (c) 2025 UltiMaker
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
from typing import Tuple, Optional
|
||||
from typing import Tuple, Optional, Dict
|
||||
|
||||
from PyQt6.QtCore import QRect
|
||||
import numpy
|
||||
from PyQt6.QtGui import QUndoCommand, QImage, QPainter, QBrush
|
||||
|
||||
from UM.View.GL.Texture import Texture
|
||||
from cura.CuraApplication import CuraApplication
|
||||
from cura.Scene.SliceableObjectDecorator import SliceableObjectDecorator
|
||||
|
||||
|
||||
class PaintCommand(QUndoCommand):
|
||||
|
|
@ -14,7 +16,11 @@ class PaintCommand(QUndoCommand):
|
|||
|
||||
FULL_INT32 = 0xffffffff
|
||||
|
||||
def __init__(self, texture: Texture, bit_range: tuple[int, int], make_original_image = True) -> None:
|
||||
def __init__(self,
|
||||
texture: Texture,
|
||||
bit_range: tuple[int, int],
|
||||
make_original_image = True,
|
||||
sliceable_object_decorator: Optional[SliceableObjectDecorator] = None) -> None:
|
||||
super().__init__()
|
||||
|
||||
self._texture: Texture = texture
|
||||
|
|
@ -22,6 +28,8 @@ class PaintCommand(QUndoCommand):
|
|||
self._original_texture_image = None
|
||||
self._bounding_rect = texture.getImage().rect()
|
||||
|
||||
self._sliceable_object_decorator: Optional[SliceableObjectDecorator] = sliceable_object_decorator
|
||||
|
||||
if make_original_image:
|
||||
self._original_texture_image = self._texture.getImage().copy()
|
||||
painter = QPainter(self._original_texture_image)
|
||||
|
|
@ -35,21 +43,26 @@ class PaintCommand(QUndoCommand):
|
|||
if self._original_texture_image is None:
|
||||
return
|
||||
|
||||
painter = self._makeClearedTexture()
|
||||
painter = self._makeClearedTexture(extended=True)
|
||||
painter.setCompositionMode(QPainter.CompositionMode.RasterOp_SourceOrDestination)
|
||||
painter.drawImage(0, 0, self._original_texture_image)
|
||||
painter.end()
|
||||
|
||||
self._setPaintedExtrudersCountDirty()
|
||||
self._texture.updateImagePart(self._bounding_rect)
|
||||
|
||||
def _makeClearedTexture(self) -> QPainter:
|
||||
def _setPaintedExtrudersCountDirty(self) -> None:
|
||||
if self._sliceable_object_decorator is not None:
|
||||
self._sliceable_object_decorator.setPaintedExtrudersCountDirty()
|
||||
|
||||
def _makeClearedTexture(self, extended = False) -> QPainter:
|
||||
painter = QPainter(self._texture.getImage())
|
||||
painter.setRenderHint(QPainter.RenderHint.Antialiasing, False)
|
||||
|
||||
self._clearTextureBits(painter)
|
||||
self._clearTextureBits(painter, extended)
|
||||
return painter
|
||||
|
||||
def _clearTextureBits(self, painter: QPainter):
|
||||
def _clearTextureBits(self, painter: QPainter, extended = False):
|
||||
raise NotImplementedError()
|
||||
|
||||
@staticmethod
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import math
|
|||
from PyQt6.QtCore import QRect, QRectF, QPoint
|
||||
from PyQt6.QtGui import QUndoCommand, QImage, QPainter, QPainterPath, QPen, QBrush
|
||||
|
||||
from cura.Scene.SliceableObjectDecorator import SliceableObjectDecorator
|
||||
from UM.View.GL.Texture import Texture
|
||||
from UM.Math.Polygon import Polygon
|
||||
|
||||
|
|
@ -16,14 +17,16 @@ class PaintStrokeCommand(PaintCommand):
|
|||
"""Provides the command that does the actual painting on objects with undo/redo mechanisms"""
|
||||
|
||||
PEN_OVERLAP_WIDTH = 2.5
|
||||
PEN_OVERLAP_WIDTH_EXTENDED = PEN_OVERLAP_WIDTH + 0.5
|
||||
|
||||
def __init__(self,
|
||||
texture: Texture,
|
||||
stroke_polygons: List[Polygon],
|
||||
set_value: int,
|
||||
bit_range: tuple[int, int],
|
||||
mergeable: bool) -> None:
|
||||
super().__init__(texture, bit_range, make_original_image = not mergeable)
|
||||
mergeable: bool,
|
||||
sliceable_object_decorator: Optional[SliceableObjectDecorator] = None) -> None:
|
||||
super().__init__(texture, bit_range, make_original_image = not mergeable, sliceable_object_decorator=sliceable_object_decorator)
|
||||
self._stroke_polygons: List[Polygon] = stroke_polygons
|
||||
self._calculateBoundingRect()
|
||||
self._set_value: int = set_value
|
||||
|
|
@ -40,6 +43,7 @@ class PaintStrokeCommand(PaintCommand):
|
|||
painter.drawPath(self._makePainterPath())
|
||||
painter.end()
|
||||
|
||||
self._setPaintedExtrudersCountDirty()
|
||||
self._texture.updateImagePart(self._bounding_rect)
|
||||
|
||||
def mergeWith(self, command: QUndoCommand) -> bool:
|
||||
|
|
@ -55,9 +59,9 @@ class PaintStrokeCommand(PaintCommand):
|
|||
|
||||
return True
|
||||
|
||||
def _clearTextureBits(self, painter: QPainter):
|
||||
def _clearTextureBits(self, painter: QPainter, extended = False):
|
||||
painter.setBrush(QBrush(self._getBitRangeMask()))
|
||||
painter.setPen(QPen(painter.brush(), self.PEN_OVERLAP_WIDTH))
|
||||
painter.setPen(QPen(painter.brush(), self.PEN_OVERLAP_WIDTH_EXTENDED if extended else self.PEN_OVERLAP_WIDTH))
|
||||
painter.setCompositionMode(QPainter.CompositionMode.RasterOp_NotSourceAndDestination)
|
||||
painter.drawPath(self._makePainterPath())
|
||||
|
||||
|
|
|
|||
|
|
@ -412,7 +412,7 @@ class PaintTool(Tool):
|
|||
Logger.logException("e", "Error when adding paint stroke")
|
||||
|
||||
self._last_world_coords = world_coords
|
||||
self._updateScene(painted_object, update_node = self._mouse_held)
|
||||
self._updateScene(painted_object, update_node = event_caught)
|
||||
return event_caught
|
||||
|
||||
return False
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ from UM.View.GL.OpenGL import OpenGL
|
|||
from UM.i18n import i18nCatalog
|
||||
from UM.Math.Color import Color
|
||||
from UM.Math.Polygon import Polygon
|
||||
from cura.Scene.SliceableObjectDecorator import SliceableObjectDecorator
|
||||
|
||||
from .PaintStrokeCommand import PaintStrokeCommand
|
||||
from .PaintClearCommand import PaintClearCommand
|
||||
|
|
@ -242,7 +243,14 @@ class PaintView(CuraView):
|
|||
stroke_path,
|
||||
set_value,
|
||||
self._current_bits_ranges,
|
||||
merge_with_previous))
|
||||
merge_with_previous,
|
||||
self._getSliceableObjectDecorator()))
|
||||
|
||||
def _getSliceableObjectDecorator(self) -> Optional[SliceableObjectDecorator]:
|
||||
if self._painted_object is None or self._current_paint_type != "extruder":
|
||||
return None
|
||||
|
||||
return self._painted_object.getDecorator(SliceableObjectDecorator)
|
||||
|
||||
def _makeClearCommand(self) -> Optional[PaintClearCommand]:
|
||||
if self._painted_object is None or self._paint_texture is None or self._current_bits_ranges is None:
|
||||
|
|
@ -254,7 +262,10 @@ class PaintView(CuraView):
|
|||
if extruder_stack is not None:
|
||||
set_value = extruder_stack.getValue("extruder_nr")
|
||||
|
||||
return PaintClearCommand(self._paint_texture, self._current_bits_ranges, set_value)
|
||||
return PaintClearCommand(self._paint_texture,
|
||||
self._current_bits_ranges,
|
||||
set_value,
|
||||
self._getSliceableObjectDecorator())
|
||||
|
||||
def clearPaint(self):
|
||||
self._prepareDataMapping()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue