mirror of
https://git.mirrors.martin98.com/https://github.com/Ultimaker/Cura
synced 2025-08-18 02:25:54 +08:00
Painting UI work: Update image-part(s) instead of pixel(s) w.r.t. render-backend.
part of CURA-12543
This commit is contained in:
parent
704f9453f0
commit
109f37657b
@ -1,9 +1,9 @@
|
|||||||
# Copyright (c) 2025 UltiMaker
|
# Copyright (c) 2025 UltiMaker
|
||||||
# Cura is released under the terms of the LGPLv3 or higher.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
from copy import deepcopy
|
|
||||||
|
|
||||||
import numpy
|
import numpy
|
||||||
from PyQt6.QtCore import Qt
|
from PyQt6.QtCore import Qt
|
||||||
|
from PyQt6.QtGui import QImage, QPainter, QColor, QBrush
|
||||||
from typing import cast, Dict, List, Optional, Tuple
|
from typing import cast, Dict, List, Optional, Tuple
|
||||||
|
|
||||||
from UM.Application import Application
|
from UM.Application import Application
|
||||||
@ -12,9 +12,9 @@ from UM.Logger import Logger
|
|||||||
from UM.Scene.SceneNode import SceneNode
|
from UM.Scene.SceneNode import SceneNode
|
||||||
from UM.Scene.Selection import Selection
|
from UM.Scene.Selection import Selection
|
||||||
from UM.Tool import Tool
|
from UM.Tool import Tool
|
||||||
from UM.View.View import View
|
|
||||||
|
|
||||||
from cura.PickingPass import PickingPass
|
from cura.PickingPass import PickingPass
|
||||||
|
from .PaintView import PaintView
|
||||||
|
|
||||||
|
|
||||||
class PaintTool(Tool):
|
class PaintTool(Tool):
|
||||||
@ -42,22 +42,82 @@ class PaintTool(Tool):
|
|||||||
self._brush_size: int = 10
|
self._brush_size: int = 10
|
||||||
self._brush_color: str = "A"
|
self._brush_color: str = "A"
|
||||||
self._brush_shape: str = "A"
|
self._brush_shape: str = "A"
|
||||||
|
self._brush_image = self._createBrushImage()
|
||||||
|
|
||||||
self._mouse_held: bool = False
|
self._mouse_held: bool = False
|
||||||
self._last_mouse_drag: Optional[Tuple[int, int]] = None
|
self._last_text_coords: Optional[Tuple[int, int]] = None
|
||||||
|
|
||||||
|
def _createBrushImage(self) -> QImage:
|
||||||
|
brush_image = QImage(self._brush_size, self._brush_size, QImage.Format.Format_RGBA8888)
|
||||||
|
brush_image.fill(QColor(255,255,255,0))
|
||||||
|
|
||||||
|
color = self._color_str_to_rgba[self._brush_color]
|
||||||
|
qcolor = QColor(color[0], color[1], color[2], color[3])
|
||||||
|
|
||||||
|
painter = QPainter(brush_image)
|
||||||
|
painter.setRenderHint(QPainter.RenderHint.Antialiasing, False)
|
||||||
|
painter.setPen(Qt.PenStyle.NoPen)
|
||||||
|
painter.setBrush(QBrush(qcolor))
|
||||||
|
match self._brush_shape:
|
||||||
|
case "A": # Square brush
|
||||||
|
painter.drawRect(0, 0, self._brush_size, self._brush_size)
|
||||||
|
case "B": # Circle brush
|
||||||
|
painter.drawEllipse(0, 0, self._brush_size, self._brush_size)
|
||||||
|
case _:
|
||||||
|
painter.drawRect(0, 0, self._brush_size, self._brush_size)
|
||||||
|
painter.end()
|
||||||
|
|
||||||
|
return brush_image
|
||||||
|
|
||||||
|
def _createStrokeImage(self, x0: float, y0: float, x1: float, y1: float) -> Tuple[QImage, Tuple[int, int]]:
|
||||||
|
distance = numpy.hypot(x1 - x0, y1 - y0)
|
||||||
|
angle = numpy.arctan2(y1 - y0, x1 - x0)
|
||||||
|
stroke_width = self._brush_size
|
||||||
|
stroke_height = int(distance) + self._brush_size
|
||||||
|
|
||||||
|
half_brush_size = self._brush_size // 2
|
||||||
|
start_x = int(x0 - half_brush_size)
|
||||||
|
start_y = int(y0 - half_brush_size)
|
||||||
|
|
||||||
|
stroke_image = QImage(stroke_height, stroke_width, QImage.Format.Format_RGBA8888)
|
||||||
|
stroke_image.fill(QColor(255,255,255,0))
|
||||||
|
|
||||||
|
painter = QPainter(stroke_image)
|
||||||
|
painter.setRenderHint(QPainter.RenderHint.SmoothPixmapTransform)
|
||||||
|
painter.setRenderHint(QPainter.RenderHint.Antialiasing, False)
|
||||||
|
|
||||||
|
# rotate the brush-image to follow the stroke-direction
|
||||||
|
transform = painter.transform()
|
||||||
|
transform.translate(0, stroke_width / 2) # translate to match the brush-alignment
|
||||||
|
transform.rotate(-numpy.degrees(angle))
|
||||||
|
painter.setTransform(transform)
|
||||||
|
|
||||||
|
# tile the brush along the stroke-length
|
||||||
|
brush_stride = max(1, half_brush_size)
|
||||||
|
for i in range(0, int(distance) + brush_stride, brush_stride):
|
||||||
|
painter.drawImage(i, -stroke_width, self._brush_image)
|
||||||
|
painter.end()
|
||||||
|
|
||||||
|
return stroke_image, (start_x, start_y)
|
||||||
|
|
||||||
def setPaintType(self, paint_type: str) -> None:
|
def setPaintType(self, paint_type: str) -> None:
|
||||||
Logger.warning(f"TODO: Implement paint-types ({paint_type}).")
|
Logger.warning(f"TODO: Implement paint-types ({paint_type}).")
|
||||||
pass
|
pass # FIXME: ... and also please call `self._stroke_image = self._createBrushStrokeImage()` (see other funcs).
|
||||||
|
|
||||||
def setBrushSize(self, brush_size: float) -> None:
|
def setBrushSize(self, brush_size: float) -> None:
|
||||||
|
if brush_size != self._brush_size:
|
||||||
self._brush_size = int(brush_size)
|
self._brush_size = int(brush_size)
|
||||||
|
self._brush_image = self._createBrushImage()
|
||||||
|
|
||||||
def setBrushColor(self, brush_color: str) -> None:
|
def setBrushColor(self, brush_color: str) -> None:
|
||||||
|
if brush_color != self._brush_color:
|
||||||
self._brush_color = brush_color
|
self._brush_color = brush_color
|
||||||
|
self._brush_image = self._createBrushImage()
|
||||||
|
|
||||||
def setBrushShape(self, brush_shape: str) -> None:
|
def setBrushShape(self, brush_shape: str) -> None:
|
||||||
|
if brush_shape != self._brush_shape:
|
||||||
self._brush_shape = brush_shape
|
self._brush_shape = brush_shape
|
||||||
|
self._brush_image = self._createBrushImage()
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _get_intersect_ratio_via_pt(a: numpy.ndarray, pt: numpy.ndarray, b: numpy.ndarray, c: numpy.ndarray) -> float:
|
def _get_intersect_ratio_via_pt(a: numpy.ndarray, pt: numpy.ndarray, b: numpy.ndarray, c: numpy.ndarray) -> float:
|
||||||
@ -85,28 +145,10 @@ class PaintTool(Tool):
|
|||||||
def _nodeTransformChanged(self, *args) -> None:
|
def _nodeTransformChanged(self, *args) -> None:
|
||||||
self._cache_dirty = True
|
self._cache_dirty = True
|
||||||
|
|
||||||
def _getBrushPixels(self, mid_x: float, mid_y: float, w: float, h: float) -> List[Tuple[float, float]]:
|
def _getTexCoordsFromClick(self, node: SceneNode, x: int, y: int) -> Optional[Tuple[float, float]]:
|
||||||
res = []
|
|
||||||
include = False
|
|
||||||
for y in range(-self._brush_size//2, (self._brush_size + 1)//2):
|
|
||||||
for x in range(-self._brush_size//2, (self._brush_size + 1)//2):
|
|
||||||
match self._brush_shape:
|
|
||||||
case "A":
|
|
||||||
include = True
|
|
||||||
case "B":
|
|
||||||
include = x * x + y * y <= self._brush_size * self._brush_size
|
|
||||||
if include:
|
|
||||||
xx = mid_x + (x / w)
|
|
||||||
yy = mid_y + (y / h)
|
|
||||||
if xx < 0 or xx > 1 or yy < 0 or yy > 1:
|
|
||||||
continue
|
|
||||||
res.append((xx, yy))
|
|
||||||
return res
|
|
||||||
|
|
||||||
def _handleMouseAction(self, node: SceneNode, paintview: View, x: int, y: int) -> bool:
|
|
||||||
face_id = self._selection_pass.getFaceIdAtPosition(x, y)
|
face_id = self._selection_pass.getFaceIdAtPosition(x, y)
|
||||||
if face_id < 0:
|
if face_id < 0:
|
||||||
return False
|
return None
|
||||||
|
|
||||||
pt = self._picking_pass.getPickedPosition(x, y).getData()
|
pt = self._picking_pass.getPickedPosition(x, y).getData()
|
||||||
|
|
||||||
@ -123,13 +165,7 @@ class PaintTool(Tool):
|
|||||||
wb /= wt
|
wb /= wt
|
||||||
wc /= wt
|
wc /= wt
|
||||||
texcoords = wa * ta + wb * tb + wc * tc
|
texcoords = wa * ta + wb * tb + wc * tc
|
||||||
|
return texcoords
|
||||||
color = self._color_str_to_rgba[self._brush_color]
|
|
||||||
w, h = paintview.getUvTexDimensions()
|
|
||||||
for (x, y) in self._getBrushPixels(texcoords[0], texcoords[1], float(w), float(h)):
|
|
||||||
paintview.setUvPixel(x, y, color)
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
def event(self, event: Event) -> bool:
|
def event(self, event: Event) -> bool:
|
||||||
"""Handle mouse and keyboard events.
|
"""Handle mouse and keyboard events.
|
||||||
@ -160,7 +196,7 @@ class PaintTool(Tool):
|
|||||||
if MouseEvent.LeftButton not in cast(MouseEvent, event).buttons:
|
if MouseEvent.LeftButton not in cast(MouseEvent, event).buttons:
|
||||||
return False
|
return False
|
||||||
self._mouse_held = False
|
self._mouse_held = False
|
||||||
self._last_mouse_drag = None
|
self._last_text_coords = None
|
||||||
return True
|
return True
|
||||||
|
|
||||||
is_moved = event.type == Event.MouseMoveEvent
|
is_moved = event.type == Event.MouseMoveEvent
|
||||||
@ -175,12 +211,11 @@ class PaintTool(Tool):
|
|||||||
return False
|
return False
|
||||||
else:
|
else:
|
||||||
self._mouse_held = True
|
self._mouse_held = True
|
||||||
drags = ([self._last_mouse_drag] if self._last_mouse_drag else []) + [(evt.x, evt.y)]
|
|
||||||
self._last_mouse_drag = (evt.x, evt.y)
|
|
||||||
|
|
||||||
paintview = controller.getActiveView()
|
paintview = controller.getActiveView()
|
||||||
if paintview is None or paintview.getPluginId() != "PaintTool":
|
if paintview is None or paintview.getPluginId() != "PaintTool":
|
||||||
return False
|
return False
|
||||||
|
paintview = cast(PaintView, paintview)
|
||||||
|
|
||||||
if not self._selection_pass:
|
if not self._selection_pass:
|
||||||
return False
|
return False
|
||||||
@ -205,7 +240,6 @@ class PaintTool(Tool):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
evt = cast(MouseEvent, event)
|
evt = cast(MouseEvent, event)
|
||||||
drags.append((evt.x, evt.y))
|
|
||||||
|
|
||||||
if not self._picking_pass:
|
if not self._picking_pass:
|
||||||
self._picking_pass = PickingPass(camera.getViewportWidth(), camera.getViewportHeight())
|
self._picking_pass = PickingPass(camera.getViewportWidth(), camera.getViewportHeight())
|
||||||
@ -213,11 +247,23 @@ class PaintTool(Tool):
|
|||||||
|
|
||||||
self._selection_pass.renderFacesMode()
|
self._selection_pass.renderFacesMode()
|
||||||
|
|
||||||
res = False
|
texcoords = self._getTexCoordsFromClick(node, evt.x, evt.y)
|
||||||
for (x, y) in drags:
|
if texcoords is None:
|
||||||
res |= self._handleMouseAction(node, paintview, x, y)
|
return False
|
||||||
if res:
|
if self._last_text_coords is None:
|
||||||
|
self._last_text_coords = texcoords
|
||||||
|
|
||||||
|
w, h = paintview.getUvTexDimensions()
|
||||||
|
sub_image, (start_x, start_y) = self._createStrokeImage(
|
||||||
|
self._last_text_coords[0] * w,
|
||||||
|
self._last_text_coords[1] * h,
|
||||||
|
texcoords[0] * w,
|
||||||
|
texcoords[1] * h
|
||||||
|
)
|
||||||
|
paintview.addStroke(sub_image, start_x, start_y)
|
||||||
|
|
||||||
|
self._last_text_coords = texcoords
|
||||||
Application.getInstance().getController().getScene().sceneChanged.emit(node)
|
Application.getInstance().getController().getScene().sceneChanged.emit(node)
|
||||||
return res
|
return True
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
# Copyright (c) 2025 UltiMaker
|
# Copyright (c) 2025 UltiMaker
|
||||||
# Cura is released under the terms of the LGPLv3 or higher.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
from PyQt6.QtGui import QImage
|
||||||
|
|
||||||
from UM.PluginRegistry import PluginRegistry
|
from UM.PluginRegistry import PluginRegistry
|
||||||
from UM.View.View import View
|
from UM.View.View import View
|
||||||
@ -18,8 +20,10 @@ class PaintView(View):
|
|||||||
super().__init__()
|
super().__init__()
|
||||||
self._paint_shader = None
|
self._paint_shader = None
|
||||||
self._paint_texture = None
|
self._paint_texture = None
|
||||||
self._tex_width = 256
|
|
||||||
self._tex_height = 256
|
# FIXME: When the texture UV-unwrapping is done, these two values will need to be set to a proper value (suggest 4096 for both).
|
||||||
|
self._tex_width = 512
|
||||||
|
self._tex_height = 512
|
||||||
|
|
||||||
def _checkSetup(self):
|
def _checkSetup(self):
|
||||||
if not self._paint_shader:
|
if not self._paint_shader:
|
||||||
@ -29,8 +33,8 @@ class PaintView(View):
|
|||||||
self._paint_texture = OpenGL.getInstance().createTexture(self._tex_width, self._tex_height)
|
self._paint_texture = OpenGL.getInstance().createTexture(self._tex_width, self._tex_height)
|
||||||
self._paint_shader.setTexture(0, self._paint_texture)
|
self._paint_shader.setTexture(0, self._paint_texture)
|
||||||
|
|
||||||
def setUvPixel(self, x, y, color) -> None:
|
def addStroke(self, stroke_image: QImage, start_x: int, start_y: int) -> None:
|
||||||
self._paint_texture.setPixel(x, y, color)
|
self._paint_texture.setSubImage(stroke_image, start_x, start_y)
|
||||||
|
|
||||||
def getUvTexDimensions(self):
|
def getUvTexDimensions(self):
|
||||||
return self._tex_width, self._tex_height
|
return self._tex_width, self._tex_height
|
||||||
|
Loading…
x
Reference in New Issue
Block a user