From d315e0a727e345f52e4208f2c5ee5217b8c586b7 Mon Sep 17 00:00:00 2001 From: Erwan MATHIEU Date: Mon, 6 Oct 2025 12:31:16 +0200 Subject: [PATCH 1/5] Handle group selection as multi-selection w.r.t painting CURA-12761 --- plugins/PaintTool/PaintTool.py | 26 +++++++++++++------------- plugins/PaintTool/PaintTool.qml | 2 +- plugins/PaintTool/PaintView.py | 16 ++++++++++++---- 3 files changed, 26 insertions(+), 18 deletions(-) diff --git a/plugins/PaintTool/PaintTool.py b/plugins/PaintTool/PaintTool.py index a943a5f748..48ff031f54 100644 --- a/plugins/PaintTool/PaintTool.py +++ b/plugins/PaintTool/PaintTool.py @@ -317,8 +317,8 @@ class PaintTool(Tool): """ super().event(event) - node = Selection.getSelectedObject(0) - if node is None: + painted_object = self._view.getPaintedObject() + if painted_object is None: return False # Make sure the displayed values are updated if the bounding box of the selected mesh(es) changes @@ -364,10 +364,10 @@ class PaintTool(Tool): if self._camera is None: return False - if node != self._node_cache: + if painted_object != self._node_cache: if self._node_cache is not None: self._node_cache.transformationChanged.disconnect(self._nodeTransformChanged) - self._node_cache = node + self._node_cache = painted_object self._node_cache.transformationChanged.connect(self._nodeTransformChanged) self._cache_dirty = True if self._cache_dirty: @@ -379,7 +379,7 @@ class PaintTool(Tool): face_id = self._faces_selection_pass.getFaceIdAtPosition(mouse_evt.x, mouse_evt.y) if face_id < 0 or face_id >= self._mesh_transformed_cache.getFaceCount(): if self._view.clearCursorStroke(): - self._updateScene(node) + self._updateScene(painted_object) return True return False @@ -408,7 +408,7 @@ class PaintTool(Tool): Logger.logException("e", "Error when adding paint stroke") self._last_world_coords = world_coords - self._updateScene(node) + self._updateScene(painted_object) return event_caught return False @@ -418,7 +418,7 @@ class PaintTool(Tool): def _updateScene(self, node: SceneNode = None): if node is None: - node = Selection.getSelectedObject(0) + node = self._view.getPaintedObject() if node is not None: if self._mouse_held: Application.getInstance().getController().getScene().sceneChanged.emit(node) @@ -430,18 +430,18 @@ class PaintTool(Tool): super()._onSelectionChanged() single_selection = len(Selection.getAllSelectedObjects()) == 1 - self.setActiveView("PaintTool" if single_selection else None) - self._view.setCurrentPaintedObject(Selection.getSelectedObject(0) if single_selection else None) + self._view.setPaintedObject(Selection.getSelectedObject(0) if single_selection else None) + self.setActiveView("PaintTool" if self._view.hasPaintedObject() else None) self._updateState() def _updateState(self): - if len(Selection.getAllSelectedObjects()) == 1 and self._controller.getActiveTool() == self: - selected_object = Selection.getSelectedObject(0) - if selected_object.callDecoration("getPaintTexture") is not None: + painted_object = self._view.getPaintedObject() + if painted_object is not None and self._controller.getActiveTool() == self: + if painted_object.callDecoration("getPaintTexture") is not None: new_state = PaintTool.Paint.State.READY else: new_state = PaintTool.Paint.State.PREPARING_MODEL - self._prepare_texture_job = PrepareTextureJob(selected_object) + self._prepare_texture_job = PrepareTextureJob(painted_object) self._prepare_texture_job.finished.connect(self._onPrepareTextureFinished) self._prepare_texture_job.start() else: diff --git a/plugins/PaintTool/PaintTool.qml b/plugins/PaintTool/PaintTool.qml index 8548c3c14e..2f96bffd24 100644 --- a/plugins/PaintTool/PaintTool.qml +++ b/plugins/PaintTool/PaintTool.qml @@ -327,7 +327,7 @@ Item UM.Label { anchors.fill: parent - text: catalog.i18nc("@label", "Select a single model to start painting") + text: catalog.i18nc("@label", "Select a single ungrouped model to start painting") verticalAlignment: Text.AlignVCenter horizontalAlignment: Text.AlignHCenter } diff --git a/plugins/PaintTool/PaintView.py b/plugins/PaintTool/PaintView.py index 13f6acff3c..6750c5b33e 100644 --- a/plugins/PaintTool/PaintView.py +++ b/plugins/PaintTool/PaintView.py @@ -61,7 +61,7 @@ class PaintView(CuraView): canUndoChanged = pyqtSignal(bool) canRedoChanged = pyqtSignal(bool) - def setCurrentPaintedObject(self, current_painted_object: Optional[SceneNode]): + def setPaintedObject(self, painted_object: Optional[SceneNode]): if self._painted_object is not None: texture_changed_signal = self._painted_object.callDecoration("getPaintTextureChangedSignal") texture_changed_signal.disconnect(self._onCurrentPaintedObjectTextureChanged) @@ -69,15 +69,23 @@ class PaintView(CuraView): self._paint_texture = None self._cursor_texture = None - self._painted_object = current_painted_object + self._painted_object = None - if self._painted_object is not None: + if painted_object is not None and painted_object.callDecoration("isSliceable"): + self._painted_object = painted_object texture_changed_signal = self._painted_object.callDecoration("getPaintTextureChangedSignal") - texture_changed_signal.connect(self._onCurrentPaintedObjectTextureChanged) + if texture_changed_signal is not None: + texture_changed_signal.connect(self._onCurrentPaintedObjectTextureChanged) self._onCurrentPaintedObjectTextureChanged() self._updateCurrentBitsRanges() + def getPaintedObject(self) -> Optional[SceneNode]: + return self._painted_object + + def hasPaintedObject(self) -> bool: + return self._painted_object is not None + def _onCurrentPaintedObjectTextureChanged(self) -> None: paint_texture = self._painted_object.callDecoration("getPaintTexture") self._paint_texture = paint_texture From 52f571e70583cf81419fd97a53323d29cdd126b1 Mon Sep 17 00:00:00 2001 From: Erwan MATHIEU Date: Mon, 6 Oct 2025 13:35:52 +0200 Subject: [PATCH 2/5] Fix unable to slice when using paint-on-seam first CURA-12747 --- plugins/CuraEngineBackend/StartSliceJob.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/plugins/CuraEngineBackend/StartSliceJob.py b/plugins/CuraEngineBackend/StartSliceJob.py index 8f312a4afb..72a2a203e8 100644 --- a/plugins/CuraEngineBackend/StartSliceJob.py +++ b/plugins/CuraEngineBackend/StartSliceJob.py @@ -770,15 +770,18 @@ class StartSliceJob(Job): 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().copy() - image_ptr = texture_image.bits() + 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"] - image_array = (image_array << (32 - 1 - (bit_range_end - bit_range_start))) >> (32 - 1 - bit_range_end) - used_extruders = numpy.unique(image_array).tolist() + 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() # There is no relevant painting data, just take the extruder associated to the model if not used_extruders: From c04ca0dde598d49e16d4e32234635fe551f89025 Mon Sep 17 00:00:00 2001 From: Erwan MATHIEU Date: Tue, 7 Oct 2025 12:28:47 +0200 Subject: [PATCH 3/5] Restore initial text to avoid untranslated text CURA-12761 --- plugins/PaintTool/PaintTool.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/PaintTool/PaintTool.qml b/plugins/PaintTool/PaintTool.qml index 2f96bffd24..8548c3c14e 100644 --- a/plugins/PaintTool/PaintTool.qml +++ b/plugins/PaintTool/PaintTool.qml @@ -327,7 +327,7 @@ Item UM.Label { anchors.fill: parent - text: catalog.i18nc("@label", "Select a single ungrouped model to start painting") + text: catalog.i18nc("@label", "Select a single model to start painting") verticalAlignment: Text.AlignVCenter horizontalAlignment: Text.AlignHCenter } From b30a2407d6f714e09192e386a48f379155d4274c Mon Sep 17 00:00:00 2001 From: Erwan MATHIEU Date: Tue, 7 Oct 2025 16:40:01 +0200 Subject: [PATCH 4/5] Only display paint view in Prepare stage CURA-12750 --- plugins/PaintTool/PaintTool.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/plugins/PaintTool/PaintTool.py b/plugins/PaintTool/PaintTool.py index dc9245f4b5..ff2f196431 100644 --- a/plugins/PaintTool/PaintTool.py +++ b/plugins/PaintTool/PaintTool.py @@ -78,6 +78,7 @@ class PaintTool(Tool): self._controller.activeViewChanged.connect(self._updateIgnoreUnselectedObjects) self._controller.activeToolChanged.connect(self._updateState) + self._controller.activeStageChanged.connect(self._updateActiveView) self._camera: Optional[Camera] = None self._cam_pos: numpy.ndarray = numpy.array([0.0, 0.0, 0.0]) @@ -331,6 +332,9 @@ class PaintTool(Tool): if self._state != PaintTool.Paint.State.READY: return False + if self._controller.getActiveView() is not self._view: + return False + if event.type == Event.MouseReleaseEvent and self._controller.getToolsEnabled(): if MouseEvent.LeftButton not in cast(MouseEvent, event).buttons: return False @@ -433,14 +437,19 @@ class PaintTool(Tool): scene = self.getController().getScene() scene.sceneChanged.emit(scene.getRoot()) - def _onSelectionChanged(self): + def _onSelectionChanged(self) -> None: super()._onSelectionChanged() single_selection = len(Selection.getAllSelectedObjects()) == 1 self._view.setPaintedObject(Selection.getSelectedObject(0) if single_selection else None) - self.setActiveView("PaintTool" if self._view.hasPaintedObject() else None) + self._updateActiveView() self._updateState() + def _updateActiveView(self) -> None: + has_painted_object = self._view.hasPaintedObject() + stage_is_prepare = self._controller.getActiveStage().stageId == "PrepareStage" + self.setActiveView("PaintTool" if has_painted_object and stage_is_prepare else None) + def _updateState(self): painted_object = self._view.getPaintedObject() if painted_object is not None and self._controller.getActiveTool() == self: From a93f27f894d0f7a437cd35e38ba20a1cd58db746 Mon Sep 17 00:00:00 2001 From: THeijmans Date: Fri, 10 Oct 2025 14:42:24 +0200 Subject: [PATCH 5/5] PP-680 --- resources/definitions/ultimaker_s8.def.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/definitions/ultimaker_s8.def.json b/resources/definitions/ultimaker_s8.def.json index b7ee5524db..08d566e6e9 100644 --- a/resources/definitions/ultimaker_s8.def.json +++ b/resources/definitions/ultimaker_s8.def.json @@ -236,7 +236,7 @@ "flooring_layer_count": { "value": 1 }, "flooring_material_flow": { "value": "skin_material_flow * 110/93" }, "flooring_monotonic": { "value": false }, - "gradual_flow_discretisation_step_size": { "value": 1 }, + "gradual_flow_discretisation_step_size": { "value": 0.3 }, "gradual_flow_enabled": { "value": true }, "hole_xy_offset": { "value": 0.075 }, "infill_material_flow": { "value": "material_flow if infill_sparse_density < 95 else 95" },