Merge remote-tracking branch 'origin/CURA-12543_painting_ux' into CURA-12528_unwrap_uvs

This commit is contained in:
Remco Burema 2025-06-19 15:03:33 +02:00
commit fbf3c1395d
13 changed files with 1180 additions and 56 deletions

View file

@ -1,6 +1,8 @@
import copy
from typing import Optional
from typing import Optional, Dict
from PyQt6.QtGui import QImage
import UM.View.GL.Texture
from UM.Scene.SceneNodeDecorator import SceneNodeDecorator
@ -16,6 +18,7 @@ class SliceableObjectDecorator(SceneNodeDecorator):
def __init__(self) -> None:
super().__init__()
self._paint_texture = None
self._texture_data_mapping: Dict[str, tuple[int, int]] = {}
def isSliceable(self) -> bool:
return True
@ -23,12 +26,22 @@ class SliceableObjectDecorator(SceneNodeDecorator):
def getPaintTexture(self, create_if_required: bool = True) -> Optional[UM.View.GL.Texture.Texture]:
if self._paint_texture is None and create_if_required:
self._paint_texture = OpenGL.getInstance().createTexture(TEXTURE_WIDTH, TEXTURE_HEIGHT)
image = QImage(TEXTURE_WIDTH, TEXTURE_HEIGHT, QImage.Format.Format_RGB32)
image.fill(0)
self._paint_texture.setImage(image)
return self._paint_texture
def setPaintTexture(self, texture: UM.View.GL.Texture) -> None:
self._paint_texture = texture
def getTextureDataMapping(self) -> Dict[str, tuple[int, int]]:
return self._texture_data_mapping
def setTextureDataMapping(self, mapping: Dict[str, tuple[int, int]]) -> None:
self._texture_data_mapping = mapping
def __deepcopy__(self, memo) -> "SliceableObjectDecorator":
copied_decorator = SliceableObjectDecorator()
copied_decorator.setPaintTexture(copy.deepcopy(self.getPaintTexture()))
copied_decorator.setTextureDataMapping(copy.deepcopy(self.getTextureDataMapping()))
return copied_decorator

View file

@ -39,14 +39,6 @@ class PaintTool(Tool):
self._mesh_transformed_cache = None
self._cache_dirty: bool = True
# TODO: Colors will need to be replaced on a 'per type of painting' basis.
self._color_str_to_rgba: Dict[str, List[int]] = {
"A": [192, 0, 192, 255],
"B": [232, 128, 0, 255],
"C": [0, 255, 0, 255],
"D": [255, 255, 255, 255],
}
self._brush_size: int = 10
self._brush_color: str = "A"
self._brush_shape: PaintTool.BrushShape = PaintTool.BrushShape.SQUARE
@ -61,8 +53,8 @@ class PaintTool(Tool):
def _createBrushPen(self) -> QPen:
pen = QPen()
pen.setWidth(self._brush_size)
color = self._color_str_to_rgba[self._brush_color]
pen.setColor(QColor(color[0], color[1], color[2], color[3]))
pen.setColor(Qt.GlobalColor.white)
match self._brush_shape:
case PaintTool.BrushShape.SQUARE:
pen.setCapStyle(Qt.PenCapStyle.SquareCap)
@ -78,8 +70,8 @@ class PaintTool(Tool):
start_x = int(min(x0, x1) - half_brush_size)
start_y = int(min(y0, y1) - half_brush_size)
stroke_image = QImage(abs(xdiff) + self._brush_size, abs(ydiff) + self._brush_size, QImage.Format.Format_RGBA8888)
stroke_image.fill(QColor(0,0,0,0))
stroke_image = QImage(abs(xdiff) + self._brush_size, abs(ydiff) + self._brush_size, QImage.Format.Format_RGB32)
stroke_image.fill(0)
painter = QPainter(stroke_image)
painter.setRenderHint(QPainter.RenderHint.Antialiasing, False)
@ -93,8 +85,14 @@ class PaintTool(Tool):
return stroke_image, (start_x, start_y)
def setPaintType(self, paint_type: str) -> None:
Logger.warning(f"TODO: Implement paint-types ({paint_type}).")
pass # FIXME: ... and also please call `self._brush_pen = self._createBrushPen()` (see other funcs).
paint_view = self._get_paint_view()
if paint_view is None:
return
paint_view.setPaintType(paint_type)
self._brush_pen = self._createBrushPen()
self._updateScene()
def setBrushSize(self, brush_size: float) -> None:
if brush_size != self._brush_size:
@ -102,9 +100,7 @@ class PaintTool(Tool):
self._brush_pen = self._createBrushPen()
def setBrushColor(self, brush_color: str) -> None:
if brush_color != self._brush_color:
self._brush_color = brush_color
self._brush_pen = self._createBrushPen()
self._brush_color = brush_color
def setBrushShape(self, brush_shape: int) -> None:
if brush_shape != self._brush_shape:
@ -112,19 +108,25 @@ class PaintTool(Tool):
self._brush_pen = self._createBrushPen()
def undoStackAction(self, redo_instead: bool) -> bool:
paintview = Application.getInstance().getController().getActiveView()
if paintview is None or paintview.getPluginId() != "PaintTool":
paint_view = self._get_paint_view()
if paint_view is None:
return False
paintview = cast(PaintView, paintview)
if redo_instead:
paintview.redoStroke()
paint_view.redoStroke()
else:
paintview.undoStroke()
node = Selection.getSelectedObject(0)
if node is not None:
Application.getInstance().getController().getScene().sceneChanged.emit(node)
paint_view.undoStroke()
self._updateScene()
return True
@staticmethod
def _get_paint_view() -> Optional[PaintView]:
paint_view = Application.getInstance().getController().getActiveView()
if paint_view is None or paint_view.getPluginId() != "PaintTool":
return None
return cast(PaintView, paint_view)
@staticmethod
def _get_intersect_ratio_via_pt(a: numpy.ndarray, pt: numpy.ndarray, b: numpy.ndarray, c: numpy.ndarray) -> float:
# compute the intersection of (param) A - pt with (param) B - (param) C
@ -314,12 +316,19 @@ class PaintTool(Tool):
end_coords[0] * w,
end_coords[1] * h
)
paintview.addStroke(sub_image, start_x, start_y)
paintview.addStroke(sub_image, start_x, start_y, self._brush_color)
self._last_text_coords = texcoords
self._last_mouse_coords = (mouse_evt.x, mouse_evt.y)
self._last_face_id = face_id
Application.getInstance().getController().getScene().sceneChanged.emit(node)
self._updateScene(node)
return True
return False
@staticmethod
def _updateScene(node: SceneNode = None):
if node is None:
node = Selection.getSelectedObject(0)
if node is not None:
Application.getInstance().getController().getScene().sceneChanged.emit(node)

View file

@ -2,10 +2,12 @@
# Cura is released under the terms of the LGPLv3 or higher.
import os
from typing import Optional, List, Tuple
from PyQt6.QtCore import QRect
from typing import Optional, List, Tuple, Dict
from PyQt6.QtGui import QImage, QColor, QPainter
from cura.CuraApplication import CuraApplication
from UM.PluginRegistry import PluginRegistry
from UM.View.GL.ShaderProgram import ShaderProgram
from UM.View.GL.Texture import Texture
@ -13,6 +15,7 @@ from UM.View.View import View
from UM.Scene.Selection import Selection
from UM.View.GL.OpenGL import OpenGL
from UM.i18n import i18nCatalog
from UM.Math.Color import Color
catalog = i18nCatalog("cura")
@ -22,10 +25,24 @@ class PaintView(View):
UNDO_STACK_SIZE = 1024
class PaintType:
def __init__(self, icon: str, display_color: Color, value: int):
self.icon: str = icon
self.display_color: Color = display_color
self.value: int = value
class PaintMode:
def __init__(self, icon: str, types: Dict[str, "PaintView.PaintType"]):
self.icon: str = icon
self.types = types
def __init__(self) -> None:
super().__init__()
self._paint_shader: Optional[ShaderProgram] = None
self._current_paint_texture: Optional[Texture] = None
self._current_bits_ranges: tuple[int, int] = (0, 0)
self._current_paint_type = ""
self._paint_modes: Dict[str, PaintView.PaintMode] = {}
self._stroke_undo_stack: List[Tuple[QImage, int, int]] = []
self._stroke_redo_stack: List[Tuple[QImage, int, int]] = []
@ -33,6 +50,18 @@ class PaintView(View):
self._force_opaque_mask = QImage(2, 2, QImage.Format.Format_Mono)
self._force_opaque_mask.fill(1)
CuraApplication.getInstance().engineCreatedSignal.connect(self._makePaintModes)
def _makePaintModes(self):
theme = CuraApplication.getInstance().getTheme()
usual_types = {"A": self.PaintType("Buildplate", Color(*theme.getColor("paint_normal_area").getRgb()), 0),
"B": self.PaintType("BlackMagic", Color(*theme.getColor("paint_preferred_area").getRgb()), 1),
"C": self.PaintType("Eye", Color(*theme.getColor("paint_avoid_area").getRgb()), 2)}
self._paint_modes = {
"A": self.PaintMode("MeshTypeNormal", usual_types),
"B": self.PaintMode("CircleOutline", usual_types),
}
def _checkSetup(self):
if not self._paint_shader:
shader_filename = os.path.join(PluginRegistry.getInstance().getPluginPath("PaintTool"), "paint.shader")
@ -49,14 +78,43 @@ class PaintView(View):
res.setAlphaChannel(self._force_opaque_mask.scaled(image.width(), image.height()))
return res
def addStroke(self, stroke_image: QImage, start_x: int, start_y: int) -> None:
if self._current_paint_texture is None:
def addStroke(self, stroke_image: QImage, start_x: int, start_y: int, brush_color: str) -> None:
if self._current_paint_texture is None or self._current_paint_texture.getImage() is None:
return
actual_image = self._current_paint_texture.getImage()
bit_range_start, bit_range_end = self._current_bits_ranges
set_value = self._paint_modes[self._current_paint_type].types[brush_color].value << self._current_bits_ranges[0]
full_int32 = 0xffffffff
clear_mask = full_int32 ^ (((full_int32 << (32 - 1 - (bit_range_end - bit_range_start))) & full_int32) >> (32 - 1 - bit_range_end))
image_rect = QRect(0, 0, stroke_image.width(), stroke_image.height())
clear_bits_image = stroke_image.copy()
clear_bits_image.invertPixels()
painter = QPainter(clear_bits_image)
painter.setCompositionMode(QPainter.CompositionMode.CompositionMode_Lighten)
painter.fillRect(image_rect, clear_mask)
painter.end()
set_value_image = stroke_image.copy()
painter = QPainter(set_value_image)
painter.setCompositionMode(QPainter.CompositionMode.CompositionMode_Multiply)
painter.fillRect(image_rect, set_value)
painter.end()
stroked_image = actual_image.copy(start_x, start_y, stroke_image.width(), stroke_image.height())
painter = QPainter(stroked_image)
painter.setCompositionMode(QPainter.CompositionMode.RasterOp_SourceAndDestination)
painter.drawImage(0, 0, clear_bits_image)
painter.setCompositionMode(QPainter.CompositionMode.RasterOp_SourceOrDestination)
painter.drawImage(0, 0, set_value_image)
painter.end()
self._stroke_redo_stack.clear()
if len(self._stroke_undo_stack) >= PaintView.UNDO_STACK_SIZE:
self._stroke_undo_stack.pop(0)
undo_image = self._forceOpaqueDeepCopy(self._current_paint_texture.setSubImage(stroke_image, start_x, start_y))
undo_image = self._forceOpaqueDeepCopy(self._current_paint_texture.setSubImage(stroked_image, start_x, start_y))
if undo_image is not None:
self._stroke_undo_stack.append((undo_image, start_x, start_y))
@ -83,6 +141,31 @@ class PaintView(View):
return self._current_paint_texture.getWidth(), self._current_paint_texture.getHeight()
return 0, 0
def setPaintType(self, paint_type: str) -> None:
node = Selection.getAllSelectedObjects()[0]
if node is None:
return
paint_data_mapping = node.callDecoration("getTextureDataMapping")
if paint_type not in paint_data_mapping:
new_mapping = self._add_mapping(paint_data_mapping, len(self._paint_modes[paint_type].types))
paint_data_mapping[paint_type] = new_mapping
node.callDecoration("setTextureDataMapping", paint_data_mapping)
self._current_paint_type = paint_type
self._current_bits_ranges = paint_data_mapping[paint_type]
@staticmethod
def _add_mapping(actual_mapping: Dict[str, tuple[int, int]], nb_storable_values: int) -> tuple[int, int]:
start_index = 0
if actual_mapping:
start_index = max(end_index for _, end_index in actual_mapping.values()) + 1
end_index = start_index + int.bit_length(nb_storable_values - 1) - 1
return start_index, end_index
def beginRendering(self) -> None:
renderer = self.getRenderer()
self._checkSetup()
@ -93,6 +176,17 @@ class PaintView(View):
if node is None:
return
if self._current_paint_type == "":
self.setPaintType("A")
self._paint_shader.setUniformValue("u_bitsRangesStart", self._current_bits_ranges[0])
self._paint_shader.setUniformValue("u_bitsRangesEnd", self._current_bits_ranges[1])
colors = [paint_type_obj.display_color for paint_type_obj in self._paint_modes[self._current_paint_type].types.values()]
colors_values = [[int(color_part * 255) for color_part in [color.r, color.g, color.b]] for color in colors]
self._paint_shader.setUniformValueArray("u_renderColors", colors_values)
self._current_paint_texture = node.callDecoration("getPaintTexture")
self._paint_shader.setTexture(0, self._current_paint_texture)
paint_batch.addItem(node.getWorldTransformation(copy=False), node.getMeshData(), normal_transformation=node.getCachedNormalMatrix())

View file

@ -27,11 +27,13 @@ vertex =
fragment =
uniform mediump vec4 u_ambientColor;
uniform mediump vec4 u_diffuseColor;
uniform highp vec3 u_lightPosition;
uniform highp vec3 u_viewPosition;
uniform mediump float u_opacity;
uniform sampler2D u_texture;
uniform mediump int u_bitsRangesStart;
uniform mediump int u_bitsRangesEnd;
uniform mediump vec3 u_renderColors[16];
varying highp vec3 v_vertex;
varying highp vec3 v_normal;
@ -48,15 +50,17 @@ fragment =
highp vec3 light_dir = normalize(u_lightPosition - v_vertex);
/* Diffuse Component */
ivec4 texture = ivec4(texture(u_texture, v_uvs) * 255.0);
uint color_index = (texture.r << 16) | (texture.g << 8) | texture.b;
color_index = (color_index << (32 - 1 - u_bitsRangesEnd)) >> 32 - 1 - (u_bitsRangesEnd - u_bitsRangesStart);
vec4 diffuse_color = vec4(u_renderColors[color_index] / 255.0, 1.0);
highp float n_dot_l = clamp(dot(normal, light_dir), 0.0, 1.0);
final_color += (n_dot_l * u_diffuseColor);
final_color += (n_dot_l * diffuse_color);
final_color.a = u_opacity;
lowp vec4 texture = texture2D(u_texture, v_uvs);
final_color = mix(final_color, texture, texture.a);
gl_FragColor = final_color;
frag_color = final_color;
}
vertex41core =
@ -89,11 +93,13 @@ vertex41core =
fragment41core =
#version 410
uniform mediump vec4 u_ambientColor;
uniform mediump vec4 u_diffuseColor;
uniform highp vec3 u_lightPosition;
uniform highp vec3 u_viewPosition;
uniform mediump float u_opacity;
uniform sampler2D u_texture;
uniform mediump int u_bitsRangesStart;
uniform mediump int u_bitsRangesEnd;
uniform mediump vec3 u_renderColors[16];
in highp vec3 v_vertex;
in highp vec3 v_normal;
@ -111,20 +117,21 @@ fragment41core =
highp vec3 light_dir = normalize(u_lightPosition - v_vertex);
/* Diffuse Component */
ivec4 texture = ivec4(texture(u_texture, v_uvs) * 255.0);
uint color_index = (texture.r << 16) | (texture.g << 8) | texture.b;
color_index = (color_index << (32 - 1 - u_bitsRangesEnd)) >> 32 - 1 - (u_bitsRangesEnd - u_bitsRangesStart);
vec4 diffuse_color = vec4(u_renderColors[color_index] / 255.0, 1.0);
highp float n_dot_l = clamp(dot(normal, light_dir), 0.0, 1.0);
final_color += (n_dot_l * u_diffuseColor);
final_color += (n_dot_l * diffuse_color);
final_color.a = u_opacity;
lowp vec4 texture = texture(u_texture, v_uvs);
final_color = mix(final_color, texture, texture.a);
frag_color = final_color;
}
[defaults]
u_ambientColor = [0.3, 0.3, 0.3, 1.0]
u_diffuseColor = [1.0, 1.0, 1.0, 1.0]
u_opacity = 0.5
u_texture = 0

View file

@ -103,8 +103,8 @@ geometry41core =
vec4 g_vertex_offset_vert;
vec3 g_vertex_normal_horz_head;
vec4 g_vertex_offset_horz_head;
vec3 g_axial_plan_vector;
vec3 g_radial_plan_vector;
vec3 g_axial_plane_vector;
vec3 g_radial_plane_vector;
float size_x;
float size_y;
@ -143,25 +143,25 @@ geometry41core =
if (g_vertex_delta.y == 0.0)
{
// vector is in the horizontal plan, radial vector is a simple rotation around Y axis
g_radial_plan_vector = vec3(g_vertex_delta.z, 0.0, -g_vertex_delta.x);
// vector is in the horizontal plane, radial vector is a simple rotation around Y axis
g_radial_plane_vector = vec3(g_vertex_delta.z, 0.0, -g_vertex_delta.x);
}
else if(g_vertex_delta.x == 0.0 && g_vertex_delta.z == 0.0)
{
// delta vector is purely vertical, display the line rotated vertically so that it is visible in front and side views
g_radial_plan_vector = vec3(1.0, 0.0, -1.0);
g_radial_plane_vector = vec3(1.0, 0.0, -1.0);
}
else
{
// delta vector is completely 3D
g_axial_plan_vector = vec3(g_vertex_delta.x, 0.0, g_vertex_delta.z); // Vector projected in the horizontal plan
g_radial_plan_vector = cross(g_vertex_delta, g_axial_plan_vector); // Radial vector in the horizontal plan, pointing right.
g_axial_plane_vector = vec3(g_vertex_delta.x, 0.0, g_vertex_delta.z); // Vector projected in the horizontal plane
g_radial_plane_vector = cross(g_vertex_delta, g_axial_plane_vector); // Radial vector in the horizontal plane, pointing right.
}
g_vertex_normal_horz_head = normalize(g_vertex_delta); //Lengthwise normal vector
g_vertex_offset_horz_head = vec4(g_vertex_normal_horz_head * size_x, 0.0); //Lengthwise offset vector
g_vertex_normal_horz = normalize(g_radial_plan_vector); //Normal vector pointing right.
g_vertex_normal_horz = normalize(g_radial_plane_vector); //Normal vector pointing right.
g_vertex_offset_horz = vec4(g_vertex_normal_horz * size_x, 0.0); //Offset vector pointing right.
g_vertex_normal_vert = vec3(0.0, 1.0, 0.0); //Upwards normal vector.

View file

@ -0,0 +1,15 @@
[general]
definition = ultimaker_s8
name = Extra Fine
version = 4
[metadata]
global_quality = True
quality_type = high
setting_version = 25
type = quality
weight = 1
[values]
layer_height = =round(0.06 * material_shrinkage_percentage_z / 100, 5)

View file

@ -0,0 +1,15 @@
[general]
definition = ultimaker_s8
name = Sprint
version = 4
[metadata]
global_quality = True
quality_type = superdraft
setting_version = 25
type = quality
weight = -4
[values]
layer_height = =round(0.4 * material_shrinkage_percentage_z / 100, 5)

View file

@ -1 +1,26 @@
{"metadata": {"name": "Colorblind Assist Dark", "inherits": "cura-dark"}, "colors": {"x_axis": [212, 0, 0, 255], "y_axis": [64, 64, 255, 255], "model_overhang": [200, 0, 255, 255], "xray": [26, 26, 62, 255], "xray_error": [255, 0, 0, 255], "layerview_inset_0": [255, 64, 0, 255], "layerview_inset_x": [0, 156, 128, 255], "layerview_skin": [255, 255, 86, 255], "layerview_support": [255, 255, 0, 255], "layerview_infill": [0, 255, 255, 255], "layerview_support_infill": [0, 200, 200, 255], "layerview_move_retraction": [0, 100, 255, 255], "main_window_header_background": [192, 199, 65, 255]}}
{
"metadata": {
"name": "Colorblind Assist Dark",
"inherits": "cura-dark"
},
"colors": {
"x_axis": [212, 0, 0, 255],
"y_axis": [64, 64, 255, 255],
"model_overhang": [200, 0, 255, 255],
"xray": [26, 26, 62, 255],
"xray_error": [255, 0, 0, 255],
"layerview_inset_0": [255, 64, 0, 255],
"layerview_inset_x": [0, 156, 128, 255],
"layerview_skin": [255, 255, 86, 255],
"layerview_support": [255, 255, 0, 255],
"layerview_infill": [0, 255, 255, 255],
"layerview_support_infill": [0, 200, 200, 255],
"layerview_move_retraction": [0, 100, 255, 255]
}
}

File diff suppressed because one or more lines are too long

View file

@ -1 +1,29 @@
{"metadata": {"name": "Colorblind Assist Light", "inherits": "cura-light"}, "colors": {"x_axis": [200, 0, 0, 255], "y_axis": [64, 64, 255, 255], "model_overhang": [200, 0, 255, 255], "model_selection_outline": [12, 169, 227, 255], "xray_error_dark": [255, 0, 0, 255], "xray_error_light": [255, 255, 0, 255], "xray": [26, 26, 62, 255], "xray_error": [255, 0, 0, 255], "layerview_inset_0": [255, 64, 0, 255], "layerview_inset_x": [0, 156, 128, 255], "layerview_skin": [255, 255, 86, 255], "layerview_support": [255, 255, 0, 255], "layerview_infill": [0, 255, 255, 255], "layerview_support_infill": [0, 200, 200, 255], "layerview_move_retraction": [0, 100, 255, 255], "main_window_header_background": [192, 199, 65, 255]}}
{
"metadata": {
"name": "Colorblind Assist Light",
"inherits": "cura-light"
},
"colors": {
"x_axis": [200, 0, 0, 255],
"y_axis": [64, 64, 255, 255],
"model_overhang": [200, 0, 255, 255],
"model_selection_outline": [12, 169, 227, 255],
"xray_error_dark": [255, 0, 0, 255],
"xray_error_light": [255, 255, 0, 255],
"xray": [26, 26, 62, 255],
"xray_error": [255, 0, 0, 255],
"layerview_inset_0": [255, 64, 0, 255],
"layerview_inset_x": [0, 156, 128, 255],
"layerview_skin": [255, 255, 86, 255],
"layerview_support": [255, 255, 0, 255],
"layerview_infill": [0, 255, 255, 255],
"layerview_support_infill": [0, 200, 200, 255],
"layerview_move_retraction": [0, 100, 255, 255]
}
}

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,16 @@
[
[ 62, 33, 55, 255],
[126, 196, 193, 255],
[126, 196, 193, 255],
[215, 155, 125, 255],
[228, 148, 58, 255],
[192, 199, 65, 255],
[157, 48, 59, 255],
[140, 143, 174, 255],
[ 23, 67, 75, 255],
[ 23, 67, 75, 255],
[154, 99, 72, 255],
[112, 55, 127, 255],
[100, 125, 52, 255],
[210, 100, 113, 255]
]

View file

@ -11,6 +11,7 @@ type = variant
[values]
machine_nozzle_heat_up_speed = 1.5
machine_nozzle_id = BB 0.4
machine_nozzle_size = 0.4
machine_nozzle_tip_outer_diameter = 1.0
retraction_amount = 4.5
support_bottom_height = =layer_height * 2