Coverage for paintmaskanimator/geometry.py: 95%
35 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"""Pure geometry / math helpers extracted from PaintCanvas.
3Stateless functions over angles, points, grids and homographies. No widget or
4document state — trivially unit-testable. PaintCanvas keeps thin delegating
5wrappers so its call sites are unchanged.
6"""
7import math
9import numpy as np
10from PySide6.QtCore import QPointF
11from PySide6.QtGui import QTransform
14def normalized_angle(angle):
15 return ((float(angle) + 180.0) % 360.0) - 180.0
18def rotated_vector(x, y, angle_degrees):
19 angle = math.radians(float(angle_degrees))
20 return QPointF(
21 float(x) * math.cos(angle) - float(y) * math.sin(angle),
22 float(x) * math.sin(angle) + float(y) * math.cos(angle),
23 )
26def quad_homography(source_points, target_points):
27 matrix = []
28 values = []
29 for source, target in zip(source_points, target_points):
30 x, y = float(source.x()), float(source.y())
31 u, v = float(target.x()), float(target.y())
32 matrix.append([x, y, 1, 0, 0, 0, -u * x, -u * y])
33 values.append(u)
34 matrix.append([0, 0, 0, x, y, 1, -v * x, -v * y])
35 values.append(v)
36 try:
37 a, b, c, d, e, f, g, h = np.linalg.solve(
38 np.asarray(matrix, dtype=np.float64),
39 np.asarray(values, dtype=np.float64),
40 )
41 except np.linalg.LinAlgError:
42 return QTransform()
43 return QTransform(a, d, g, b, e, h, c, f, 1.0)
46def mesh_catmull_scalar(p0, p1, p2, p3, t):
47 """制御点を通過するCatmull-Rom補間。"""
48 t = max(0.0, min(1.0, float(t)))
49 t2 = t * t
50 t3 = t2 * t
51 return 0.5 * (
52 2.0 * p1
53 + (-p0 + p2) * t
54 + (2.0*p0 - 5.0*p1 + 4.0*p2 - p3) * t2
55 + (-p0 + 3.0*p1 - 3.0*p2 + p3) * t3
56 )
59def regular_grid_points(rect, cols, rows=None):
60 cols = max(2, int(cols))
61 rows = max(2, int(rows if rows is not None else cols))
62 width = max(0.0, rect.width())
63 height = max(0.0, rect.height())
64 return [
65 QPointF(
66 rect.left() + width * gx / (cols - 1),
67 rect.top() + height * gy / (rows - 1),
68 )
69 for gy in range(rows)
70 for gx in range(cols)
71 ]