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

1"""あとから直せるトゥイーン:キーのセルに持たせる設定と、区間の判定。 

2 

3確定したトゥイーンは、中割りの絵を各コマに持つ(再生・書き出しは通常の 

4セルと同じ)。そのうえで、キーのセルの ``Layer.tween`` に設定を、中割りの 

5セルの ``Layer.tween_member`` に同じ id を持たせ、タイムラインでは 1 本の 

6トゥイーンの帯として見せる。中割りのセルを動かすなどして並びが崩れたら、 

7帯としては扱わず、通常のセルとして見せる。 

8""" 

9import uuid 

10 

11from PySide6.QtCore import QPointF 

12 

13MODES = ("free", "mesh") 

14 

15 

16def new_id(): 

17 return uuid.uuid4().hex 

18 

19 

20def points_to_data(points): 

21 return [[float(point.x()), float(point.y())] for point in points] 

22 

23 

24def points_from_data(data): 

25 return [QPointF(float(x), float(y)) for x, y in data] 

26 

27 

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 } 

44 

45 

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 ) 

53 

54 

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 

79 

80 

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 

104 

105 

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