From e3204707dbbc576984007994d769d33c526f520b Mon Sep 17 00:00:00 2001 From: Remco Burema Date: Wed, 1 Oct 2025 22:39:22 +0200 Subject: [PATCH 01/31] Move 'get painted on extruders' method, use for build-volume. (Not sure I'm happy with this, but) now we can use this _both_ in the slicing itself _and_ the bounds. The big downsides are a) I had to connect the scene changed signal to the on-stack-changed method, that seems ugly and potentially slow b) I'm not sure this method belongs in the ExtruderManager -- otoh, where else is it going to live (unless we want to make a new type of plugin-object?). CURA-12752 --- cura/BuildVolume.py | 3 ++- cura/Settings/ExtruderManager.py | 23 ++++++++++++++++++++++ plugins/CuraEngineBackend/StartSliceJob.py | 18 ++--------------- 3 files changed, 27 insertions(+), 17 deletions(-) diff --git a/cura/BuildVolume.py b/cura/BuildVolume.py index 18b762bb2c..1dd50b5a0d 100755 --- a/cura/BuildVolume.py +++ b/cura/BuildVolume.py @@ -117,6 +117,7 @@ class BuildVolume(SceneNode): self._has_errors = False self._application.getController().getScene().sceneChanged.connect(self._onSceneChanged) + self._application.getController().getScene().sceneChanged.connect(self._onStackChanged) # 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: diff --git a/cura/Settings/ExtruderManager.py b/cura/Settings/ExtruderManager.py index cd39947bf8..4be5c3121c 100755 --- a/cura/Settings/ExtruderManager.py +++ b/cura/Settings/ExtruderManager.py @@ -19,6 +19,8 @@ from cura.Settings.ExtruderStack import ExtruderStack from typing import Any, cast, Dict, List, Optional, TYPE_CHECKING, Union +import numpy + if TYPE_CHECKING: from cura.Settings.ExtruderStack import ExtruderStack @@ -196,6 +198,24 @@ class ExtruderManager(QObject): else: return value + @staticmethod + def getPaintedExtruders(node: "SceneNode") -> List[int]: + 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() + 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) + + return numpy.unique(image_array).tolist() + else: + return [] + def getUsedExtruderStacks(self) -> List["ExtruderStack"]: """Gets the extruder stacks that are actually being used at the moment. @@ -254,6 +274,9 @@ class ExtruderManager(QObject): if not support_roof_enabled: support_roof_enabled |= stack_to_use.getProperty("support_roof_enable", "value") + for extruder_nr in ExtruderManager.getPaintedExtruders(node): + 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", diff --git a/plugins/CuraEngineBackend/StartSliceJob.py b/plugins/CuraEngineBackend/StartSliceJob.py index 8f312a4afb..78f2f3054d 100644 --- a/plugins/CuraEngineBackend/StartSliceJob.py +++ b/plugins/CuraEngineBackend/StartSliceJob.py @@ -764,24 +764,10 @@ class StartSliceJob(Job): @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().copy() - image_ptr = texture_image.bits() - 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() + used_extruders = ExtruderManager.getInstance().getPaintedExtruders(node) # 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 \ No newline at end of file + return used_extruders From f910983616741f8a554cc9fdb831fd85c5f76ac2 Mon Sep 17 00:00:00 2001 From: Remco Burema Date: Wed, 1 Oct 2025 23:12:27 +0200 Subject: [PATCH 02/31] Spam the signals a bit less. done as part of CURA-12752 --- plugins/PaintTool/PaintTool.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/PaintTool/PaintTool.py b/plugins/PaintTool/PaintTool.py index 087119e3d6..a3acbb38b1 100644 --- a/plugins/PaintTool/PaintTool.py +++ b/plugins/PaintTool/PaintTool.py @@ -455,7 +455,7 @@ class PaintTool(Tool): Logger.logException("e", "Error when adding paint stroke") self._last_world_coords = world_coords - self._updateScene(node) + self._updateScene(node if event_caught else None) return event_caught return False From 9d52e5a2a6b2085b7736b7e225f1c8826353e021 Mon Sep 17 00:00:00 2001 From: Remco Burema Date: Wed, 1 Oct 2025 23:14:11 +0200 Subject: [PATCH 03/31] Fix ordering of texture-mappings. ... unless little vs. big endian-ness was the problem. shoved into CURA-12752 since I was in the code anyway. --- cura/Settings/ExtruderManager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cura/Settings/ExtruderManager.py b/cura/Settings/ExtruderManager.py index 4be5c3121c..9c12d6d92b 100755 --- a/cura/Settings/ExtruderManager.py +++ b/cura/Settings/ExtruderManager.py @@ -210,7 +210,7 @@ class ExtruderManager(QObject): 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) + image_array = (image_array << (32 - 1 - bit_range_end)) >> (32 - 1 - (bit_range_end - bit_range_start)) return numpy.unique(image_array).tolist() else: From ec38c8d4c67d1d47280c995270540144bc5ad7bd Mon Sep 17 00:00:00 2001 From: Erwan MATHIEU Date: Thu, 2 Oct 2025 14:15:45 +0200 Subject: [PATCH 04/31] Optimize function by not coping the image CURA-12752 --- cura/Settings/ExtruderManager.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/cura/Settings/ExtruderManager.py b/cura/Settings/ExtruderManager.py index 9c12d6d92b..bd8d956988 100755 --- a/cura/Settings/ExtruderManager.py +++ b/cura/Settings/ExtruderManager.py @@ -203,16 +203,17 @@ class ExtruderManager(QObject): 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)) >> (32 - 1 - (bit_range_end - bit_range_start)) + full_int32 = 0xffffffff + bit_mask = (((full_int32 << ( 32 - 1 - (bit_range_end - bit_range_start))) & full_int32) >> (32 - 1 - bit_range_end)) - return numpy.unique(image_array).tolist() + return (numpy.unique(image_array & bit_mask) >> bit_range_start).tolist() else: return [] From c412def982bd3188dc16eea273a51539b2943cd7 Mon Sep 17 00:00:00 2001 From: Remco Burema Date: Wed, 8 Oct 2025 09:58:05 +0200 Subject: [PATCH 05/31] Extruder counts in image was too slow, cache and do per bounding-rect. Rewrite the whole 'count pixels to get extruders for paint on materials' so that it's cached outside of the extruder manager instead, so that counting pixels in a 4096x4096 image isn't called xx of times per second. part of CURA-12752 --- cura/BuildVolume.py | 1 - cura/Scene/SliceableObjectDecorator.py | 22 ++++++++++++++++++++ cura/Settings/ExtruderManager.py | 17 +++------------- plugins/PaintTool/PaintClearCommand.py | 5 +++-- plugins/PaintTool/PaintCommand.py | 27 +++++++++++++++++++++++-- plugins/PaintTool/PaintStrokeCommand.py | 3 +++ plugins/PaintTool/PaintView.py | 13 +++++++++--- 7 files changed, 66 insertions(+), 22 deletions(-) diff --git a/cura/BuildVolume.py b/cura/BuildVolume.py index 1dd50b5a0d..929a1425b4 100755 --- a/cura/BuildVolume.py +++ b/cura/BuildVolume.py @@ -117,7 +117,6 @@ class BuildVolume(SceneNode): self._has_errors = False self._application.getController().getScene().sceneChanged.connect(self._onSceneChanged) - self._application.getController().getScene().sceneChanged.connect(self._onStackChanged) # Objects loaded at the moment. We are connected to the property changed events of these objects. self._scene_objects = set() # type: Set[SceneNode] diff --git a/cura/Scene/SliceableObjectDecorator.py b/cura/Scene/SliceableObjectDecorator.py index 7b4cbab3e5..eb33979dfe 100644 --- a/cura/Scene/SliceableObjectDecorator.py +++ b/cura/Scene/SliceableObjectDecorator.py @@ -1,3 +1,5 @@ +# Copyright (c) 2025 UltiMaker +# Cura is released under the terms of the LGPLv3 or higher. import copy import json @@ -18,6 +20,8 @@ class SliceableObjectDecorator(SceneNodeDecorator): self._paint_texture = None self._texture_data_mapping: Dict[str, tuple[int, int]] = {} + self._extruder_texel_counts: Dict[int, int] = {} + self.paintTextureChanged = Signal() def isSliceable(self) -> bool: @@ -29,8 +33,15 @@ class SliceableObjectDecorator(SceneNodeDecorator): def getPaintTextureChangedSignal(self) -> Signal: return self.paintTextureChanged + def _initTexelCounts(self) -> None: + if "extruder" in self._texture_data_mapping: + full_rect = self._paint_texture.getImage().rect() + bit_range = self._texture_data_mapping["extruder"] + self._extruder_texel_counts = self._paint_texture.getTexelCountsInRect(full_rect, bit_range) + def setPaintTexture(self, texture: Texture) -> None: self._paint_texture = texture + self._initTexelCounts() self.paintTextureChanged.emit() def getTextureDataMapping(self) -> Dict[str, tuple[int, int]]: @@ -38,12 +49,14 @@ class SliceableObjectDecorator(SceneNodeDecorator): def setTextureDataMapping(self, mapping: Dict[str, tuple[int, int]]) -> None: self._texture_data_mapping = mapping + self._initTexelCounts() def prepareTexture(self, width: int, height: int) -> None: if self._paint_texture is None: self._paint_texture = OpenGL.getInstance().createTexture(width, height) image = QImage(width, height, QImage.Format.Format_RGB32) image.fill(0) + self._extruder_texel_counts = {0: self._paint_texture.getWidth() * self._paint_texture.getHeight()} self._paint_texture.setImage(image) self.paintTextureChanged.emit() @@ -63,6 +76,15 @@ class SliceableObjectDecorator(SceneNodeDecorator): return texture_buffer.data() + def changeExtruderTexelCounts(self, texel_changes: Dict[int, int]) -> None: + for extruder_id, texel_count in texel_changes.items(): + if extruder_id not in self._extruder_texel_counts: + self._extruder_texel_counts[extruder_id] = 0 + self._extruder_texel_counts[extruder_id] += texel_count + + def getExtruderTexelCounts(self) -> Dict[int, int]: + return self._extruder_texel_counts + def __deepcopy__(self, memo) -> "SliceableObjectDecorator": copied_decorator = SliceableObjectDecorator() copied_decorator.setPaintTexture(copy.deepcopy(self.getPaintTexture())) diff --git a/cura/Settings/ExtruderManager.py b/cura/Settings/ExtruderManager.py index bd8d956988..a3b91d2fbe 100755 --- a/cura/Settings/ExtruderManager.py +++ b/cura/Settings/ExtruderManager.py @@ -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. @@ -19,8 +19,6 @@ from cura.Settings.ExtruderStack import ExtruderStack from typing import Any, cast, Dict, List, Optional, TYPE_CHECKING, Union -import numpy - if TYPE_CHECKING: from cura.Settings.ExtruderStack import ExtruderStack @@ -203,17 +201,8 @@ class ExtruderManager(QObject): 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)) - - return (numpy.unique(image_array & bit_mask) >> bit_range_start).tolist() + texel_counts_per_extruder = node.callDecoration("getExtruderTexelCounts") + return [extruder_id for extruder_id, count in texel_counts_per_extruder.items() if count > 0] else: return [] diff --git a/plugins/PaintTool/PaintClearCommand.py b/plugins/PaintTool/PaintClearCommand.py index 1a7d95c98a..7e80b9571c 100644 --- a/plugins/PaintTool/PaintClearCommand.py +++ b/plugins/PaintTool/PaintClearCommand.py @@ -21,14 +21,15 @@ class PaintClearCommand(PaintCommand): return 1 def redo(self) -> None: - painter = self._makeClearedTexture() + texel_counts_before = self._countTexels() + 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._pushTexelDifference(texel_counts_before) self._texture.updateImagePart(self._bounding_rect) def mergeWith(self, command: QUndoCommand) -> bool: diff --git a/plugins/PaintTool/PaintCommand.py b/plugins/PaintTool/PaintCommand.py index 9dfae1d092..0fc57bbcc4 100644 --- a/plugins/PaintTool/PaintCommand.py +++ b/plugins/PaintTool/PaintCommand.py @@ -1,12 +1,13 @@ # 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.Scene.SliceableObjectDecorator import SliceableObjectDecorator class PaintCommand(QUndoCommand): @@ -22,6 +23,8 @@ class PaintCommand(QUndoCommand): self._original_texture_image = None self._bounding_rect = texture.getImage().rect() + self._texel_count_object: Optional[SliceableObjectDecorator] = None + if make_original_image: self._original_texture_image = self._texture.getImage().copy() painter = QPainter(self._original_texture_image) @@ -31,17 +34,37 @@ class PaintCommand(QUndoCommand): painter.fillRect(self._original_texture_image.rect(), QBrush(self._getBitRangeMask())) painter.end() + def enableTexelCounting(self, texel_count_object: Optional[SliceableObjectDecorator] = None): + self._texel_count_object = texel_count_object + def undo(self) -> None: if self._original_texture_image is None: return + texel_counts_before = self._countTexels() + painter = self._makeClearedTexture() painter.setCompositionMode(QPainter.CompositionMode.RasterOp_SourceOrDestination) painter.drawImage(0, 0, self._original_texture_image) painter.end() + self._pushTexelDifference(texel_counts_before) self._texture.updateImagePart(self._bounding_rect) + def _pushTexelDifference(self, texel_counts_before: Dict[int, int]) -> None: + if self._texel_count_object is None: + return + texel_counts_changed = {} + texel_counts_after = self._countTexels() + for bit in self._bit_range: + texel_counts_changed[bit] = texel_counts_after.get(bit, 0) - texel_counts_before.get(bit, 0) + self._texel_count_object.changeExtruderTexelCounts(texel_counts_changed) + + def _countTexels(self) -> Dict[int, int]: + if self._texel_count_object is None: + return {} + return self._texture.getTexelCountsInRect(self._bounding_rect, self._bit_range) + def _makeClearedTexture(self) -> QPainter: painter = QPainter(self._texture.getImage()) painter.setRenderHint(QPainter.RenderHint.Antialiasing, False) diff --git a/plugins/PaintTool/PaintStrokeCommand.py b/plugins/PaintTool/PaintStrokeCommand.py index 8d4a5c2dbd..570b8af70b 100644 --- a/plugins/PaintTool/PaintStrokeCommand.py +++ b/plugins/PaintTool/PaintStrokeCommand.py @@ -33,6 +33,8 @@ class PaintStrokeCommand(PaintCommand): return 0 def redo(self) -> None: + texel_counts_before = self._countTexels() + painter = self._makeClearedTexture() painter.setBrush(QBrush(self._set_value)) painter.setPen(QPen(painter.brush(), self.PEN_OVERLAP_WIDTH)) @@ -40,6 +42,7 @@ class PaintStrokeCommand(PaintCommand): painter.drawPath(self._makePainterPath()) painter.end() + self._pushTexelDifference(texel_counts_before) self._texture.updateImagePart(self._bounding_rect) def mergeWith(self, command: QUndoCommand) -> bool: diff --git a/plugins/PaintTool/PaintView.py b/plugins/PaintTool/PaintView.py index 13f6acff3c..032d843266 100644 --- a/plugins/PaintTool/PaintView.py +++ b/plugins/PaintTool/PaintView.py @@ -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 @@ -230,11 +231,14 @@ class PaintView(CuraView): return set_value = self._shiftTextureValue(self._paint_modes[self._current_paint_type][brush_color].value) - stack.push(PaintStrokeCommand(self._paint_texture, + res = PaintStrokeCommand(self._paint_texture, stroke_path, set_value, self._current_bits_ranges, - merge_with_previous)) + merge_with_previous) + if self._current_paint_type == "extruder": + res.enableTexelCounting(self._painted_object.getDecorator(SliceableObjectDecorator)) + stack.push(res) def _makeClearCommand(self) -> Optional[PaintClearCommand]: if self._painted_object is None or self._paint_texture is None or self._current_bits_ranges is None: @@ -246,7 +250,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) + res = PaintClearCommand(self._paint_texture, self._current_bits_ranges, set_value) + if self._current_paint_type == "extruder": + res.enableTexelCounting(self._painted_object.getDecorator(SliceableObjectDecorator)) + return res def clearPaint(self): self._prepareDataMapping() From a93f27f894d0f7a437cd35e38ba20a1cd58db746 Mon Sep 17 00:00:00 2001 From: THeijmans Date: Fri, 10 Oct 2025 14:42:24 +0200 Subject: [PATCH 06/31] 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" }, From 8d312325eee47b5a10eda305e3622df1b3e976c9 Mon Sep 17 00:00:00 2001 From: THeijmans Date: Fri, 10 Oct 2025 15:05:52 +0200 Subject: [PATCH 07/31] Hide seams on Factor 4 --- resources/definitions/ultimaker_factor4.def.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/resources/definitions/ultimaker_factor4.def.json b/resources/definitions/ultimaker_factor4.def.json index 2149116a03..a71c42c6d6 100644 --- a/resources/definitions/ultimaker_factor4.def.json +++ b/resources/definitions/ultimaker_factor4.def.json @@ -200,6 +200,7 @@ "value": "skin_material_flow" }, "roofing_monotonic": { "value": "True" }, + "seam_overhang_angle": { "value": 30 }, "skin_angles": { "value": "[-40, 50]" }, "skin_material_flow": { @@ -361,8 +362,9 @@ "maximum_value": "100", "value": "(wall_material_flow + roofing_material_flow) / 2" }, + "z_seam_corner": { "value": "'z_seam_corner_weighted'" }, "z_seam_position": { "value": "'backleft'" }, - "z_seam_type": { "value": "'back'" }, + "z_seam_type": { "value": "'sharpest_corner'" }, "zig_zaggify_infill": { "value": "True" } } } \ No newline at end of file From 0e5442d5706c55bffdc82677290621c27856628f Mon Sep 17 00:00:00 2001 From: Erwan MATHIEU Date: Mon, 13 Oct 2025 13:33:51 +0200 Subject: [PATCH 08/31] Fix extruders counting method CURA-12752 --- plugins/PaintTool/PaintCommand.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/PaintTool/PaintCommand.py b/plugins/PaintTool/PaintCommand.py index 0fc57bbcc4..fb26f04181 100644 --- a/plugins/PaintTool/PaintCommand.py +++ b/plugins/PaintTool/PaintCommand.py @@ -56,8 +56,8 @@ class PaintCommand(QUndoCommand): return texel_counts_changed = {} texel_counts_after = self._countTexels() - for bit in self._bit_range: - texel_counts_changed[bit] = texel_counts_after.get(bit, 0) - texel_counts_before.get(bit, 0) + for extruder_nr in range(2 ** (self._bit_range[1] - self._bit_range[0] + 1)): + texel_counts_changed[extruder_nr] = texel_counts_after.get(extruder_nr, 0) - texel_counts_before.get(extruder_nr, 0) self._texel_count_object.changeExtruderTexelCounts(texel_counts_changed) def _countTexels(self) -> Dict[int, int]: From 375f030c0915a7ee214d90d3521b3b094d409df7 Mon Sep 17 00:00:00 2001 From: Erwan MATHIEU Date: Mon, 13 Oct 2025 14:22:39 +0200 Subject: [PATCH 09/31] Update extruders count only when inactive CURA-12752 The previous method was not efficient enough in case of large models, where a single painting stroke can easily cover almost the whole texture (in bounding box). Reverted to the version where the whole texture is counted, but cached in the SliceableObjectDecorator and updated on timer so that it is not done during painting. --- cura/BuildVolume.py | 1 + cura/Scene/SliceableObjectDecorator.py | 52 ++++++++++++------- cura/Settings/ExtruderManager.py | 16 ++---- plugins/CuraEngineBackend/StartSliceJob.py | 2 +- .../MultiMaterialExtruderConverter.py | 2 + plugins/PaintTool/PaintClearCommand.py | 13 +++-- plugins/PaintTool/PaintCommand.py | 31 ++++------- plugins/PaintTool/PaintStrokeCommand.py | 10 ++-- plugins/PaintTool/PaintView.py | 22 ++++---- 9 files changed, 77 insertions(+), 72 deletions(-) diff --git a/cura/BuildVolume.py b/cura/BuildVolume.py index 929a1425b4..1dd50b5a0d 100755 --- a/cura/BuildVolume.py +++ b/cura/BuildVolume.py @@ -117,6 +117,7 @@ class BuildVolume(SceneNode): self._has_errors = False self._application.getController().getScene().sceneChanged.connect(self._onSceneChanged) + self._application.getController().getScene().sceneChanged.connect(self._onStackChanged) # Objects loaded at the moment. We are connected to the property changed events of these objects. self._scene_objects = set() # type: Set[SceneNode] diff --git a/cura/Scene/SliceableObjectDecorator.py b/cura/Scene/SliceableObjectDecorator.py index eb33979dfe..56b7ed555c 100644 --- a/cura/Scene/SliceableObjectDecorator.py +++ b/cura/Scene/SliceableObjectDecorator.py @@ -2,10 +2,11 @@ # 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 @@ -20,10 +21,15 @@ class SliceableObjectDecorator(SceneNodeDecorator): self._paint_texture = None self._texture_data_mapping: Dict[str, tuple[int, int]] = {} - self._extruder_texel_counts: Dict[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 @@ -33,15 +39,31 @@ class SliceableObjectDecorator(SceneNodeDecorator): def getPaintTextureChangedSignal(self) -> Signal: return self.paintTextureChanged - def _initTexelCounts(self) -> None: - if "extruder" in self._texture_data_mapping: - full_rect = self._paint_texture.getImage().rect() - bit_range = self._texture_data_mapping["extruder"] - self._extruder_texel_counts = self._paint_texture.getTexelCountsInRect(full_rect, bit_range) + 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] def setPaintTexture(self, texture: Texture) -> None: self._paint_texture = texture - self._initTexelCounts() self.paintTextureChanged.emit() def getTextureDataMapping(self) -> Dict[str, tuple[int, int]]: @@ -49,14 +71,12 @@ class SliceableObjectDecorator(SceneNodeDecorator): def setTextureDataMapping(self, mapping: Dict[str, tuple[int, int]]) -> None: self._texture_data_mapping = mapping - self._initTexelCounts() def prepareTexture(self, width: int, height: int) -> None: if self._paint_texture is None: self._paint_texture = OpenGL.getInstance().createTexture(width, height) image = QImage(width, height, QImage.Format.Format_RGB32) image.fill(0) - self._extruder_texel_counts = {0: self._paint_texture.getWidth() * self._paint_texture.getHeight()} self._paint_texture.setImage(image) self.paintTextureChanged.emit() @@ -76,14 +96,8 @@ class SliceableObjectDecorator(SceneNodeDecorator): return texture_buffer.data() - def changeExtruderTexelCounts(self, texel_changes: Dict[int, int]) -> None: - for extruder_id, texel_count in texel_changes.items(): - if extruder_id not in self._extruder_texel_counts: - self._extruder_texel_counts[extruder_id] = 0 - self._extruder_texel_counts[extruder_id] += texel_count - - def getExtruderTexelCounts(self) -> Dict[int, int]: - return self._extruder_texel_counts + def getPaintedExtruders(self) -> Optional[List[int]]: + return self._painted_extruders def __deepcopy__(self, memo) -> "SliceableObjectDecorator": copied_decorator = SliceableObjectDecorator() diff --git a/cura/Settings/ExtruderManager.py b/cura/Settings/ExtruderManager.py index a3b91d2fbe..8e950350b3 100755 --- a/cura/Settings/ExtruderManager.py +++ b/cura/Settings/ExtruderManager.py @@ -196,16 +196,6 @@ class ExtruderManager(QObject): else: return value - @staticmethod - def getPaintedExtruders(node: "SceneNode") -> List[int]: - 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: - texel_counts_per_extruder = node.callDecoration("getExtruderTexelCounts") - return [extruder_id for extruder_id, count in texel_counts_per_extruder.items() if count > 0] - else: - return [] - def getUsedExtruderStacks(self) -> List["ExtruderStack"]: """Gets the extruder stacks that are actually being used at the moment. @@ -264,8 +254,10 @@ class ExtruderManager(QObject): if not support_roof_enabled: support_roof_enabled |= stack_to_use.getProperty("support_roof_enable", "value") - for extruder_nr in ExtruderManager.getPaintedExtruders(node): - used_extruder_stack_ids.add(self.extruderIds[str(extruder_nr)]) + 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", diff --git a/plugins/CuraEngineBackend/StartSliceJob.py b/plugins/CuraEngineBackend/StartSliceJob.py index 78f2f3054d..7f8a069509 100644 --- a/plugins/CuraEngineBackend/StartSliceJob.py +++ b/plugins/CuraEngineBackend/StartSliceJob.py @@ -764,7 +764,7 @@ class StartSliceJob(Job): @staticmethod def _getUsedExtruders(node: SceneNode) -> List[int]: - used_extruders = ExtruderManager.getInstance().getPaintedExtruders(node) + used_extruders = node.callDecoration("getPaintedExtruders") # There is no relevant painting data, just take the extruder associated to the model if not used_extruders: diff --git a/plugins/PaintTool/MultiMaterialExtruderConverter.py b/plugins/PaintTool/MultiMaterialExtruderConverter.py index be1d0e05db..8dbf56893e 100644 --- a/plugins/PaintTool/MultiMaterialExtruderConverter.py +++ b/plugins/PaintTool/MultiMaterialExtruderConverter.py @@ -109,4 +109,6 @@ class MultiMaterialExtruderConverter: texture.updateImagePart(image.rect()) + node.callDecoration("setPaintedExtrudersCountDirty") + self.mainExtruderChanged.emit(node) diff --git a/plugins/PaintTool/PaintClearCommand.py b/plugins/PaintTool/PaintClearCommand.py index 7e80b9571c..2c9f3f216d 100644 --- a/plugins/PaintTool/PaintClearCommand.py +++ b/plugins/PaintTool/PaintClearCommand.py @@ -5,6 +5,7 @@ from typing import Optional from PyQt6.QtGui import QUndoCommand, QImage, QPainter, QBrush +from Scene.SliceableObjectDecorator import SliceableObjectDecorator from UM.View.GL.Texture import Texture from .PaintCommand import PaintCommand @@ -13,23 +14,25 @@ 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: return 1 def redo(self) -> None: - texel_counts_before = self._countTexels() - 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._pushTexelDifference(texel_counts_before) + self._setPaintedExtrudersCountDirty() self._texture.updateImagePart(self._bounding_rect) def mergeWith(self, command: QUndoCommand) -> bool: diff --git a/plugins/PaintTool/PaintCommand.py b/plugins/PaintTool/PaintCommand.py index fb26f04181..ad4afa7fa3 100644 --- a/plugins/PaintTool/PaintCommand.py +++ b/plugins/PaintTool/PaintCommand.py @@ -15,7 +15,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 @@ -23,7 +27,7 @@ class PaintCommand(QUndoCommand): self._original_texture_image = None self._bounding_rect = texture.getImage().rect() - self._texel_count_object: Optional[SliceableObjectDecorator] = None + self._sliceable_object_decorator: Optional[SliceableObjectDecorator] = sliceable_object_decorator if make_original_image: self._original_texture_image = self._texture.getImage().copy() @@ -34,36 +38,21 @@ class PaintCommand(QUndoCommand): painter.fillRect(self._original_texture_image.rect(), QBrush(self._getBitRangeMask())) painter.end() - def enableTexelCounting(self, texel_count_object: Optional[SliceableObjectDecorator] = None): - self._texel_count_object = texel_count_object - def undo(self) -> None: if self._original_texture_image is None: return - texel_counts_before = self._countTexels() - painter = self._makeClearedTexture() painter.setCompositionMode(QPainter.CompositionMode.RasterOp_SourceOrDestination) painter.drawImage(0, 0, self._original_texture_image) painter.end() - self._pushTexelDifference(texel_counts_before) + self._setPaintedExtrudersCountDirty() self._texture.updateImagePart(self._bounding_rect) - def _pushTexelDifference(self, texel_counts_before: Dict[int, int]) -> None: - if self._texel_count_object is None: - return - texel_counts_changed = {} - texel_counts_after = self._countTexels() - for extruder_nr in range(2 ** (self._bit_range[1] - self._bit_range[0] + 1)): - texel_counts_changed[extruder_nr] = texel_counts_after.get(extruder_nr, 0) - texel_counts_before.get(extruder_nr, 0) - self._texel_count_object.changeExtruderTexelCounts(texel_counts_changed) - - def _countTexels(self) -> Dict[int, int]: - if self._texel_count_object is None: - return {} - return self._texture.getTexelCountsInRect(self._bounding_rect, self._bit_range) + def _setPaintedExtrudersCountDirty(self) -> None: + if self._sliceable_object_decorator is not None: + self._sliceable_object_decorator.setPaintedExtrudersCountDirty() def _makeClearedTexture(self) -> QPainter: painter = QPainter(self._texture.getImage()) diff --git a/plugins/PaintTool/PaintStrokeCommand.py b/plugins/PaintTool/PaintStrokeCommand.py index 570b8af70b..fa995f2aac 100644 --- a/plugins/PaintTool/PaintStrokeCommand.py +++ b/plugins/PaintTool/PaintStrokeCommand.py @@ -7,6 +7,7 @@ import math from PyQt6.QtCore import QRect, QRectF, QPoint from PyQt6.QtGui import QUndoCommand, QImage, QPainter, QPainterPath, QPen, QBrush +from Scene.SliceableObjectDecorator import SliceableObjectDecorator from UM.View.GL.Texture import Texture from UM.Math.Polygon import Polygon @@ -22,8 +23,9 @@ class PaintStrokeCommand(PaintCommand): 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 @@ -33,8 +35,6 @@ class PaintStrokeCommand(PaintCommand): return 0 def redo(self) -> None: - texel_counts_before = self._countTexels() - painter = self._makeClearedTexture() painter.setBrush(QBrush(self._set_value)) painter.setPen(QPen(painter.brush(), self.PEN_OVERLAP_WIDTH)) @@ -42,7 +42,7 @@ class PaintStrokeCommand(PaintCommand): painter.drawPath(self._makePainterPath()) painter.end() - self._pushTexelDifference(texel_counts_before) + self._setPaintedExtrudersCountDirty() self._texture.updateImagePart(self._bounding_rect) def mergeWith(self, command: QUndoCommand) -> bool: diff --git a/plugins/PaintTool/PaintView.py b/plugins/PaintTool/PaintView.py index 032d843266..c40724c6cb 100644 --- a/plugins/PaintTool/PaintView.py +++ b/plugins/PaintTool/PaintView.py @@ -231,14 +231,18 @@ class PaintView(CuraView): return set_value = self._shiftTextureValue(self._paint_modes[self._current_paint_type][brush_color].value) - res = PaintStrokeCommand(self._paint_texture, + stack.push(PaintStrokeCommand(self._paint_texture, stroke_path, set_value, self._current_bits_ranges, - merge_with_previous) - if self._current_paint_type == "extruder": - res.enableTexelCounting(self._painted_object.getDecorator(SliceableObjectDecorator)) - stack.push(res) + 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: @@ -250,10 +254,10 @@ class PaintView(CuraView): if extruder_stack is not None: set_value = extruder_stack.getValue("extruder_nr") - res = PaintClearCommand(self._paint_texture, self._current_bits_ranges, set_value) - if self._current_paint_type == "extruder": - res.enableTexelCounting(self._painted_object.getDecorator(SliceableObjectDecorator)) - return res + return PaintClearCommand(self._paint_texture, + self._current_bits_ranges, + set_value, + self._getSliceableObjectDecorator()) def clearPaint(self): self._prepareDataMapping() From e0bd68e6fd860ccd04301f18ab14f5fdc3c8d00a Mon Sep 17 00:00:00 2001 From: HellAholic Date: Mon, 13 Oct 2025 15:47:37 +0200 Subject: [PATCH 10/31] apply the linter comment unnecessary override was removed for z_seam_corner ultimaker definition has the same value --- resources/definitions/ultimaker_factor4.def.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/resources/definitions/ultimaker_factor4.def.json b/resources/definitions/ultimaker_factor4.def.json index a71c42c6d6..633875f193 100644 --- a/resources/definitions/ultimaker_factor4.def.json +++ b/resources/definitions/ultimaker_factor4.def.json @@ -362,9 +362,8 @@ "maximum_value": "100", "value": "(wall_material_flow + roofing_material_flow) / 2" }, - "z_seam_corner": { "value": "'z_seam_corner_weighted'" }, "z_seam_position": { "value": "'backleft'" }, "z_seam_type": { "value": "'sharpest_corner'" }, "zig_zaggify_infill": { "value": "True" } } -} \ No newline at end of file +} From 3b18ae78fe1d9fb56783b63a74b920015ae9727e Mon Sep 17 00:00:00 2001 From: HellAholic <28710690+HellAholic@users.noreply.github.com> Date: Mon, 13 Oct 2025 13:49:18 +0000 Subject: [PATCH 11/31] Apply printer-linter format --- resources/definitions/ultimaker_factor4.def.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/definitions/ultimaker_factor4.def.json b/resources/definitions/ultimaker_factor4.def.json index 633875f193..07ec095cb5 100644 --- a/resources/definitions/ultimaker_factor4.def.json +++ b/resources/definitions/ultimaker_factor4.def.json @@ -366,4 +366,4 @@ "z_seam_type": { "value": "'sharpest_corner'" }, "zig_zaggify_infill": { "value": "True" } } -} +} \ No newline at end of file From 0396a782b721c4e5d3363a4be4ba459586f65812 Mon Sep 17 00:00:00 2001 From: Remco Burema <41987080+rburema@users.noreply.github.com> Date: Tue, 14 Oct 2025 11:35:28 +0200 Subject: [PATCH 12/31] Change from code-review. Easier to read. done as part or CURA-12752 Co-authored-by: Casper Lamboo --- cura/BuildVolume.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/cura/BuildVolume.py b/cura/BuildVolume.py index 1dd50b5a0d..9037320238 100755 --- a/cura/BuildVolume.py +++ b/cura/BuildVolume.py @@ -116,8 +116,9 @@ class BuildVolume(SceneNode): self._application.engineCreatedSignal.connect(self._onEngineCreated) self._has_errors = False - self._application.getController().getScene().sceneChanged.connect(self._onSceneChanged) - self._application.getController().getScene().sceneChanged.connect(self._onStackChanged) + scene = self._application.getController().getScene() + scene.sceneChanged.connect(self._onSceneChanged) + scene.sceneChanged.connect(self._onStackChanged) # Objects loaded at the moment. We are connected to the property changed events of these objects. self._scene_objects = set() # type: Set[SceneNode] From 0f18b5e32392225886866de77df69a43f68cfb37 Mon Sep 17 00:00:00 2001 From: Remco Burema Date: Tue, 14 Oct 2025 11:37:14 +0200 Subject: [PATCH 13/31] Rename method to better cover intended meaning. done as part of CURA-12752 --- plugins/CuraEngineBackend/StartSliceJob.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/CuraEngineBackend/StartSliceJob.py b/plugins/CuraEngineBackend/StartSliceJob.py index 7f8a069509..b376aaaf78 100644 --- a/plugins/CuraEngineBackend/StartSliceJob.py +++ b/plugins/CuraEngineBackend/StartSliceJob.py @@ -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,7 +763,7 @@ class StartSliceJob(Job): self._addRelations(relations_set, relation.target.relations) @staticmethod - def _getUsedExtruders(node: SceneNode) -> List[int]: + 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 From db904992311a418d045ec7eec464a8e87fe5697f Mon Sep 17 00:00:00 2001 From: Erwan MATHIEU Date: Thu, 5 Jun 2025 14:46:03 +0200 Subject: [PATCH 14/31] Add setting for roofing extension CURA-12446 --- resources/definitions/fdmprinter.def.json | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/resources/definitions/fdmprinter.def.json b/resources/definitions/fdmprinter.def.json index e633017431..8afba9b8bb 100644 --- a/resources/definitions/fdmprinter.def.json +++ b/resources/definitions/fdmprinter.def.json @@ -1542,6 +1542,19 @@ "enabled": "roofing_pattern != 'concentric' and roofing_layer_count > 0 and top_layers > 0", "limit_to_extruder": "roofing_extruder_nr", "settable_per_mesh": true + }, + "roofing_extension": + { + "label": "Top Surface Extension", + "description": "Determines how much the top surfaces are extended beneath overlapping surfaces. By adjusting this value, you can ensure that the outer edges of the top surfaces are concealed by the layers above, resulting in a better visual quality, particularly for models with curved surfaces.", + "type": "float", + "default_value": "0.4", + "value": "roofing_line_width * (wall_line_count_roofing + 1)", + "minimum_value": "0", + "maximum_value_warning": "roofing_line_width * 10", + "enabled": "roofing_layer_count > 0 and top_layers > 0", + "limit_to_extruder": "roofing_extruder_nr", + "settable_per_mesh": true } } }, From 5eeb10cd8c3dae6e64e5b4cd8270b9f8cb8c344d Mon Sep 17 00:00:00 2001 From: Erwan MATHIEU Date: Mon, 13 Oct 2025 08:55:46 +0200 Subject: [PATCH 15/31] Disable roofing extension by default CURA-12446 --- resources/definitions/fdmprinter.def.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/definitions/fdmprinter.def.json b/resources/definitions/fdmprinter.def.json index 8afba9b8bb..e8310a540a 100644 --- a/resources/definitions/fdmprinter.def.json +++ b/resources/definitions/fdmprinter.def.json @@ -1548,8 +1548,8 @@ "label": "Top Surface Extension", "description": "Determines how much the top surfaces are extended beneath overlapping surfaces. By adjusting this value, you can ensure that the outer edges of the top surfaces are concealed by the layers above, resulting in a better visual quality, particularly for models with curved surfaces.", "type": "float", - "default_value": "0.4", - "value": "roofing_line_width * (wall_line_count_roofing + 1)", + "default_value": "0", + "value": "0", "minimum_value": "0", "maximum_value_warning": "roofing_line_width * 10", "enabled": "roofing_layer_count > 0 and top_layers > 0", From c8528bd1af6168537a11ebc0fe7229a1b69047da Mon Sep 17 00:00:00 2001 From: HellAholic Date: Tue, 14 Oct 2025 18:53:28 +0200 Subject: [PATCH 16/31] add the roofing_extension setting to expert setting_visibility --- resources/setting_visibility/expert.cfg | 1 + 1 file changed, 1 insertion(+) diff --git a/resources/setting_visibility/expert.cfg b/resources/setting_visibility/expert.cfg index 1aaa6f999e..8f07f7a2f9 100644 --- a/resources/setting_visibility/expert.cfg +++ b/resources/setting_visibility/expert.cfg @@ -49,6 +49,7 @@ z_seam_corner z_seam_relative [top_bottom] +roofing_extension roofing_layer_count flooring_layer_count top_bottom_extruder_nr From d5fc04684afbbbfc39c1fb78944afec88ac3ae60 Mon Sep 17 00:00:00 2001 From: Remco Burema <41987080+rburema@users.noreply.github.com> Date: Wed, 15 Oct 2025 08:55:10 +0200 Subject: [PATCH 17/31] Apply suggestions from code review (imports). done as part of CURA-12752 Co-authored-by: HellAholic --- plugins/PaintTool/PaintClearCommand.py | 2 +- plugins/PaintTool/PaintStrokeCommand.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/PaintTool/PaintClearCommand.py b/plugins/PaintTool/PaintClearCommand.py index 2c9f3f216d..1fb5ce4467 100644 --- a/plugins/PaintTool/PaintClearCommand.py +++ b/plugins/PaintTool/PaintClearCommand.py @@ -5,7 +5,7 @@ from typing import Optional from PyQt6.QtGui import QUndoCommand, QImage, QPainter, QBrush -from Scene.SliceableObjectDecorator import SliceableObjectDecorator +from cura.Scene.SliceableObjectDecorator import SliceableObjectDecorator from UM.View.GL.Texture import Texture from .PaintCommand import PaintCommand diff --git a/plugins/PaintTool/PaintStrokeCommand.py b/plugins/PaintTool/PaintStrokeCommand.py index fa995f2aac..ec61864838 100644 --- a/plugins/PaintTool/PaintStrokeCommand.py +++ b/plugins/PaintTool/PaintStrokeCommand.py @@ -7,7 +7,7 @@ import math from PyQt6.QtCore import QRect, QRectF, QPoint from PyQt6.QtGui import QUndoCommand, QImage, QPainter, QPainterPath, QPen, QBrush -from Scene.SliceableObjectDecorator import SliceableObjectDecorator +from cura.Scene.SliceableObjectDecorator import SliceableObjectDecorator from UM.View.GL.Texture import Texture from UM.Math.Polygon import Polygon From f9c77f87307ab6b4c00fe259b7fff24933756023 Mon Sep 17 00:00:00 2001 From: Remco Burema Date: Wed, 15 Oct 2025 11:15:08 +0200 Subject: [PATCH 18/31] For multi-material painting; stack needs to be updated. ... because the extruders used for the current object can change (clear all bits of extruder #2 paint on a single object, which results in the object printed with extruder #1 only, which could result in the prime-tower needing to be gone -- or the other way around). The _previous_ way of doing that was just spamming the stack changes, but that gave other problems. part of CURA-12752 --- cura/BuildVolume.py | 1 - plugins/PaintTool/PaintClearCommand.py | 2 ++ plugins/PaintTool/PaintCommand.py | 6 ++++++ plugins/PaintTool/PaintStrokeCommand.py | 2 ++ 4 files changed, 10 insertions(+), 1 deletion(-) diff --git a/cura/BuildVolume.py b/cura/BuildVolume.py index 9037320238..e931b7c056 100755 --- a/cura/BuildVolume.py +++ b/cura/BuildVolume.py @@ -118,7 +118,6 @@ class BuildVolume(SceneNode): self._has_errors = False scene = self._application.getController().getScene() scene.sceneChanged.connect(self._onSceneChanged) - scene.sceneChanged.connect(self._onStackChanged) # Objects loaded at the moment. We are connected to the property changed events of these objects. self._scene_objects = set() # type: Set[SceneNode] diff --git a/plugins/PaintTool/PaintClearCommand.py b/plugins/PaintTool/PaintClearCommand.py index 1fb5ce4467..5b809d2c1f 100644 --- a/plugins/PaintTool/PaintClearCommand.py +++ b/plugins/PaintTool/PaintClearCommand.py @@ -35,6 +35,8 @@ class PaintClearCommand(PaintCommand): self._setPaintedExtrudersCountDirty() self._texture.updateImagePart(self._bounding_rect) + self._signalUpdated() + def mergeWith(self, command: QUndoCommand) -> bool: if not isinstance(command, PaintClearCommand): return False diff --git a/plugins/PaintTool/PaintCommand.py b/plugins/PaintTool/PaintCommand.py index ad4afa7fa3..a1dce6b7d0 100644 --- a/plugins/PaintTool/PaintCommand.py +++ b/plugins/PaintTool/PaintCommand.py @@ -7,6 +7,7 @@ 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 @@ -50,6 +51,8 @@ class PaintCommand(QUndoCommand): self._setPaintedExtrudersCountDirty() self._texture.updateImagePart(self._bounding_rect) + self._signalUpdated() + def _setPaintedExtrudersCountDirty(self) -> None: if self._sliceable_object_decorator is not None: self._sliceable_object_decorator.setPaintedExtrudersCountDirty() @@ -72,3 +75,6 @@ class PaintCommand(QUndoCommand): def _getBitRangeMask(self) -> int: return PaintCommand.getBitRangeMask(self._bit_range) + + def _signalUpdated(self): + CuraApplication.getInstance().globalContainerStackChanged.emit() diff --git a/plugins/PaintTool/PaintStrokeCommand.py b/plugins/PaintTool/PaintStrokeCommand.py index ec61864838..b71b98e942 100644 --- a/plugins/PaintTool/PaintStrokeCommand.py +++ b/plugins/PaintTool/PaintStrokeCommand.py @@ -45,6 +45,8 @@ class PaintStrokeCommand(PaintCommand): self._setPaintedExtrudersCountDirty() self._texture.updateImagePart(self._bounding_rect) + self._signalUpdated() + def mergeWith(self, command: QUndoCommand) -> bool: if not isinstance(command, PaintStrokeCommand): return False From 73b9bdc91b7a939d65127b589010bc47617c2026 Mon Sep 17 00:00:00 2001 From: Frederic Meeuwissen <13856291+Frederic98@users.noreply.github.com> Date: Wed, 15 Oct 2025 13:17:46 +0200 Subject: [PATCH 19/31] Constrain skin expand distance --- resources/definitions/fdmprinter.def.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/definitions/fdmprinter.def.json b/resources/definitions/fdmprinter.def.json index e633017431..a64461bac3 100644 --- a/resources/definitions/fdmprinter.def.json +++ b/resources/definitions/fdmprinter.def.json @@ -2053,7 +2053,7 @@ "unit": "mm", "type": "float", "default_value": 1, - "value": "wall_line_width_0 + (wall_line_count - 1) * wall_line_width_x", + "value": "max(-skin_preshrink, wall_line_width_0 + (wall_line_count - 1) * wall_line_width_x)", "minimum_value": "-skin_preshrink", "limit_to_extruder": "top_bottom_extruder_nr", "enabled": "top_layers > 0 or bottom_layers > 0", From a75b907fd47e31669646773498478e9b97b1170e Mon Sep 17 00:00:00 2001 From: Frederic Meeuwissen <13856291+Frederic98@users.noreply.github.com> Date: Wed, 15 Oct 2025 13:17:58 +0200 Subject: [PATCH 20/31] Set initial_bottom_layers when user changes top/bottom thickness --- 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 08d566e6e9..8f9683412f 100644 --- a/resources/definitions/ultimaker_s8.def.json +++ b/resources/definitions/ultimaker_s8.def.json @@ -244,7 +244,7 @@ "infill_pattern": { "value": "'zigzag' if infill_sparse_density > 50 else 'grid'" }, "infill_sparse_density": { "value": 15 }, "infill_wall_line_count": { "value": "1 if infill_sparse_density > 80 else 0" }, - "initial_bottom_layers": { "value": 2 }, + "initial_bottom_layers": { "value": "2 if extruderValueFromContainer(extruder_nr, 'bottom_layers', 2) == bottom_layers else bottom_layers" }, "jerk_flooring": { "maximum_value_warning": "machine_max_jerk_xy / 2", From 3ce1c779aa65d7af95086244613ebb9de12b8a21 Mon Sep 17 00:00:00 2001 From: Frederic Meeuwissen <13856291+Frederic98@users.noreply.github.com> Date: Wed, 15 Oct 2025 13:18:08 +0200 Subject: [PATCH 21/31] S8 engineering: initial_bottom_layers = bottom_layers --- .../um_s8_aa_plus_0.4_abs_0.2mm_engineering.inst.cfg | 1 + .../um_s8_aa_plus_0.4_cpe-plus_0.2mm_engineering.inst.cfg | 1 + .../um_s8_aa_plus_0.4_cpe_0.2mm_engineering.inst.cfg | 1 + .../ultimaker_s8/um_s8_aa_plus_0.4_pc_0.2mm_engineering.inst.cfg | 1 + .../um_s8_aa_plus_0.4_petg_0.2mm_engineering.inst.cfg | 1 + .../um_s8_aa_plus_0.4_pla_0.2mm_engineering.inst.cfg | 1 + .../um_s8_aa_plus_0.4_tough-pla_0.2mm_engineering.inst.cfg | 1 + .../um_s8_cc_plus_0.4_cpe-plus_0.2mm_engineering.inst.cfg | 1 + .../um_s8_cc_plus_0.4_nylon-cf-slide_0.2mm_engineering.inst.cfg | 1 + .../ultimaker_s8/um_s8_cc_plus_0.4_pc_0.2mm_engineering.inst.cfg | 1 + .../um_s8_cc_plus_0.4_petcf_0.2mm_engineering.inst.cfg | 1 + .../um_s8_cc_plus_0.6_nylon-cf-slide_0.2mm_engineering.inst.cfg | 1 + .../um_s8_cc_plus_0.6_petcf_0.2mm_engineering.inst.cfg | 1 + 13 files changed, 13 insertions(+) diff --git a/resources/intent/ultimaker_s8/um_s8_aa_plus_0.4_abs_0.2mm_engineering.inst.cfg b/resources/intent/ultimaker_s8/um_s8_aa_plus_0.4_abs_0.2mm_engineering.inst.cfg index 9e6032024b..fde2e098c1 100644 --- a/resources/intent/ultimaker_s8/um_s8_aa_plus_0.4_abs_0.2mm_engineering.inst.cfg +++ b/resources/intent/ultimaker_s8/um_s8_aa_plus_0.4_abs_0.2mm_engineering.inst.cfg @@ -14,6 +14,7 @@ variant = AA+ 0.4 [values] hole_xy_offset = 0.075 infill_sparse_density = 20 +initial_bottom_layers = =bottom_layers inset_direction = outside_in top_bottom_thickness = =wall_thickness wall_thickness = =line_width * 4 diff --git a/resources/intent/ultimaker_s8/um_s8_aa_plus_0.4_cpe-plus_0.2mm_engineering.inst.cfg b/resources/intent/ultimaker_s8/um_s8_aa_plus_0.4_cpe-plus_0.2mm_engineering.inst.cfg index 74a80d566c..505458f677 100644 --- a/resources/intent/ultimaker_s8/um_s8_aa_plus_0.4_cpe-plus_0.2mm_engineering.inst.cfg +++ b/resources/intent/ultimaker_s8/um_s8_aa_plus_0.4_cpe-plus_0.2mm_engineering.inst.cfg @@ -14,6 +14,7 @@ variant = AA+ 0.4 [values] hole_xy_offset = 0.075 infill_sparse_density = 20 +initial_bottom_layers = =bottom_layers inset_direction = outside_in top_bottom_thickness = =wall_thickness wall_thickness = =line_width * 4 diff --git a/resources/intent/ultimaker_s8/um_s8_aa_plus_0.4_cpe_0.2mm_engineering.inst.cfg b/resources/intent/ultimaker_s8/um_s8_aa_plus_0.4_cpe_0.2mm_engineering.inst.cfg index 33fd2b9d8b..f86e7d4ffb 100644 --- a/resources/intent/ultimaker_s8/um_s8_aa_plus_0.4_cpe_0.2mm_engineering.inst.cfg +++ b/resources/intent/ultimaker_s8/um_s8_aa_plus_0.4_cpe_0.2mm_engineering.inst.cfg @@ -14,6 +14,7 @@ variant = AA+ 0.4 [values] hole_xy_offset = 0.075 infill_sparse_density = 20 +initial_bottom_layers = =bottom_layers inset_direction = outside_in top_bottom_thickness = =wall_thickness wall_thickness = =line_width * 4 diff --git a/resources/intent/ultimaker_s8/um_s8_aa_plus_0.4_pc_0.2mm_engineering.inst.cfg b/resources/intent/ultimaker_s8/um_s8_aa_plus_0.4_pc_0.2mm_engineering.inst.cfg index 5c8d802225..a5d829d6d4 100644 --- a/resources/intent/ultimaker_s8/um_s8_aa_plus_0.4_pc_0.2mm_engineering.inst.cfg +++ b/resources/intent/ultimaker_s8/um_s8_aa_plus_0.4_pc_0.2mm_engineering.inst.cfg @@ -14,6 +14,7 @@ variant = AA+ 0.4 [values] hole_xy_offset = 0.075 infill_sparse_density = 20 +initial_bottom_layers = =bottom_layers inset_direction = outside_in top_bottom_thickness = =wall_thickness wall_thickness = =line_width * 4 diff --git a/resources/intent/ultimaker_s8/um_s8_aa_plus_0.4_petg_0.2mm_engineering.inst.cfg b/resources/intent/ultimaker_s8/um_s8_aa_plus_0.4_petg_0.2mm_engineering.inst.cfg index ecd6554ecb..2299d9e7b6 100644 --- a/resources/intent/ultimaker_s8/um_s8_aa_plus_0.4_petg_0.2mm_engineering.inst.cfg +++ b/resources/intent/ultimaker_s8/um_s8_aa_plus_0.4_petg_0.2mm_engineering.inst.cfg @@ -14,6 +14,7 @@ variant = AA+ 0.4 [values] hole_xy_offset = 0.075 infill_sparse_density = 20 +initial_bottom_layers = =bottom_layers inset_direction = outside_in top_bottom_thickness = =wall_thickness wall_thickness = =line_width * 4 diff --git a/resources/intent/ultimaker_s8/um_s8_aa_plus_0.4_pla_0.2mm_engineering.inst.cfg b/resources/intent/ultimaker_s8/um_s8_aa_plus_0.4_pla_0.2mm_engineering.inst.cfg index 11761903ee..71235046b5 100644 --- a/resources/intent/ultimaker_s8/um_s8_aa_plus_0.4_pla_0.2mm_engineering.inst.cfg +++ b/resources/intent/ultimaker_s8/um_s8_aa_plus_0.4_pla_0.2mm_engineering.inst.cfg @@ -14,6 +14,7 @@ variant = AA+ 0.4 [values] hole_xy_offset = 0.075 infill_sparse_density = 20 +initial_bottom_layers = =bottom_layers inset_direction = outside_in top_bottom_thickness = =wall_thickness wall_thickness = =line_width * 4 diff --git a/resources/intent/ultimaker_s8/um_s8_aa_plus_0.4_tough-pla_0.2mm_engineering.inst.cfg b/resources/intent/ultimaker_s8/um_s8_aa_plus_0.4_tough-pla_0.2mm_engineering.inst.cfg index 8e88841ec1..6499267f9f 100644 --- a/resources/intent/ultimaker_s8/um_s8_aa_plus_0.4_tough-pla_0.2mm_engineering.inst.cfg +++ b/resources/intent/ultimaker_s8/um_s8_aa_plus_0.4_tough-pla_0.2mm_engineering.inst.cfg @@ -14,6 +14,7 @@ variant = AA+ 0.4 [values] hole_xy_offset = 0.075 infill_sparse_density = 20 +initial_bottom_layers = =bottom_layers inset_direction = outside_in top_bottom_thickness = =wall_thickness wall_thickness = =line_width * 4 diff --git a/resources/intent/ultimaker_s8/um_s8_cc_plus_0.4_cpe-plus_0.2mm_engineering.inst.cfg b/resources/intent/ultimaker_s8/um_s8_cc_plus_0.4_cpe-plus_0.2mm_engineering.inst.cfg index aad54ac9d7..8eee54c478 100644 --- a/resources/intent/ultimaker_s8/um_s8_cc_plus_0.4_cpe-plus_0.2mm_engineering.inst.cfg +++ b/resources/intent/ultimaker_s8/um_s8_cc_plus_0.4_cpe-plus_0.2mm_engineering.inst.cfg @@ -15,6 +15,7 @@ variant = CC+ 0.4 [values] hole_xy_offset = 0.075 infill_sparse_density = 20 +initial_bottom_layers = =bottom_layers inset_direction = outside_in top_bottom_thickness = =wall_thickness wall_thickness = =line_width * 4 diff --git a/resources/intent/ultimaker_s8/um_s8_cc_plus_0.4_nylon-cf-slide_0.2mm_engineering.inst.cfg b/resources/intent/ultimaker_s8/um_s8_cc_plus_0.4_nylon-cf-slide_0.2mm_engineering.inst.cfg index 9892f6b555..409b670a7b 100644 --- a/resources/intent/ultimaker_s8/um_s8_cc_plus_0.4_nylon-cf-slide_0.2mm_engineering.inst.cfg +++ b/resources/intent/ultimaker_s8/um_s8_cc_plus_0.4_nylon-cf-slide_0.2mm_engineering.inst.cfg @@ -15,6 +15,7 @@ variant = CC+ 0.4 [values] hole_xy_offset = 0.075 infill_sparse_density = 20 +initial_bottom_layers = =bottom_layers inset_direction = outside_in top_bottom_thickness = =wall_thickness wall_thickness = =line_width * 4 diff --git a/resources/intent/ultimaker_s8/um_s8_cc_plus_0.4_pc_0.2mm_engineering.inst.cfg b/resources/intent/ultimaker_s8/um_s8_cc_plus_0.4_pc_0.2mm_engineering.inst.cfg index 18ff3a861b..cf0de659e6 100644 --- a/resources/intent/ultimaker_s8/um_s8_cc_plus_0.4_pc_0.2mm_engineering.inst.cfg +++ b/resources/intent/ultimaker_s8/um_s8_cc_plus_0.4_pc_0.2mm_engineering.inst.cfg @@ -15,6 +15,7 @@ variant = CC+ 0.4 [values] hole_xy_offset = 0.075 infill_sparse_density = 20 +initial_bottom_layers = =bottom_layers inset_direction = outside_in top_bottom_thickness = =wall_thickness wall_thickness = =line_width * 4 diff --git a/resources/intent/ultimaker_s8/um_s8_cc_plus_0.4_petcf_0.2mm_engineering.inst.cfg b/resources/intent/ultimaker_s8/um_s8_cc_plus_0.4_petcf_0.2mm_engineering.inst.cfg index f41bf80386..886ce200c6 100644 --- a/resources/intent/ultimaker_s8/um_s8_cc_plus_0.4_petcf_0.2mm_engineering.inst.cfg +++ b/resources/intent/ultimaker_s8/um_s8_cc_plus_0.4_petcf_0.2mm_engineering.inst.cfg @@ -15,6 +15,7 @@ variant = CC+ 0.4 [values] hole_xy_offset = 0.075 infill_sparse_density = 20 +initial_bottom_layers = =bottom_layers inset_direction = outside_in top_bottom_thickness = =wall_thickness wall_thickness = =line_width * 4 diff --git a/resources/intent/ultimaker_s8/um_s8_cc_plus_0.6_nylon-cf-slide_0.2mm_engineering.inst.cfg b/resources/intent/ultimaker_s8/um_s8_cc_plus_0.6_nylon-cf-slide_0.2mm_engineering.inst.cfg index cb43ac7cfa..f8909a1d9a 100644 --- a/resources/intent/ultimaker_s8/um_s8_cc_plus_0.6_nylon-cf-slide_0.2mm_engineering.inst.cfg +++ b/resources/intent/ultimaker_s8/um_s8_cc_plus_0.6_nylon-cf-slide_0.2mm_engineering.inst.cfg @@ -14,6 +14,7 @@ variant = CC+ 0.6 [values] hole_xy_offset = 0.075 infill_sparse_density = 20 +initial_bottom_layers = =bottom_layers inset_direction = outside_in top_bottom_thickness = =wall_thickness wall_thickness = =line_width * 4 diff --git a/resources/intent/ultimaker_s8/um_s8_cc_plus_0.6_petcf_0.2mm_engineering.inst.cfg b/resources/intent/ultimaker_s8/um_s8_cc_plus_0.6_petcf_0.2mm_engineering.inst.cfg index 0ecb4c3a61..dcea5a265b 100644 --- a/resources/intent/ultimaker_s8/um_s8_cc_plus_0.6_petcf_0.2mm_engineering.inst.cfg +++ b/resources/intent/ultimaker_s8/um_s8_cc_plus_0.6_petcf_0.2mm_engineering.inst.cfg @@ -14,6 +14,7 @@ variant = CC+ 0.6 [values] hole_xy_offset = 0.075 infill_sparse_density = 20 +initial_bottom_layers = =bottom_layers inset_direction = outside_in top_bottom_thickness = =wall_thickness wall_thickness = =line_width * 4 From 06a75924839e56f431a1882f4e4c4924935613c1 Mon Sep 17 00:00:00 2001 From: Erwan MATHIEU Date: Wed, 15 Oct 2025 16:01:03 +0200 Subject: [PATCH 22/31] Do not update stacks at every stroke, but under the anti-bounce timer CURA-12752 --- cura/Scene/SliceableObjectDecorator.py | 3 +++ plugins/PaintTool/PaintClearCommand.py | 2 -- plugins/PaintTool/PaintCommand.py | 5 ----- plugins/PaintTool/PaintStrokeCommand.py | 2 -- 4 files changed, 3 insertions(+), 9 deletions(-) diff --git a/cura/Scene/SliceableObjectDecorator.py b/cura/Scene/SliceableObjectDecorator.py index 56b7ed555c..3e7d9901ad 100644 --- a/cura/Scene/SliceableObjectDecorator.py +++ b/cura/Scene/SliceableObjectDecorator.py @@ -62,6 +62,9 @@ class SliceableObjectDecorator(SceneNodeDecorator): 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() diff --git a/plugins/PaintTool/PaintClearCommand.py b/plugins/PaintTool/PaintClearCommand.py index 5b809d2c1f..1fb5ce4467 100644 --- a/plugins/PaintTool/PaintClearCommand.py +++ b/plugins/PaintTool/PaintClearCommand.py @@ -35,8 +35,6 @@ class PaintClearCommand(PaintCommand): self._setPaintedExtrudersCountDirty() self._texture.updateImagePart(self._bounding_rect) - self._signalUpdated() - def mergeWith(self, command: QUndoCommand) -> bool: if not isinstance(command, PaintClearCommand): return False diff --git a/plugins/PaintTool/PaintCommand.py b/plugins/PaintTool/PaintCommand.py index a1dce6b7d0..c4596405cc 100644 --- a/plugins/PaintTool/PaintCommand.py +++ b/plugins/PaintTool/PaintCommand.py @@ -51,8 +51,6 @@ class PaintCommand(QUndoCommand): self._setPaintedExtrudersCountDirty() self._texture.updateImagePart(self._bounding_rect) - self._signalUpdated() - def _setPaintedExtrudersCountDirty(self) -> None: if self._sliceable_object_decorator is not None: self._sliceable_object_decorator.setPaintedExtrudersCountDirty() @@ -75,6 +73,3 @@ class PaintCommand(QUndoCommand): def _getBitRangeMask(self) -> int: return PaintCommand.getBitRangeMask(self._bit_range) - - def _signalUpdated(self): - CuraApplication.getInstance().globalContainerStackChanged.emit() diff --git a/plugins/PaintTool/PaintStrokeCommand.py b/plugins/PaintTool/PaintStrokeCommand.py index b71b98e942..ec61864838 100644 --- a/plugins/PaintTool/PaintStrokeCommand.py +++ b/plugins/PaintTool/PaintStrokeCommand.py @@ -45,8 +45,6 @@ class PaintStrokeCommand(PaintCommand): self._setPaintedExtrudersCountDirty() self._texture.updateImagePart(self._bounding_rect) - self._signalUpdated() - def mergeWith(self, command: QUndoCommand) -> bool: if not isinstance(command, PaintStrokeCommand): return False From a1411cebf34c81e22e27e8f531a9d4804979e06f Mon Sep 17 00:00:00 2001 From: THeijmans Date: Wed, 15 Oct 2025 16:12:22 +0200 Subject: [PATCH 23/31] PP-686 --- .../um_s8_aa_plus_0.4_abs_0.2mm_engineering.inst.cfg | 6 +++--- .../um_s8_aa_plus_0.4_cpe-plus_0.2mm_engineering.inst.cfg | 6 +++--- .../um_s8_aa_plus_0.4_cpe_0.2mm_engineering.inst.cfg | 6 +++--- .../um_s8_aa_plus_0.4_pc_0.2mm_engineering.inst.cfg | 6 +++--- .../um_s8_aa_plus_0.4_petg_0.2mm_engineering.inst.cfg | 6 +++--- .../um_s8_aa_plus_0.4_pla_0.2mm_engineering.inst.cfg | 6 +++--- .../um_s8_aa_plus_0.4_tough-pla_0.2mm_engineering.inst.cfg | 6 +++--- .../um_s8_cc_plus_0.4_cpe-plus_0.2mm_engineering.inst.cfg | 6 +++--- ...s8_cc_plus_0.4_nylon-cf-slide_0.2mm_engineering.inst.cfg | 6 +++--- .../um_s8_cc_plus_0.4_pc_0.2mm_engineering.inst.cfg | 6 +++--- .../um_s8_cc_plus_0.4_petcf_0.2mm_engineering.inst.cfg | 6 +++--- ...s8_cc_plus_0.6_nylon-cf-slide_0.2mm_engineering.inst.cfg | 6 +++--- .../um_s8_cc_plus_0.6_petcf_0.2mm_engineering.inst.cfg | 6 +++--- .../ultimaker_s8/um_s8_aa_plus_0.4_abs_0.2mm.inst.cfg | 1 - .../ultimaker_s8/um_s8_aa_plus_0.4_petg_0.2mm.inst.cfg | 1 - .../ultimaker_s8/um_s8_aa_plus_0.4_pla_0.15mm.inst.cfg | 1 - .../ultimaker_s8/um_s8_aa_plus_0.4_pla_0.1mm.inst.cfg | 1 - .../ultimaker_s8/um_s8_aa_plus_0.4_pla_0.2mm.inst.cfg | 1 - .../um_s8_aa_plus_0.4_tough-pla_0.15mm.inst.cfg | 1 - .../ultimaker_s8/um_s8_aa_plus_0.4_tough-pla_0.1mm.inst.cfg | 1 - .../ultimaker_s8/um_s8_aa_plus_0.4_tough-pla_0.2mm.inst.cfg | 1 - 21 files changed, 39 insertions(+), 47 deletions(-) diff --git a/resources/intent/ultimaker_s8/um_s8_aa_plus_0.4_abs_0.2mm_engineering.inst.cfg b/resources/intent/ultimaker_s8/um_s8_aa_plus_0.4_abs_0.2mm_engineering.inst.cfg index 9e6032024b..92fd4bbc0f 100644 --- a/resources/intent/ultimaker_s8/um_s8_aa_plus_0.4_abs_0.2mm_engineering.inst.cfg +++ b/resources/intent/ultimaker_s8/um_s8_aa_plus_0.4_abs_0.2mm_engineering.inst.cfg @@ -12,10 +12,10 @@ type = intent variant = AA+ 0.4 [values] -hole_xy_offset = 0.075 +hole_xy_offset = 0.1 infill_sparse_density = 20 -inset_direction = outside_in +inset_direction = inside_out top_bottom_thickness = =wall_thickness wall_thickness = =line_width * 4 -xy_offset = 0.075 +xy_offset = 0.025 diff --git a/resources/intent/ultimaker_s8/um_s8_aa_plus_0.4_cpe-plus_0.2mm_engineering.inst.cfg b/resources/intent/ultimaker_s8/um_s8_aa_plus_0.4_cpe-plus_0.2mm_engineering.inst.cfg index 74a80d566c..4fe009ff83 100644 --- a/resources/intent/ultimaker_s8/um_s8_aa_plus_0.4_cpe-plus_0.2mm_engineering.inst.cfg +++ b/resources/intent/ultimaker_s8/um_s8_aa_plus_0.4_cpe-plus_0.2mm_engineering.inst.cfg @@ -12,10 +12,10 @@ type = intent variant = AA+ 0.4 [values] -hole_xy_offset = 0.075 +hole_xy_offset = 0.1 infill_sparse_density = 20 -inset_direction = outside_in +inset_direction = inside_out top_bottom_thickness = =wall_thickness wall_thickness = =line_width * 4 -xy_offset = 0.075 +xy_offset = 0.025 diff --git a/resources/intent/ultimaker_s8/um_s8_aa_plus_0.4_cpe_0.2mm_engineering.inst.cfg b/resources/intent/ultimaker_s8/um_s8_aa_plus_0.4_cpe_0.2mm_engineering.inst.cfg index 33fd2b9d8b..6eeeb5e79b 100644 --- a/resources/intent/ultimaker_s8/um_s8_aa_plus_0.4_cpe_0.2mm_engineering.inst.cfg +++ b/resources/intent/ultimaker_s8/um_s8_aa_plus_0.4_cpe_0.2mm_engineering.inst.cfg @@ -12,10 +12,10 @@ type = intent variant = AA+ 0.4 [values] -hole_xy_offset = 0.075 +hole_xy_offset = 0.1 infill_sparse_density = 20 -inset_direction = outside_in +inset_direction = inside_out top_bottom_thickness = =wall_thickness wall_thickness = =line_width * 4 -xy_offset = 0.075 +xy_offset = 0.025 diff --git a/resources/intent/ultimaker_s8/um_s8_aa_plus_0.4_pc_0.2mm_engineering.inst.cfg b/resources/intent/ultimaker_s8/um_s8_aa_plus_0.4_pc_0.2mm_engineering.inst.cfg index 5c8d802225..3311a52015 100644 --- a/resources/intent/ultimaker_s8/um_s8_aa_plus_0.4_pc_0.2mm_engineering.inst.cfg +++ b/resources/intent/ultimaker_s8/um_s8_aa_plus_0.4_pc_0.2mm_engineering.inst.cfg @@ -12,10 +12,10 @@ type = intent variant = AA+ 0.4 [values] -hole_xy_offset = 0.075 +hole_xy_offset = 0.1 infill_sparse_density = 20 -inset_direction = outside_in +inset_direction = inside_out top_bottom_thickness = =wall_thickness wall_thickness = =line_width * 4 -xy_offset = 0.075 +xy_offset = 0.025 diff --git a/resources/intent/ultimaker_s8/um_s8_aa_plus_0.4_petg_0.2mm_engineering.inst.cfg b/resources/intent/ultimaker_s8/um_s8_aa_plus_0.4_petg_0.2mm_engineering.inst.cfg index ecd6554ecb..da321460ea 100644 --- a/resources/intent/ultimaker_s8/um_s8_aa_plus_0.4_petg_0.2mm_engineering.inst.cfg +++ b/resources/intent/ultimaker_s8/um_s8_aa_plus_0.4_petg_0.2mm_engineering.inst.cfg @@ -12,10 +12,10 @@ type = intent variant = AA+ 0.4 [values] -hole_xy_offset = 0.075 +hole_xy_offset = 0.1 infill_sparse_density = 20 -inset_direction = outside_in +inset_direction = inside_out top_bottom_thickness = =wall_thickness wall_thickness = =line_width * 4 -xy_offset = 0.075 +xy_offset = 0.025 diff --git a/resources/intent/ultimaker_s8/um_s8_aa_plus_0.4_pla_0.2mm_engineering.inst.cfg b/resources/intent/ultimaker_s8/um_s8_aa_plus_0.4_pla_0.2mm_engineering.inst.cfg index 11761903ee..cd28cfc2bf 100644 --- a/resources/intent/ultimaker_s8/um_s8_aa_plus_0.4_pla_0.2mm_engineering.inst.cfg +++ b/resources/intent/ultimaker_s8/um_s8_aa_plus_0.4_pla_0.2mm_engineering.inst.cfg @@ -12,10 +12,10 @@ type = intent variant = AA+ 0.4 [values] -hole_xy_offset = 0.075 +hole_xy_offset = 0.1 infill_sparse_density = 20 -inset_direction = outside_in +inset_direction = inside_out top_bottom_thickness = =wall_thickness wall_thickness = =line_width * 4 -xy_offset = 0.075 +xy_offset = 0.025 diff --git a/resources/intent/ultimaker_s8/um_s8_aa_plus_0.4_tough-pla_0.2mm_engineering.inst.cfg b/resources/intent/ultimaker_s8/um_s8_aa_plus_0.4_tough-pla_0.2mm_engineering.inst.cfg index 8e88841ec1..2fd590636e 100644 --- a/resources/intent/ultimaker_s8/um_s8_aa_plus_0.4_tough-pla_0.2mm_engineering.inst.cfg +++ b/resources/intent/ultimaker_s8/um_s8_aa_plus_0.4_tough-pla_0.2mm_engineering.inst.cfg @@ -12,10 +12,10 @@ type = intent variant = AA+ 0.4 [values] -hole_xy_offset = 0.075 +hole_xy_offset = 0.1 infill_sparse_density = 20 -inset_direction = outside_in +inset_direction = inside_out top_bottom_thickness = =wall_thickness wall_thickness = =line_width * 4 -xy_offset = 0.075 +xy_offset = 0.025 diff --git a/resources/intent/ultimaker_s8/um_s8_cc_plus_0.4_cpe-plus_0.2mm_engineering.inst.cfg b/resources/intent/ultimaker_s8/um_s8_cc_plus_0.4_cpe-plus_0.2mm_engineering.inst.cfg index aad54ac9d7..b03db1d681 100644 --- a/resources/intent/ultimaker_s8/um_s8_cc_plus_0.4_cpe-plus_0.2mm_engineering.inst.cfg +++ b/resources/intent/ultimaker_s8/um_s8_cc_plus_0.4_cpe-plus_0.2mm_engineering.inst.cfg @@ -13,10 +13,10 @@ type = intent variant = CC+ 0.4 [values] -hole_xy_offset = 0.075 +hole_xy_offset = 0.1 infill_sparse_density = 20 -inset_direction = outside_in +inset_direction = inside_out top_bottom_thickness = =wall_thickness wall_thickness = =line_width * 4 -xy_offset = 0.075 +xy_offset = 0.025 diff --git a/resources/intent/ultimaker_s8/um_s8_cc_plus_0.4_nylon-cf-slide_0.2mm_engineering.inst.cfg b/resources/intent/ultimaker_s8/um_s8_cc_plus_0.4_nylon-cf-slide_0.2mm_engineering.inst.cfg index 9892f6b555..8cd533437d 100644 --- a/resources/intent/ultimaker_s8/um_s8_cc_plus_0.4_nylon-cf-slide_0.2mm_engineering.inst.cfg +++ b/resources/intent/ultimaker_s8/um_s8_cc_plus_0.4_nylon-cf-slide_0.2mm_engineering.inst.cfg @@ -13,10 +13,10 @@ type = intent variant = CC+ 0.4 [values] -hole_xy_offset = 0.075 +hole_xy_offset = 0.1 infill_sparse_density = 20 -inset_direction = outside_in +inset_direction = inside_out top_bottom_thickness = =wall_thickness wall_thickness = =line_width * 4 -xy_offset = 0.075 +xy_offset = 0.025 diff --git a/resources/intent/ultimaker_s8/um_s8_cc_plus_0.4_pc_0.2mm_engineering.inst.cfg b/resources/intent/ultimaker_s8/um_s8_cc_plus_0.4_pc_0.2mm_engineering.inst.cfg index 18ff3a861b..8cd4b18930 100644 --- a/resources/intent/ultimaker_s8/um_s8_cc_plus_0.4_pc_0.2mm_engineering.inst.cfg +++ b/resources/intent/ultimaker_s8/um_s8_cc_plus_0.4_pc_0.2mm_engineering.inst.cfg @@ -13,10 +13,10 @@ type = intent variant = CC+ 0.4 [values] -hole_xy_offset = 0.075 +hole_xy_offset = 0.1 infill_sparse_density = 20 -inset_direction = outside_in +inset_direction = inside_out top_bottom_thickness = =wall_thickness wall_thickness = =line_width * 4 -xy_offset = 0.075 +xy_offset = 0.025 diff --git a/resources/intent/ultimaker_s8/um_s8_cc_plus_0.4_petcf_0.2mm_engineering.inst.cfg b/resources/intent/ultimaker_s8/um_s8_cc_plus_0.4_petcf_0.2mm_engineering.inst.cfg index f41bf80386..985f032ddc 100644 --- a/resources/intent/ultimaker_s8/um_s8_cc_plus_0.4_petcf_0.2mm_engineering.inst.cfg +++ b/resources/intent/ultimaker_s8/um_s8_cc_plus_0.4_petcf_0.2mm_engineering.inst.cfg @@ -13,10 +13,10 @@ type = intent variant = CC+ 0.4 [values] -hole_xy_offset = 0.075 +hole_xy_offset = 0.1 infill_sparse_density = 20 -inset_direction = outside_in +inset_direction = inside_out top_bottom_thickness = =wall_thickness wall_thickness = =line_width * 4 -xy_offset = 0.075 +xy_offset = 0.025 diff --git a/resources/intent/ultimaker_s8/um_s8_cc_plus_0.6_nylon-cf-slide_0.2mm_engineering.inst.cfg b/resources/intent/ultimaker_s8/um_s8_cc_plus_0.6_nylon-cf-slide_0.2mm_engineering.inst.cfg index cb43ac7cfa..18dc8fa339 100644 --- a/resources/intent/ultimaker_s8/um_s8_cc_plus_0.6_nylon-cf-slide_0.2mm_engineering.inst.cfg +++ b/resources/intent/ultimaker_s8/um_s8_cc_plus_0.6_nylon-cf-slide_0.2mm_engineering.inst.cfg @@ -12,10 +12,10 @@ type = intent variant = CC+ 0.6 [values] -hole_xy_offset = 0.075 +hole_xy_offset = 0.1 infill_sparse_density = 20 -inset_direction = outside_in +inset_direction = inside_out top_bottom_thickness = =wall_thickness wall_thickness = =line_width * 4 -xy_offset = 0.075 +xy_offset = 0.025 diff --git a/resources/intent/ultimaker_s8/um_s8_cc_plus_0.6_petcf_0.2mm_engineering.inst.cfg b/resources/intent/ultimaker_s8/um_s8_cc_plus_0.6_petcf_0.2mm_engineering.inst.cfg index 0ecb4c3a61..ea417f171f 100644 --- a/resources/intent/ultimaker_s8/um_s8_cc_plus_0.6_petcf_0.2mm_engineering.inst.cfg +++ b/resources/intent/ultimaker_s8/um_s8_cc_plus_0.6_petcf_0.2mm_engineering.inst.cfg @@ -12,10 +12,10 @@ type = intent variant = CC+ 0.6 [values] -hole_xy_offset = 0.075 +hole_xy_offset = 0.1 infill_sparse_density = 20 -inset_direction = outside_in +inset_direction = inside_out top_bottom_thickness = =wall_thickness wall_thickness = =line_width * 4 -xy_offset = 0.075 +xy_offset = 0.025 diff --git a/resources/quality/ultimaker_s8/um_s8_aa_plus_0.4_abs_0.2mm.inst.cfg b/resources/quality/ultimaker_s8/um_s8_aa_plus_0.4_abs_0.2mm.inst.cfg index dcb79ffbee..be7813a2dc 100644 --- a/resources/quality/ultimaker_s8/um_s8_aa_plus_0.4_abs_0.2mm.inst.cfg +++ b/resources/quality/ultimaker_s8/um_s8_aa_plus_0.4_abs_0.2mm.inst.cfg @@ -16,7 +16,6 @@ cool_min_layer_time = 4 cool_min_layer_time_fan_speed_max = 9 cool_min_temperature = =material_print_temperature - 20 hole_xy_offset = 0.1 -inset_direction = inside_out retraction_prime_speed = 15 speed_roofing = =speed_topbottom * 1/3 speed_wall_x = =speed_wall diff --git a/resources/quality/ultimaker_s8/um_s8_aa_plus_0.4_petg_0.2mm.inst.cfg b/resources/quality/ultimaker_s8/um_s8_aa_plus_0.4_petg_0.2mm.inst.cfg index fd3942a782..acf6ed24e0 100644 --- a/resources/quality/ultimaker_s8/um_s8_aa_plus_0.4_petg_0.2mm.inst.cfg +++ b/resources/quality/ultimaker_s8/um_s8_aa_plus_0.4_petg_0.2mm.inst.cfg @@ -14,7 +14,6 @@ weight = -2 [values] cool_min_layer_time = 4 hole_xy_offset = 0.1 -inset_direction = inside_out material_print_temperature = =default_material_print_temperature + 5 retraction_prime_speed = 15 speed_roofing = =speed_topbottom * 1/3 diff --git a/resources/quality/ultimaker_s8/um_s8_aa_plus_0.4_pla_0.15mm.inst.cfg b/resources/quality/ultimaker_s8/um_s8_aa_plus_0.4_pla_0.15mm.inst.cfg index 3807598f26..68e7629157 100644 --- a/resources/quality/ultimaker_s8/um_s8_aa_plus_0.4_pla_0.15mm.inst.cfg +++ b/resources/quality/ultimaker_s8/um_s8_aa_plus_0.4_pla_0.15mm.inst.cfg @@ -14,7 +14,6 @@ weight = -1 [values] cool_min_temperature = =material_print_temperature - 20 hole_xy_offset = 0.1 -inset_direction = inside_out material_final_print_temperature = =material_print_temperature - 15 material_initial_print_temperature = =material_print_temperature - 15 retraction_prime_speed = =retraction_speed diff --git a/resources/quality/ultimaker_s8/um_s8_aa_plus_0.4_pla_0.1mm.inst.cfg b/resources/quality/ultimaker_s8/um_s8_aa_plus_0.4_pla_0.1mm.inst.cfg index c047ded9c3..fde3aae9b0 100644 --- a/resources/quality/ultimaker_s8/um_s8_aa_plus_0.4_pla_0.1mm.inst.cfg +++ b/resources/quality/ultimaker_s8/um_s8_aa_plus_0.4_pla_0.1mm.inst.cfg @@ -14,7 +14,6 @@ weight = 0 [values] cool_min_temperature = =material_print_temperature - 20 hole_xy_offset = 0.1 -inset_direction = inside_out material_final_print_temperature = =material_print_temperature - 15 material_initial_print_temperature = =material_print_temperature - 15 retraction_prime_speed = =retraction_speed diff --git a/resources/quality/ultimaker_s8/um_s8_aa_plus_0.4_pla_0.2mm.inst.cfg b/resources/quality/ultimaker_s8/um_s8_aa_plus_0.4_pla_0.2mm.inst.cfg index 580f05d0b7..47b96dd98c 100644 --- a/resources/quality/ultimaker_s8/um_s8_aa_plus_0.4_pla_0.2mm.inst.cfg +++ b/resources/quality/ultimaker_s8/um_s8_aa_plus_0.4_pla_0.2mm.inst.cfg @@ -14,7 +14,6 @@ weight = -2 [values] cool_min_temperature = =material_print_temperature - 20 hole_xy_offset = 0.1 -inset_direction = inside_out material_final_print_temperature = =material_print_temperature - 15 material_initial_print_temperature = =material_print_temperature - 15 retraction_prime_speed = =retraction_speed diff --git a/resources/quality/ultimaker_s8/um_s8_aa_plus_0.4_tough-pla_0.15mm.inst.cfg b/resources/quality/ultimaker_s8/um_s8_aa_plus_0.4_tough-pla_0.15mm.inst.cfg index 06d3c93cf3..4ac348f94b 100644 --- a/resources/quality/ultimaker_s8/um_s8_aa_plus_0.4_tough-pla_0.15mm.inst.cfg +++ b/resources/quality/ultimaker_s8/um_s8_aa_plus_0.4_tough-pla_0.15mm.inst.cfg @@ -14,7 +14,6 @@ weight = -1 [values] cool_min_temperature = =material_print_temperature - 20 hole_xy_offset = 0.1 -inset_direction = inside_out retraction_prime_speed = =retraction_speed retraction_speed = 25 speed_roofing = =speed_topbottom * 1/3 diff --git a/resources/quality/ultimaker_s8/um_s8_aa_plus_0.4_tough-pla_0.1mm.inst.cfg b/resources/quality/ultimaker_s8/um_s8_aa_plus_0.4_tough-pla_0.1mm.inst.cfg index 6099457369..b3d13edf4a 100644 --- a/resources/quality/ultimaker_s8/um_s8_aa_plus_0.4_tough-pla_0.1mm.inst.cfg +++ b/resources/quality/ultimaker_s8/um_s8_aa_plus_0.4_tough-pla_0.1mm.inst.cfg @@ -14,7 +14,6 @@ weight = 0 [values] cool_min_temperature = =material_print_temperature - 20 hole_xy_offset = 0.1 -inset_direction = inside_out retraction_prime_speed = =retraction_speed speed_roofing = =speed_topbottom * 1/3 speed_wall_x = =speed_wall diff --git a/resources/quality/ultimaker_s8/um_s8_aa_plus_0.4_tough-pla_0.2mm.inst.cfg b/resources/quality/ultimaker_s8/um_s8_aa_plus_0.4_tough-pla_0.2mm.inst.cfg index 373fa7b78e..346a658ad0 100644 --- a/resources/quality/ultimaker_s8/um_s8_aa_plus_0.4_tough-pla_0.2mm.inst.cfg +++ b/resources/quality/ultimaker_s8/um_s8_aa_plus_0.4_tough-pla_0.2mm.inst.cfg @@ -14,7 +14,6 @@ weight = -2 [values] cool_min_temperature = =material_print_temperature - 20 hole_xy_offset = 0.1 -inset_direction = inside_out retraction_prime_speed = =retraction_speed speed_roofing = =speed_topbottom * 1/3 speed_wall_x = =speed_wall From 33671083cdaf3c4e2dd5d58fef02bddeeaabbdd4 Mon Sep 17 00:00:00 2001 From: Erwan MATHIEU Date: Wed, 15 Oct 2025 16:43:44 +0200 Subject: [PATCH 24/31] Make sure undo stroke properly clears all the set pixels CURA-12752 Otherwise, when merging the polygons and undo-ing the whole stroke, there may be some remaining pixels outside the mesh triangles that would not be cleared, because the rasterizing is not 100% identical --- plugins/PaintTool/PaintClearCommand.py | 2 +- plugins/PaintTool/PaintCommand.py | 8 ++++---- plugins/PaintTool/PaintStrokeCommand.py | 5 +++-- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/plugins/PaintTool/PaintClearCommand.py b/plugins/PaintTool/PaintClearCommand.py index 1fb5ce4467..1be8b9bcbf 100644 --- a/plugins/PaintTool/PaintClearCommand.py +++ b/plugins/PaintTool/PaintClearCommand.py @@ -42,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())) \ No newline at end of file diff --git a/plugins/PaintTool/PaintCommand.py b/plugins/PaintTool/PaintCommand.py index c4596405cc..5367d0e0ca 100644 --- a/plugins/PaintTool/PaintCommand.py +++ b/plugins/PaintTool/PaintCommand.py @@ -43,7 +43,7 @@ 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() @@ -55,14 +55,14 @@ class PaintCommand(QUndoCommand): if self._sliceable_object_decorator is not None: self._sliceable_object_decorator.setPaintedExtrudersCountDirty() - def _makeClearedTexture(self) -> QPainter: + 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 diff --git a/plugins/PaintTool/PaintStrokeCommand.py b/plugins/PaintTool/PaintStrokeCommand.py index ec61864838..bcb38a19bc 100644 --- a/plugins/PaintTool/PaintStrokeCommand.py +++ b/plugins/PaintTool/PaintStrokeCommand.py @@ -17,6 +17,7 @@ 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, @@ -58,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()) From b6b05f672fdb5e906e917c8522dbfee3e890e1c4 Mon Sep 17 00:00:00 2001 From: THeijmans Date: Thu, 16 Oct 2025 09:56:51 +0200 Subject: [PATCH 25/31] Top surface extension for high speed --- resources/definitions/ultimaker_s8.def.json | 5 +++-- resources/variants/ultimaker_s6_aa025.inst.cfg | 1 + resources/variants/ultimaker_s6_aa04.inst.cfg | 1 + resources/variants/ultimaker_s6_aa08.inst.cfg | 1 + resources/variants/ultimaker_s6_cc04.inst.cfg | 1 + resources/variants/ultimaker_s6_cc06.inst.cfg | 1 + resources/variants/ultimaker_s8_aa025.inst.cfg | 1 + resources/variants/ultimaker_s8_aa04.inst.cfg | 1 + resources/variants/ultimaker_s8_aa08.inst.cfg | 1 + resources/variants/ultimaker_s8_cc04.inst.cfg | 1 + resources/variants/ultimaker_s8_cc06.inst.cfg | 1 + 11 files changed, 13 insertions(+), 2 deletions(-) diff --git a/resources/definitions/ultimaker_s8.def.json b/resources/definitions/ultimaker_s8.def.json index 8f9683412f..be49d8e88d 100644 --- a/resources/definitions/ultimaker_s8.def.json +++ b/resources/definitions/ultimaker_s8.def.json @@ -430,7 +430,7 @@ "material_print_temperature": { "maximum_value_warning": 320 }, "material_print_temperature_layer_0": { "maximum_value_warning": 320 }, "max_flow_acceleration": { "value": 1.5 }, - "max_skin_angle_for_expansion": { "value": 45 }, + "max_skin_angle_for_expansion": { "value": 90 }, "meshfix_maximum_resolution": { "value": 0.4 }, "min_infill_area": { "default_value": 10 }, "optimize_wall_printing_order": { "value": false }, @@ -447,7 +447,8 @@ "retraction_hop_enabled": { "value": true }, "retraction_min_travel": { "value": "2.5 if support_enable and support_structure=='tree' else line_width * 2.5" }, "retraction_prime_speed": { "value": 15 }, - "roofing_monotonic": { "value": false }, + "roofing_extension": { "value": 2 }, + "roofing_monotonic": { "value": true }, "roofing_pattern": { "value": "'lines'" }, "seam_overhang_angle": { "value": 35 }, "skin_edge_support_thickness": { "value": 0.8 }, diff --git a/resources/variants/ultimaker_s6_aa025.inst.cfg b/resources/variants/ultimaker_s6_aa025.inst.cfg index 95ac4eeb3b..e90473103b 100644 --- a/resources/variants/ultimaker_s6_aa025.inst.cfg +++ b/resources/variants/ultimaker_s6_aa025.inst.cfg @@ -112,6 +112,7 @@ retraction_hop_after_extruder_switch_height = =retraction_hop retraction_hop_enabled = =extruders_enabled_count > 1 retraction_min_travel = 5 retraction_prime_speed = =retraction_speed +roofing_extension = 0 roofing_monotonic = True roofing_pattern = =top_bottom_pattern seam_overhang_angle = =support_angle diff --git a/resources/variants/ultimaker_s6_aa04.inst.cfg b/resources/variants/ultimaker_s6_aa04.inst.cfg index ac227c8cd0..a12753fc41 100644 --- a/resources/variants/ultimaker_s6_aa04.inst.cfg +++ b/resources/variants/ultimaker_s6_aa04.inst.cfg @@ -112,6 +112,7 @@ retraction_hop_after_extruder_switch_height = =retraction_hop retraction_hop_enabled = =extruders_enabled_count > 1 retraction_min_travel = 5 retraction_prime_speed = 15 +roofing_extension = 0 roofing_monotonic = True roofing_pattern = =top_bottom_pattern seam_overhang_angle = =support_angle diff --git a/resources/variants/ultimaker_s6_aa08.inst.cfg b/resources/variants/ultimaker_s6_aa08.inst.cfg index 760ce661b9..2269c0cba2 100644 --- a/resources/variants/ultimaker_s6_aa08.inst.cfg +++ b/resources/variants/ultimaker_s6_aa08.inst.cfg @@ -123,6 +123,7 @@ retraction_hop_only_when_collides = True retraction_min_travel = 5 retraction_prime_speed = 15 retraction_speed = 25 +roofing_extension = 0 roofing_monotonic = True roofing_pattern = =top_bottom_pattern seam_overhang_angle = =support_angle diff --git a/resources/variants/ultimaker_s6_cc04.inst.cfg b/resources/variants/ultimaker_s6_cc04.inst.cfg index 37573be739..9baa9ceb0e 100644 --- a/resources/variants/ultimaker_s6_cc04.inst.cfg +++ b/resources/variants/ultimaker_s6_cc04.inst.cfg @@ -111,6 +111,7 @@ retraction_hop_after_extruder_switch_height = =retraction_hop retraction_hop_enabled = =extruders_enabled_count > 1 retraction_min_travel = 5 retraction_prime_speed = =retraction_speed +roofing_extension = 0 roofing_monotonic = True roofing_pattern = =top_bottom_pattern seam_overhang_angle = =support_angle diff --git a/resources/variants/ultimaker_s6_cc06.inst.cfg b/resources/variants/ultimaker_s6_cc06.inst.cfg index 595a181f63..4adcb69336 100644 --- a/resources/variants/ultimaker_s6_cc06.inst.cfg +++ b/resources/variants/ultimaker_s6_cc06.inst.cfg @@ -111,6 +111,7 @@ retraction_hop_after_extruder_switch_height = =retraction_hop retraction_hop_enabled = =extruders_enabled_count > 1 retraction_min_travel = 5 retraction_prime_speed = =retraction_speed +roofing_extension = 0 roofing_monotonic = True roofing_pattern = =top_bottom_pattern seam_overhang_angle = =support_angle diff --git a/resources/variants/ultimaker_s8_aa025.inst.cfg b/resources/variants/ultimaker_s8_aa025.inst.cfg index 96e31fbb04..883a551da8 100644 --- a/resources/variants/ultimaker_s8_aa025.inst.cfg +++ b/resources/variants/ultimaker_s8_aa025.inst.cfg @@ -112,6 +112,7 @@ retraction_hop_after_extruder_switch_height = =retraction_hop retraction_hop_enabled = =extruders_enabled_count > 1 retraction_min_travel = 5 retraction_prime_speed = =retraction_speed +roofing_extension = 0 roofing_monotonic = True roofing_pattern = =top_bottom_pattern seam_overhang_angle = =support_angle diff --git a/resources/variants/ultimaker_s8_aa04.inst.cfg b/resources/variants/ultimaker_s8_aa04.inst.cfg index 8e7df64b79..3a181f9c46 100644 --- a/resources/variants/ultimaker_s8_aa04.inst.cfg +++ b/resources/variants/ultimaker_s8_aa04.inst.cfg @@ -112,6 +112,7 @@ retraction_hop_after_extruder_switch_height = =retraction_hop retraction_hop_enabled = =extruders_enabled_count > 1 retraction_min_travel = 5 retraction_prime_speed = 15 +roofing_extension = 0 roofing_monotonic = True roofing_pattern = =top_bottom_pattern seam_overhang_angle = =support_angle diff --git a/resources/variants/ultimaker_s8_aa08.inst.cfg b/resources/variants/ultimaker_s8_aa08.inst.cfg index ca0e09485e..ebbaa15620 100644 --- a/resources/variants/ultimaker_s8_aa08.inst.cfg +++ b/resources/variants/ultimaker_s8_aa08.inst.cfg @@ -123,6 +123,7 @@ retraction_hop_only_when_collides = True retraction_min_travel = 5 retraction_prime_speed = 15 retraction_speed = 25 +roofing_extension = 0 roofing_monotonic = True roofing_pattern = =top_bottom_pattern seam_overhang_angle = =support_angle diff --git a/resources/variants/ultimaker_s8_cc04.inst.cfg b/resources/variants/ultimaker_s8_cc04.inst.cfg index 91a7186c0d..f0dbf19934 100644 --- a/resources/variants/ultimaker_s8_cc04.inst.cfg +++ b/resources/variants/ultimaker_s8_cc04.inst.cfg @@ -111,6 +111,7 @@ retraction_hop_after_extruder_switch_height = =retraction_hop retraction_hop_enabled = =extruders_enabled_count > 1 retraction_min_travel = 5 retraction_prime_speed = =retraction_speed +roofing_extension = 0 roofing_monotonic = True roofing_pattern = =top_bottom_pattern seam_overhang_angle = =support_angle diff --git a/resources/variants/ultimaker_s8_cc06.inst.cfg b/resources/variants/ultimaker_s8_cc06.inst.cfg index 3ccfe3e2d6..e81dfa1543 100644 --- a/resources/variants/ultimaker_s8_cc06.inst.cfg +++ b/resources/variants/ultimaker_s8_cc06.inst.cfg @@ -111,6 +111,7 @@ retraction_hop_after_extruder_switch_height = =retraction_hop retraction_hop_enabled = =extruders_enabled_count > 1 retraction_min_travel = 5 retraction_prime_speed = =retraction_speed +roofing_extension = 0 roofing_monotonic = True roofing_pattern = =top_bottom_pattern seam_overhang_angle = =support_angle From 18b83bf777b589125dd457a0dbda86be3f91537b Mon Sep 17 00:00:00 2001 From: HellAholic Date: Thu, 16 Oct 2025 10:45:13 +0200 Subject: [PATCH 26/31] linter fix and set formula for the roofing_extension --- resources/definitions/ultimaker_s8.def.json | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/resources/definitions/ultimaker_s8.def.json b/resources/definitions/ultimaker_s8.def.json index be49d8e88d..dee444bd4d 100644 --- a/resources/definitions/ultimaker_s8.def.json +++ b/resources/definitions/ultimaker_s8.def.json @@ -430,7 +430,6 @@ "material_print_temperature": { "maximum_value_warning": 320 }, "material_print_temperature_layer_0": { "maximum_value_warning": 320 }, "max_flow_acceleration": { "value": 1.5 }, - "max_skin_angle_for_expansion": { "value": 90 }, "meshfix_maximum_resolution": { "value": 0.4 }, "min_infill_area": { "default_value": 10 }, "optimize_wall_printing_order": { "value": false }, @@ -446,9 +445,7 @@ "retraction_hop_after_extruder_switch_height": { "value": 2 }, "retraction_hop_enabled": { "value": true }, "retraction_min_travel": { "value": "2.5 if support_enable and support_structure=='tree' else line_width * 2.5" }, - "retraction_prime_speed": { "value": 15 }, - "roofing_extension": { "value": 2 }, - "roofing_monotonic": { "value": true }, + "roofing_extension": { "value": "max(line_width * 5, 2)" }, "roofing_pattern": { "value": "'lines'" }, "seam_overhang_angle": { "value": 35 }, "skin_edge_support_thickness": { "value": 0.8 }, From 41b373e08e5957e2dc5d890d940930f28f62509e Mon Sep 17 00:00:00 2001 From: HellAholic Date: Thu, 16 Oct 2025 10:54:24 +0200 Subject: [PATCH 27/31] New formula to take into account the number of walls --- 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 dee444bd4d..13e1f95101 100644 --- a/resources/definitions/ultimaker_s8.def.json +++ b/resources/definitions/ultimaker_s8.def.json @@ -445,7 +445,7 @@ "retraction_hop_after_extruder_switch_height": { "value": 2 }, "retraction_hop_enabled": { "value": true }, "retraction_min_travel": { "value": "2.5 if support_enable and support_structure=='tree' else line_width * 2.5" }, - "roofing_extension": { "value": "max(line_width * 5, 2)" }, + "roofing_extension": { "value": "max(wall_line_width * (wall_line_count + 2), 2)" }, "roofing_pattern": { "value": "'lines'" }, "seam_overhang_angle": { "value": 35 }, "skin_edge_support_thickness": { "value": 0.8 }, From 75d65efb9ce8a34288b7621b6c2bf3f52799d2d6 Mon Sep 17 00:00:00 2001 From: Erwan MATHIEU Date: Thu, 16 Oct 2025 11:11:04 +0200 Subject: [PATCH 28/31] Use more generic formula CURA-12742 --- 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 13e1f95101..dfaff3f0f2 100644 --- a/resources/definitions/ultimaker_s8.def.json +++ b/resources/definitions/ultimaker_s8.def.json @@ -445,7 +445,7 @@ "retraction_hop_after_extruder_switch_height": { "value": 2 }, "retraction_hop_enabled": { "value": true }, "retraction_min_travel": { "value": "2.5 if support_enable and support_structure=='tree' else line_width * 2.5" }, - "roofing_extension": { "value": "max(wall_line_width * (wall_line_count + 2), 2)" }, + "roofing_extension": { "value": "max(wall_line_width_0 * min(1, wall_line_count) + wall_line_width_x * max(0, wall_line_count - 1) + wall_line_width * 2, 2)" }, "roofing_pattern": { "value": "'lines'" }, "seam_overhang_angle": { "value": 35 }, "skin_edge_support_thickness": { "value": 0.8 }, From b4ec456d141eb610b213ddcd120b20835e45fbad Mon Sep 17 00:00:00 2001 From: HellAholic Date: Thu, 16 Oct 2025 09:25:37 +0000 Subject: [PATCH 29/31] Set conan package version 5.11.0 --- conandata.yml | 14 +++++++------- resources/conandata.yml | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/conandata.yml b/conandata.yml index 5712e0b9cd..5500df2f22 100644 --- a/conandata.yml +++ b/conandata.yml @@ -1,16 +1,16 @@ -version: "5.11.0-beta.1" +version: "5.11.0" commit: "unknown" requirements: - - "cura_resources/5.11.0-beta.1" - - "uranium/5.11.0-beta.1" - - "curaengine/5.11.0-beta.1" - - "cura_binary_data/5.11.0-beta.1" - - "fdm_materials/5.11.0-beta.1" + - "cura_resources/5.11.0" + - "uranium/5.11.0" + - "curaengine/5.11.0" + - "cura_binary_data/5.11.0" + - "fdm_materials/5.11.0" - "dulcificum/5.10.0" - "pysavitar/5.11.0-alpha.0" - "pynest2d/5.10.0" requirements_internal: - - "fdm_materials/5.11.0-beta.1" + - "fdm_materials/5.11.0" - "cura_private_data/5.11.0-alpha.0@internal/testing" requirements_enterprise: - "native_cad_plugin/2.0.0" diff --git a/resources/conandata.yml b/resources/conandata.yml index f06b0d86a7..eeba833884 100644 --- a/resources/conandata.yml +++ b/resources/conandata.yml @@ -1 +1 @@ -version: "5.11.0-beta.1" +version: "5.11.0" From 9f73d6fc1dbc96f2f16aa289a06e2781c473bffa Mon Sep 17 00:00:00 2001 From: HellAholic Date: Thu, 16 Oct 2025 15:43:38 +0200 Subject: [PATCH 30/31] Update Ultimaker S8 definition with new parameters Added 'max_skin_angle_for_expansion' with a value of 45 and set 'roofing_extension' to 1.2 in the Ultimaker S8 printer definition. Allows us to have the benefits of the roofing extension feature while not getting the drawbacks from increased surface area where we don't want them --- resources/definitions/ultimaker_s8.def.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/resources/definitions/ultimaker_s8.def.json b/resources/definitions/ultimaker_s8.def.json index dfaff3f0f2..af70d73e5e 100644 --- a/resources/definitions/ultimaker_s8.def.json +++ b/resources/definitions/ultimaker_s8.def.json @@ -429,6 +429,7 @@ }, "material_print_temperature": { "maximum_value_warning": 320 }, "material_print_temperature_layer_0": { "maximum_value_warning": 320 }, + "max_skin_angle_for_expansion": { "value": 45 }, "max_flow_acceleration": { "value": 1.5 }, "meshfix_maximum_resolution": { "value": 0.4 }, "min_infill_area": { "default_value": 10 }, @@ -445,7 +446,7 @@ "retraction_hop_after_extruder_switch_height": { "value": 2 }, "retraction_hop_enabled": { "value": true }, "retraction_min_travel": { "value": "2.5 if support_enable and support_structure=='tree' else line_width * 2.5" }, - "roofing_extension": { "value": "max(wall_line_width_0 * min(1, wall_line_count) + wall_line_width_x * max(0, wall_line_count - 1) + wall_line_width * 2, 2)" }, + "roofing_extension": { "value": 1.2 }, "roofing_pattern": { "value": "'lines'" }, "seam_overhang_angle": { "value": 35 }, "skin_edge_support_thickness": { "value": 0.8 }, From 0d761eae105b412884eed14ca132010f209bb52e Mon Sep 17 00:00:00 2001 From: HellAholic <28710690+HellAholic@users.noreply.github.com> Date: Thu, 16 Oct 2025 13:44:47 +0000 Subject: [PATCH 31/31] Apply printer-linter format --- 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 af70d73e5e..1fc8c05628 100644 --- a/resources/definitions/ultimaker_s8.def.json +++ b/resources/definitions/ultimaker_s8.def.json @@ -429,8 +429,8 @@ }, "material_print_temperature": { "maximum_value_warning": 320 }, "material_print_temperature_layer_0": { "maximum_value_warning": 320 }, - "max_skin_angle_for_expansion": { "value": 45 }, "max_flow_acceleration": { "value": 1.5 }, + "max_skin_angle_for_expansion": { "value": 45 }, "meshfix_maximum_resolution": { "value": 0.4 }, "min_infill_area": { "default_value": 10 }, "optimize_wall_printing_order": { "value": false },