Coverage for paintmaskanimator/main_window_auto_paint.py: 74%

142 statements  

« prev     ^ index     » next       coverage.py v7.17.0, created at 2026-11-02 04:50 +0000

1"""自動で塗る。``window.auto_paint`` が持つ。 

2 

3黒の主線と原色の赤・緑・青(と黄・水色・赤紫)の色トレス線を色分けの境界にして、見本のセル(既定は 

4タイムラインで選んでいるセル)から前後へ1セルずつ、同じ場所の色で塗り進める。 

5一通り塗ったら迷った所の少ないセルを見本に加えて塗り直す。アクティブレイヤー 

6だけを塗り、Undo は1回にまとめる。塗り残しと塗り間違いかもしれない所は、 

7「見本との違いチェック」と同じ一覧で知らせる。塗る範囲と色を決めるのは 

8:mod:`auto_paint`、ここは画面と書き込みを受け持つ。 

9 

10A collaborator rather than a mixin -- see main_window_export.py for why. 

11""" 

12import math 

13from dataclasses import replace 

14from time import monotonic 

15from typing import TYPE_CHECKING, List, Optional, Tuple 

16 

17import numpy as np 

18from PySide6.QtGui import QImage 

19from PySide6.QtWidgets import QDialog 

20 

21from .auto_paint_dialog import AutoPaintDialog 

22 

23from . import auto_paint, bucket_fill, time_display 

24from .frame_scope import FrameScope, resolve_cells 

25from .i18n import tr 

26from .imaging import qimage_rgba_array 

27from .progress import close_counter, create_counter, update_counter 

28 

29if TYPE_CHECKING: 

30 from .main_window import MainWindow 

31 

32Cell = Tuple[int, int] 

33 

34 

35def cell_label(layer, frame_index, fps=24) -> str: 

36 """設定画面の一覧に出す ``レイヤー名+絵番号・時間位置``。""" 

37 number = getattr(layer, "sequence_number", None) 

38 cell = f"{layer.name}{number}" if number is not None else str(layer.name) 

39 return f"{cell} · {time_display.frame_position(frame_index, fps)}" 

40 

41 

42class AutoPaintController: 

43 """Owned by ``MainWindow`` as ``window.auto_paint``.""" 

44 

45 def __init__(self, window: "MainWindow"): 

46 self.window = window 

47 

48 def cells(self) -> List[Cell]: 

49 """アクティブレイヤーの、絵のあるセル(保持はキーフレームへ畳む)。""" 

50 canvas = self.window.canvas 

51 return [ 

52 (frame, layer) for frame, layer in resolve_cells(canvas, FrameScope.all_frames(canvas)) 

53 if canvas.frames[frame].layers[layer].has_content 

54 ] 

55 

56 def open(self) -> None: 

57 """設定画面を出し、「実行」なら塗る。""" 

58 canvas = self.window.canvas 

59 status = self.window.statusBar() 

60 canvas.abort_skewer_fill() 

61 cells = self.cells() 

62 if len(cells) < 2: 

63 status.showMessage(tr("塗れるセルが2つ以上ありません。"), 3000) 

64 return 

65 layer_index = int(canvas.active_layer_index) 

66 key = canvas.resolve_key_frame(int(canvas.current_frame), layer_index) 

67 current = 0 

68 if key is not None and (key, layer_index) in cells: 

69 current = cells.index((key, layer_index)) 

70 fps = int(self.window.timeline.fps.value()) 

71 labels = [ 

72 cell_label(canvas.frames[frame].layers[layer], frame, fps) for frame, layer in cells 

73 ] 

74 layer_name = canvas.frames[cells[0][0]].layers[layer_index].name 

75 dialog = AutoPaintDialog( 

76 self.window, layer_name, cells, labels, current, 

77 images=[canvas.frames[frame].layers[layer].image for frame, layer in cells], 

78 numbers=[canvas.frames[frame].layers[layer].sequence_number for frame, layer in cells], 

79 fps=fps, 

80 ) 

81 if dialog.exec() != QDialog.DialogCode.Accepted: 

82 return 

83 chosen = [cells.index(cell) for cell in dialog.chosen()] 

84 self.run(cells, chosen[0], reference_indices=chosen) 

85 

86 def run(self, cells: List[Cell], reference_index: int, 

87 second_reference_index: Optional[int] = None, *, 

88 reference_indices: Optional[List[int]] = None) -> bool: 

89 """``cells[reference_index]`` を見本に塗る。何か塗れたら真。""" 

90 canvas = self.window.canvas 

91 status = self.window.statusBar() 

92 pixels = [] 

93 hints = [] 

94 lines = [] 

95 references = {reference_index, second_reference_index} 

96 references.update(reference_indices or []) 

97 for index, (frame, layer) in enumerate(cells): 

98 cell = canvas.frames[frame].layers[layer] 

99 current = np.ascontiguousarray(qimage_rgba_array(cell.image)) 

100 initial = None 

101 if cell.initial_image is not None: 

102 initial = auto_paint.line_only( 

103 np.ascontiguousarray(qimage_rgba_array(cell.initial_image)) 

104 ) 

105 # 見本は人が塗った今の絵。ほかは色塗り前の初期状態があれば、その線だけで 

106 # 部分を分け、今の絵で手で塗ってある部分はその色を答え(前後のセルの手本) 

107 # にする。セル同士の動きも、初期状態の線(見本では塗りつぶした色トレス線も 

108 # 残っている)で求める。 

109 if index not in references and initial is not None: 

110 pixels.append(initial) 

111 hints.append(current) 

112 else: 

113 pixels.append(current) 

114 hints.append(None) 

115 lines.append(initial) 

116 plan = self._plan( 

117 pixels, reference_index, 

118 hints if any(hint is not None for hint in hints) else None, 

119 lines if any(line is not None for line in lines) else None, 

120 second_reference_index=second_reference_index, 

121 reference_indices=reference_indices, 

122 ) 

123 if plan is None: 123 ↛ 124line 123 didn't jump to line 124 because the condition on line 123 was never true

124 status.showMessage(tr("自動で塗るのを中止しました。"), 2400) 

125 return False 

126 targets = {cells[index]: paint for index, paint in plan.cells.items() if paint.paints} 

127 if not targets: 127 ↛ 128line 127 didn't jump to line 128 because the condition on line 127 was never true

128 status.showMessage(tr("自動で塗る:塗るところはありませんでした。"), 4000) 

129 return False 

130 written = set() 

131 

132 def op(context): 

133 paint = targets.get((context.frame, context.layer_index)) 

134 if paint is None: 134 ↛ 135line 134 didn't jump to line 135 because the condition on line 134 was never true

135 return None 

136 image, view = canvas._layer_fill_pixels(context.layer.image) 

137 for rgb, (x0, y0, x1, y1), mask in paint.paints: 

138 patch = view[y0:y1, x0:x1] 

139 patch[mask, :3] = rgb 

140 patch[mask, 3] = 255 

141 written.add(rgb) 

142 return image.convertToFormat(QImage.Format.Format_ARGB32_Premultiplied) 

143 

144 scope = FrameScope.cells(sorted({frame for frame, _layer in targets}), (cells[0][1],)) 

145 result = self.window.scope.run_over( 

146 scope, op, label=tr("自動で塗る"), cancellable=True, 

147 ) 

148 canvas.update() 

149 if result.cancelled: 149 ↛ 150line 149 didn't jump to line 150 because the condition on line 149 was never true

150 status.showMessage(tr("自動で塗るのを中止しました。"), 2400) 

151 return False 

152 if written: 152 ↛ 154line 152 didn't jump to line 154 because the condition on line 152 was always true

153 canvas._emit_actual_paint_colors(tuple(sorted(written))) 

154 entries = self.notes(cells, plan) 

155 message = tr("自動で塗る:{cells}セルを塗りました。").format(cells=result.changed_cells) 

156 if plan.rounds > 1: 156 ↛ 157line 156 didn't jump to line 157 because the condition on line 156 was never true

157 message += tr("見本を{added}セルに増やして{rounds}回塗り直しました。").format( 

158 added=len(plan.references), rounds=plan.rounds - 1, 

159 ) 

160 if entries: 160 ↛ 162line 160 didn't jump to line 162 because the condition on line 160 was always true

161 message += tr("確かめたい所が{count}か所あります。").format(count=len(entries)) 

162 status.showMessage(message, 8000) 

163 if entries: 163 ↛ 174line 163 didn't jump to line 174 because the condition on line 163 was always true

164 self.window.reference_check.show_list( 

165 entries, cells[reference_index][0], title=tr("自動で塗った、確かめたい所"), 

166 intro=tr( 

167 "塗り残し(場所や色を決めきれなかった部分)、新しく出てきた部分(開いた口" 

168 "など、手本に無い形)、色を決められなかった色トレス線と、前後のセルと違う色で" 

169 "塗ってあった部分(塗り替えていません)です。行を選ぶと、そのコマへ移って" 

170 "場所を選択します。" 

171 ), 

172 reference_only=[], 

173 ) 

174 return bool(result.changed) 

175 

176 @staticmethod 

177 def notes(cells: List[Cell], plan: auto_paint.AutoPlan): 

178 """一覧に載せる ``(コマ, レイヤー, 見つかったもの)`` の並び。""" 

179 entries = [] 

180 for index in sorted(plan.cells): 

181 frame, layer = cells[index] 

182 for finding in plan.cells[index].notes: 

183 entries.append((frame, layer, finding)) 

184 return entries 

185 

186 def _plan(self, pixels, reference_index, hints=None, 

187 lines=None, second_reference_index=None, 

188 reference_indices=None) -> Optional[auto_paint.AutoPlan]: 

189 references = {reference_index} 

190 references.update(reference_indices or []) 

191 if second_reference_index is not None: 

192 references.add(second_reference_index) 

193 count = sum(index not in references and image.shape == pixels[reference_index].shape 

194 for index, image in enumerate(pixels)) 

195 total_steps = max(1, count * auto_paint.MAX_ROUNDS) 

196 started = monotonic() 

197 dialog = create_counter( 

198 self.window, tr("自動で塗る"), total_steps, 

199 label=tr("全体 0%・残り時間を計算中"), cancellable=True, 

200 ) 

201 

202 def progress(done, total): 

203 if dialog.wasCanceled(): 203 ↛ 204line 203 didn't jump to line 204 because the condition on line 203 was never true

204 return False 

205 total = max(1, total) 

206 value = max(0, min(done, total)) 

207 percent = value * 100 // total 

208 if done >= total: 

209 label = tr("自動塗りの計算が終わりました") 

210 else: 

211 label = tr("{round}回目:対象{count}セルを塗っています").format( 

212 round=done // max(1, count) + 1, count=count, 

213 ) 

214 if value: 

215 remaining = math.ceil(max(0.0, monotonic() - started) / value * (total - value)) 

216 minutes, seconds = divmod(remaining, 60) 

217 timing = tr("全体 {percent}%・残り約{minutes}分{seconds:02d}秒").format( 

218 percent=percent, minutes=minutes, seconds=seconds, 

219 ) 

220 else: 

221 timing = tr("全体 0%・残り時間を計算中") 

222 label = f"{label}\n{timing}" 

223 self.window.statusBar().showMessage(label.replace("\n", " ")) 

224 update_counter(dialog, value, total, label) 

225 return True 

226 

227 try: 

228 return auto_paint.plan_auto_paint( 

229 pixels, reference_index, progress=progress, hints=hints, lines=lines, 

230 second_reference_index=second_reference_index, 

231 reference_indices=reference_indices, 

232 bucket_options=replace( 

233 self.window.canvas.bucket_options(respect_modifiers=False), 

234 adjacent=True, 

235 reference=bucket_fill.REFERENCE_INITIAL, 

236 ), 

237 ) 

238 finally: 

239 close_counter(dialog) 

240 self.window.statusBar().clearMessage()