Coverage for paintmaskanimator/tween_groups.py: 90%
55 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"""あとから直せるトゥイーン:キーのセルに持たせる設定と、区間の判定。
3確定したトゥイーンは、中割りの絵を各コマに持つ(再生・書き出しは通常の
4セルと同じ)。そのうえで、キーのセルの ``Layer.tween`` に設定を、中割りの
5セルの ``Layer.tween_member`` に同じ id を持たせ、タイムラインでは 1 本の
6トゥイーンの帯として見せる。中割りのセルを動かすなどして並びが崩れたら、
7帯としては扱わず、通常のセルとして見せる。
8"""
9import uuid
11from PySide6.QtCore import QPointF
13MODES = ("free", "mesh")
16def new_id():
17 return uuid.uuid4().hex
20def points_to_data(points):
21 return [[float(point.x()), float(point.y())] for point in points]
24def points_from_data(data):
25 return [QPointF(float(x), float(y)) for x, y in data]
28def make_spec(
29 tween_id, length, mode, reverse, start_points, final_points,
30 mesh_cols=4, mesh_rows=4, mesh_reference_points=(),
31):
32 """キーのセルに保存する設定(JSON にそのまま書ける値だけ)。"""
33 return {
34 "id": str(tween_id),
35 "length": int(length),
36 "mode": "mesh" if mode == "mesh" else "free",
37 "reverse": bool(reverse),
38 "start_points": points_to_data(start_points),
39 "final_points": points_to_data(final_points),
40 "mesh_cols": int(mesh_cols),
41 "mesh_rows": int(mesh_rows),
42 "mesh_reference_points": points_to_data(mesh_reference_points),
43 }
46def _points_ok(value):
47 return isinstance(value, list) and all(
48 isinstance(point, (list, tuple))
49 and len(point) == 2
50 and all(isinstance(v, (int, float)) for v in point)
51 for point in value
52 )
55def sanitize_spec(value):
56 """読み込んだ設定を検証する。不正なら None(通常のセルとして扱う)。"""
57 if not isinstance(value, dict):
58 return None
59 try:
60 spec = {
61 "id": str(value["id"]),
62 "length": int(value["length"]),
63 "mode": "mesh" if value.get("mode") == "mesh" else "free",
64 "reverse": bool(value.get("reverse", False)),
65 "start_points": value.get("start_points", []),
66 "final_points": value.get("final_points", []),
67 "mesh_cols": int(value.get("mesh_cols", 4)),
68 "mesh_rows": int(value.get("mesh_rows", 4)),
69 "mesh_reference_points": value.get("mesh_reference_points", []),
70 }
71 except (KeyError, TypeError, ValueError):
72 return None
73 if not spec["id"] or spec["length"] < 2:
74 return None
75 for key in ("start_points", "final_points", "mesh_reference_points"):
76 if not _points_ok(spec[key]):
77 return None
78 return spec
81def group_length(frames, layer_index, key_column):
82 """``key_column`` が崩れていないトゥイーンの先頭なら、その長さを返す。"""
83 if not (0 <= key_column < len(frames)): 83 ↛ 84line 83 didn't jump to line 84 because the condition on line 83 was never true
84 return None
85 layers = frames[key_column].layers
86 if not (0 <= layer_index < len(layers)): 86 ↛ 87line 86 didn't jump to line 87 because the condition on line 86 was never true
87 return None
88 key = layers[layer_index]
89 spec = getattr(key, "tween", None)
90 if not spec or not key.has_content or int(key.exposure) != 1:
91 return None
92 length = int(spec.get("length", 0))
93 if length < 2 or key_column + length > len(frames): 93 ↛ 94line 93 didn't jump to line 94 because the condition on line 93 was never true
94 return None
95 for offset in range(1, length):
96 member = frames[key_column + offset].layers[layer_index]
97 if (
98 getattr(member, "tween_member", None) != spec.get("id")
99 or not member.has_content
100 or int(member.exposure) != 1
101 ):
102 return None
103 return length
106def tween_groups(frames, layer_index):
107 """{先頭のコマ: (長さ, 逆生成か)} を返す。"""
108 groups = {}
109 column = 0
110 while column < len(frames):
111 length = group_length(frames, layer_index, column)
112 if length:
113 spec = frames[column].layers[layer_index].tween
114 groups[column] = (length, bool(spec.get("reverse", False)))
115 column += length
116 else:
117 column += 1
118 return groups