Coverage for paintmaskanimator/brush_raster.py: 100%
38 statements
« prev ^ index » next coverage.py v7.17.0, created at 2026-11-02 04:50 +0000
« prev ^ index » next coverage.py v7.17.0, created at 2026-11-02 04:50 +0000
1"""Shared non-antialiased brush geometry for painting and cursor previews."""
2import math
3from functools import lru_cache
5import numpy as np
6from PySide6.QtCore import QPointF, QRect
7from PySide6.QtGui import QColor, QImage, QPainter, QPainterPath, QPen
8from PySide6.QtCore import Qt
11def paint_segment(painter, start, end, color, width):
12 painter.setRenderHint(QPainter.RenderHint.Antialiasing, False)
13 # Subpixel cosmetic Qt pens lower alpha even with AA disabled. Pixel brushes
14 # always cover at least one fully opaque pixel.
15 painter.setPen(QPen(color, max(1.0, width), Qt.PenStyle.SolidLine,
16 Qt.PenCapStyle.RoundCap, Qt.PenJoinStyle.RoundJoin))
17 if start == end:
18 # Qt's degenerate floating-point line can discard a click entirely.
19 painter.drawPoint(start)
20 else:
21 painter.drawLine(start, end)
24def stamp_mask(center, width):
25 margin = math.ceil(width / 2) + 3
26 origin_x, origin_y = math.floor(center.x()) - margin, math.floor(center.y()) - margin
27 size = margin * 2 + 1
28 image = QImage(size, size, QImage.Format.Format_ARGB32_Premultiplied)
29 image.fill(0)
30 painter = QPainter(image)
31 local = center - QPointF(origin_x, origin_y)
32 paint_segment(painter, local, local, QColor("white"), width)
33 painter.end()
34 return image, QRect(origin_x, origin_y, size, size)
37def stamp_outline(center, width):
38 # Integer translation does not change raster coverage. Cache the exact
39 # subpixel phase; rounding it would change the preview at fractional sizes.
40 x, y = math.floor(center.x()), math.floor(center.y())
41 path = QPainterPath(_local_outline(center.x() - x, center.y() - y, width))
42 path.translate(x, y)
43 return path
46@lru_cache(maxsize=64)
47def _local_outline(phase_x, phase_y, width):
48 image, rect = stamp_mask(QPointF(phase_x, phase_y), width)
49 alpha = np.frombuffer(image.constBits(), np.uint8).reshape(image.height(), image.bytesPerLine())[:, 3::4]
50 path = QPainterPath()
51 for y, row in enumerate(alpha):
52 edges = np.flatnonzero(np.diff(np.r_[False, row != 0, False]))
53 for left, right in zip(edges[::2], edges[1::2]):
54 path.addRect(float(rect.x() + left), float(rect.y() + y), float(right - left), 1.0)
55 return path.simplified()