Coverage for paintmaskanimator/main_window_tween.py: 78%

333 statements  

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

1"""In-between generation and its command popup; owned as ``window.tween``. 

2 

3Drives the tween command popup, enables/commits/cancels tween interpolation on 

4the timeline, and dispatches the shared "commit or cancel the current transform 

5or tween" actions. 

6 

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

8""" 

9from typing import TYPE_CHECKING 

10 

11from PySide6.QtCore import QPoint, QPointF, Qt 

12from PySide6.QtGui import QPainter 

13from PySide6.QtWidgets import QApplication, QMessageBox 

14from .i18n import tr 

15from .errors import OPERATION_ERRORS as _OPERATION_ERRORS, OperationError 

16from .widgets import TweenCommandPopup 

17from .progress import close_counter, create_counter, update_counter 

18from .logging_setup import get_logger 

19from .undo_entries import DocUndo 

20from . import tween_groups 

21 

22if TYPE_CHECKING: 

23 from .main_window import MainWindow 

24 

25log = get_logger(__name__) 

26 

27 

28class TweenController: 

29 """Owned by ``MainWindow`` as ``window.tween``. 

30 

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

32 """ 

33 

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

35 self.window = window 

36 

37 def _refresh_timeline_tween_marker(self): 

38 self.window.timeline.refresh( 

39 self.window.canvas.frames, 

40 self.window.canvas.current_frame, 

41 self.window.canvas.active_layer_index, 

42 getattr(self.window.canvas, "tween_pending", None), 

43 ) 

44 

45 def _close_tween_command_popup(self): 

46 popup = self.window._tween_command_popup 

47 self.window._tween_command_popup = None 

48 if popup is not None: 

49 popup.close() 

50 popup.deleteLater() 

51 

52 def _show_tween_command_popup(self, mode=None): 

53 self._close_tween_command_popup() 

54 pending = getattr(self.window.canvas, "tween_pending", None) or {} 

55 tween_mode = ( 

56 "mesh" 

57 if (mode or pending.get("transform_mode")) == "mesh" 

58 else "free" 

59 ) 

60 popup = TweenCommandPopup( 

61 tween_mode, 

62 pending.get( 

63 "mesh_cols", 

64 getattr(self.window.canvas, "transform_mesh_cols", 4), 

65 ), 

66 pending.get( 

67 "mesh_rows", 

68 getattr(self.window.canvas, "transform_mesh_rows", 4), 

69 ), 

70 self.window, 

71 reverse=bool( 

72 pending.get( 

73 "reverse_generation", 

74 False, 

75 ) 

76 ), 

77 ) 

78 self.window._tween_command_popup = popup 

79 popup.commitRequested.connect(self.commit_transform_or_tween) 

80 popup.cancelRequested.connect(self.cancel_transform_or_tween) 

81 popup.reverseChanged.connect( 

82 self.set_reverse_generation 

83 ) 

84 popup.rotateLeftRequested.connect( 

85 lambda: self.window.canvas.rotate_selection_transform(-90.0) 

86 ) 

87 popup.rotateRightRequested.connect( 

88 lambda: self.window.canvas.rotate_selection_transform(90.0) 

89 ) 

90 popup.destroyed.connect( 

91 lambda _obj=None, current=popup: 

92 setattr( 

93 self.window, 

94 "_tween_command_popup", 

95 None if self.window._tween_command_popup is current 

96 else self.window._tween_command_popup, 

97 ) 

98 ) 

99 popup.adjustSize() 

100 anchor = self.window.timeline_dock.mapToGlobal( 

101 QPoint( 

102 max(0, self.window.timeline_dock.width() - popup.sizeHint().width() - 16), 

103 24, 

104 ) 

105 ) 

106 popup.move(anchor) 

107 popup.show() 

108 popup.raise_() 

109 popup.activateWindow() 

110 

111 def set_reverse_generation(self, enabled): 

112 """生成方向を切り替え、タイムライン記号を即時更新する。""" 

113 pending = getattr( 

114 self.window.canvas, 

115 "tween_pending", 

116 None, 

117 ) 

118 if not isinstance(pending, dict): 

119 return 

120 

121 reverse = bool(enabled) 

122 pending["reverse_generation"] = reverse 

123 self._refresh_timeline_tween_marker() 

124 self.window.canvas.setFocus() 

125 

126 transform_mode = ( 

127 tr("メッシュ変形") 

128 if pending.get("transform_mode") == "mesh" 

129 else tr("自由変形") 

130 ) 

131 if reverse: 

132 self.window.statusBar().showMessage( 

133 tr("◆ {mode}の逆生成:キーフレーム側を変形形状、ラストコマ側を元の初期形状として生成します。").format(mode=transform_mode), 

134 5000, 

135 ) 

136 else: 

137 self.window.statusBar().showMessage( 

138 tr("♦ {mode}の通常生成:キーフレーム側を元の初期形状、ラストコマ側を変形形状として生成します。").format(mode=transform_mode), 

139 5000, 

140 ) 

141 

142 def enable(self, visual_row, key_column, mode="free"): 

143 """タイムライン終端の|を、自由/メッシュトゥイーンの♦へ切り替える。""" 

144 mode = "mesh" if mode == "mesh" else "free" 

145 mode_name = tr("メッシュ変形") if mode == "mesh" else tr("自由変形") 

146 if not self.window.canvas.frames: 146 ↛ 147line 146 didn't jump to line 147 because the condition on line 146 was never true

147 return 

148 layer_count = len(self.window.canvas.layers) 

149 layer_index = layer_count - 1 - int(visual_row) 

150 key_column = int(key_column) 

151 if not ( 151 ↛ 155line 151 didn't jump to line 155 because the condition on line 151 was never true

152 0 <= layer_index < layer_count 

153 and 0 <= key_column < len(self.window.canvas.frames) 

154 ): 

155 return 

156 key_layer = self.window.canvas.frames[key_column].layers[layer_index] 

157 exposure = max(1, int(key_layer.exposure)) 

158 if not key_layer.has_content or exposure < 2: 

159 QMessageBox.warning( 

160 self.window, 

161 tr("トゥイーン"), 

162 tr("2コマ以上の表示区間を持つ画像キーフレームで実行してください。"), 

163 ) 

164 return 

165 

166 answer = QMessageBox.question( 

167 self.window, 

168 tr("トゥイーンを有効にする"), 

169 ( 

170 tr("{exposure}コマの{name}トゥイーンを開始します。\n確定後もトゥイーンとして残り、タイムラインの帯をダブルクリックすると形を直せます。").format(exposure=exposure, name=mode_name) 

171 ), 

172 QMessageBox.StandardButton.Ok 

173 | QMessageBox.StandardButton.Cancel, 

174 QMessageBox.StandardButton.Ok, 

175 ) 

176 if answer != QMessageBox.StandardButton.Ok: 176 ↛ 177line 176 didn't jump to line 177 because the condition on line 176 was never true

177 return 

178 

179 if self.window.canvas.transform_active: 179 ↛ 180line 179 didn't jump to line 180 because the condition on line 179 was never true

180 self.cancel_transform_or_tween() 

181 

182 # 自由変形が画像を一時的に消去する前の文書全体を保存する。 

183 # Ctrl+Zではこの状態へ戻すため、元画像が消えることはない。 

184 tween_undo_snapshot = self.window.canvas.document_snapshot() 

185 self._start( 

186 layer_index, key_column, exposure, mode, tween_undo_snapshot, 

187 ) 

188 

189 def _start( 

190 self, layer_index, key_column, exposure, mode, tween_undo_snapshot, 

191 reverse=False, shape=None, reedit=None, 

192 ): 

193 """キーのコマで変形を始め、トゥイーンの編集中にする。 

194 

195 ``shape`` に保存してあった (変形前の点, 変形後の点) を渡すと、その 

196 変形から始める(あとから直すとき)。``reedit`` はキャンセル時に戻す 

197 状態など。 

198 """ 

199 mode_name = tr("メッシュ変形") if mode == "mesh" else tr("自由変形") 

200 self.window.canvas.current_frame = key_column 

201 self.window.canvas.active_layer_index = layer_index 

202 self.window.canvas.tween_pending = None 

203 self.window.tools.set_tween_active(False) 

204 self.window.palette.setProperty("tweenActive", False) 

205 self.window.canvas.selection_polygon = [] 

206 self.window.canvas.selection_mask_override = None 

207 self.window.canvas.selection_outline_polygons = [] 

208 self.window.canvas.selection_mask_rect = None 

209 self.window.canvas.lasso = [] 

210 self.window.canvas.rect_start = None 

211 self.window.canvas.rect_end = None 

212 

213 # トゥイーンでも現在のTPクオリティ、選択色、線幅を維持する。 

214 # 「すべてのコマに適用」だけはトゥイーン生成と分離する。 

215 self.window.tools.selection_all_frames.setChecked(False) 

216 self.window.colors.set_selected_used_colors(set(self.window.palette.selected_rgbs)) 

217 self.window.tools.select_tool("rect_select") 

218 

219 if not self.window.canvas.auto_select_used_area(): 219 ↛ 220line 219 didn't jump to line 220 because the condition on line 219 was never true

220 self._restore_reedit(reedit) 

221 QMessageBox.warning( 

222 self.window, 

223 tr("トゥイーン"), 

224 tr("画像内に変形対象となる描画領域がありません。"), 

225 ) 

226 return 

227 

228 self.window.line_ops.start_wire_transform(mode) 

229 if not self.window.canvas.transform_active: 229 ↛ 230line 229 didn't jump to line 230 because the condition on line 229 was never true

230 self._restore_reedit(reedit) 

231 return 

232 

233 start_points = [ 

234 QPointF(point) for point in self.window.canvas.transform_points 

235 ] 

236 if shape is not None: 

237 # キーの絵を描き足して基準の点が少しずれても形が崩れないよう、 

238 # 保存した「変形前→変形後」の差を今の点に足す。 

239 saved_start, saved_final = shape 

240 if len(saved_start) == len(saved_final) == len(start_points): 240 ↛ 253line 240 didn't jump to line 253 because the condition on line 240 was always true

241 self.window.canvas.transform_points = [ 

242 QPointF( 

243 point.x() + final.x() - start.x(), 

244 point.y() + final.y() - start.y(), 

245 ) 

246 for point, start, final in zip( 

247 start_points, saved_start, saved_final 

248 ) 

249 ] 

250 self.window.canvas._invalidate_tp_preview_cache() 

251 self.window.canvas.update() 

252 

253 end_column = key_column + exposure - 1 

254 self.window.canvas.tween_pending = { 

255 "layer_index": layer_index, 

256 "key_col": key_column, 

257 "end_col": end_column, 

258 "exposure": exposure, 

259 "undo_snapshot": tween_undo_snapshot, 

260 "quality_active": bool( 

261 self.window.canvas.transform_quality_active 

262 ), 

263 "line_threshold": int( 

264 self.window.canvas.transform_line_threshold 

265 ), 

266 "line_colors": tuple( 

267 self.window.canvas.transform_tp_line_colors 

268 ), 

269 "transform_mode": mode, 

270 "mesh_cols": int( 

271 getattr(self.window.canvas, "transform_mesh_cols", 4) 

272 ), 

273 "mesh_rows": int( 

274 getattr(self.window.canvas, "transform_mesh_rows", 4) 

275 ), 

276 "mesh_reference_points": [ 

277 QPointF(point) 

278 for point in getattr( 

279 self.window.canvas, 

280 "transform_mesh_reference_points", 

281 [], 

282 ) 

283 ], 

284 "start_points": start_points, 

285 "reverse_generation": bool(reverse), 

286 "reedit": reedit, 

287 } 

288 self.window.tools.set_tween_active(True) 

289 self.window.palette.setProperty("tweenActive", True) 

290 self._refresh_timeline_tween_marker() 

291 self._show_tween_command_popup(mode) 

292 self.window.canvas.setFocus() 

293 self.window.statusBar().showMessage( 

294 tr("♦ {name}トゥイーン中です。変形形状を指定し、ポップアップの「逆生成」で生成方向を選べます。").format(name=mode_name), 

295 5000, 

296 ) 

297 

298 def _restore_reedit(self, reedit): 

299 """編集し直しを始められなかったら、確定済みのトゥイーンへ戻す。""" 

300 if reedit is None: 

301 return 

302 self.window.canvas.apply_undo_entry(DocUndo(reedit["snapshot"])) 

303 self.window.refresh_ui() 

304 

305 def _member_numbers(self, layer_index, key_column, exposure, previous): 

306 """中割りのセルの番号。直し直しなら前の番号、なければ末尾に足す。""" 

307 count = max(0, int(exposure) - 1) 

308 if previous and len(previous) == count: 

309 return [int(number) for number in previous] 

310 used = { 

311 int(frame.layers[layer_index].sequence_number) 

312 for frame in self.window.canvas.frames 

313 if frame.layers[layer_index].sequence_number is not None 

314 } 

315 used.update( 

316 int(number) 

317 for archived_layer, number in self.window.canvas._sequence_archive 

318 if int(archived_layer) == layer_index 

319 ) 

320 next_number = max(used, default=0) + 1 

321 return list(range(next_number, next_number + count)) 

322 

323 def edit(self, visual_row, key_column): 

324 """確定済みのトゥイーンを、保存した変形の形から編集し直す。""" 

325 canvas = self.window.canvas 

326 if not canvas.frames: 326 ↛ 327line 326 didn't jump to line 327 because the condition on line 326 was never true

327 return 

328 layer_count = len(canvas.layers) 

329 layer_index = layer_count - 1 - int(visual_row) 

330 key_column = int(key_column) 

331 length = tween_groups.group_length(canvas.frames, layer_index, key_column) 

332 if not length: 332 ↛ 333line 332 didn't jump to line 333 because the condition on line 332 was never true

333 return 

334 if canvas.transform_active: 334 ↛ 335line 334 didn't jump to line 335 because the condition on line 334 was never true

335 self.cancel_transform_or_tween() 

336 key = canvas.frames[key_column].layers[layer_index] 

337 spec = dict(key.tween or {}) 

338 reverse = bool(spec.get("reverse", False)) 

339 members = [ 

340 canvas.frames[key_column + offset].layers[layer_index] 

341 for offset in range(1, length) 

342 ] 

343 reedit = { 

344 "snapshot": canvas.document_snapshot(), 

345 "id": spec.get("id"), 

346 "numbers": [member.sequence_number for member in members], 

347 } 

348 if any(number is None for number in reedit["numbers"]): 348 ↛ 349line 348 didn't jump to line 349 because the condition on line 348 was never true

349 reedit["numbers"] = None 

350 # 元の形の絵(通常生成は先頭、逆生成は末尾)をキーへ戻し、 

351 # 区間を 1 枚のキーにまとめてから変形を始める。 

352 original = (members[-1] if reverse else key).image.copy() 

353 key.image = original 

354 key.exposure = length 

355 key.tween = None 

356 for member in members: 

357 canvas._clear_timeline_layer_cell(member) 

358 mode = "mesh" if spec.get("mode") == "mesh" else "free" 

359 if mode == "mesh": 359 ↛ 360line 359 didn't jump to line 360 because the condition on line 359 was never true

360 self.window.tools.transform_mesh_grid_x.setValue( 

361 int(spec.get("mesh_cols", 4)) 

362 ) 

363 self.window.tools.transform_mesh_grid_y.setValue( 

364 int(spec.get("mesh_rows", 4)) 

365 ) 

366 canvas._cell_structure_dirty = True 

367 self._start( 

368 layer_index, key_column, length, mode, reedit["snapshot"], 

369 reverse=reverse, 

370 shape=( 

371 tween_groups.points_from_data(spec.get("start_points", [])), 

372 tween_groups.points_from_data(spec.get("final_points", [])), 

373 ), 

374 reedit=reedit, 

375 ) 

376 

377 def release(self, visual_row, key_column): 

378 """トゥイーンの帯をやめ、中割りを通常のセルとして扱う。""" 

379 canvas = self.window.canvas 

380 layer_index = len(canvas.layers) - 1 - int(visual_row) 

381 key_column = int(key_column) 

382 length = tween_groups.group_length(canvas.frames, layer_index, key_column) 

383 if not length: 383 ↛ 384line 383 didn't jump to line 384 because the condition on line 383 was never true

384 return 

385 canvas.push_doc_undo() 

386 canvas.frames[key_column].layers[layer_index].tween = None 

387 for offset in range(1, length): 

388 canvas.frames[key_column + offset].layers[layer_index].tween_member = None 

389 canvas._cell_structure_dirty = True 

390 canvas.changed.emit() 

391 canvas.update() 

392 self.window.statusBar().showMessage( 

393 tr("トゥイーンを解除し、中割りを通常のセルにしました。"), 2500 

394 ) 

395 

396 def commit_transform_or_tween(self): 

397 # Enter は串刺し塗りの「推測した範囲で塗る」にも使う(変形中でなければ)。 

398 canvas = self.window.canvas 

399 if not canvas.transform_active and canvas.skewer_fill_has_guess(): 

400 canvas.accept_skewer_guess() 

401 return 

402 if getattr(self.window.canvas, "tween_pending", None): 

403 self.commit_transform() 

404 else: 

405 self.window.canvas.commit_selection_transform() 

406 

407 def cancel_transform_or_tween(self): 

408 pending = getattr(self.window.canvas, "tween_pending", None) 

409 had_tween = bool(pending) 

410 reedit = pending.get("reedit") if isinstance(pending, dict) else None 

411 if self.window.canvas.transform_active: 

412 self.window.canvas.cancel_selection_transform() 

413 if reedit is not None: 

414 # 編集し直しをやめたら、確定済みのトゥイーンをそのまま戻す。 

415 self.window.canvas.apply_undo_entry(DocUndo(reedit["snapshot"])) 

416 self.window.canvas.tween_pending = None 

417 self.window.tools.set_tween_active(False) 

418 self.window.palette.setProperty("tweenActive", False) 

419 self._close_tween_command_popup() 

420 if had_tween: 

421 self._refresh_timeline_tween_marker() 

422 self.window.statusBar().showMessage( 

423 tr("トゥイーンをキャンセルしました。"), 

424 2200, 

425 ) 

426 

427 def commit_transform(self): 

428 """通常生成または逆生成で自由/メッシュ変形形状を補間する。""" 

429 pending = getattr(self.window.canvas, "tween_pending", None) 

430 if not pending or not self.window.canvas.transform_active: 430 ↛ 431line 430 didn't jump to line 431 because the condition on line 430 was never true

431 return 

432 

433 layer_index = int(pending["layer_index"]) 

434 key_column = int(pending["key_col"]) 

435 end_column = int(pending["end_col"]) 

436 exposure = max(2, int(pending["exposure"])) 

437 reverse_generation = bool( 

438 pending.get( 

439 "reverse_generation", 

440 False, 

441 ) 

442 ) 

443 transform_mode = ( 

444 "mesh" 

445 if pending.get("transform_mode") == "mesh" 

446 else "free" 

447 ) 

448 mode_name = ( 

449 tr("メッシュ変形") if transform_mode == "mesh" else tr("自由変形") 

450 ) 

451 self.window.canvas.transform_mode = transform_mode 

452 if transform_mode == "mesh": 452 ↛ 453line 452 didn't jump to line 453 because the condition on line 452 was never true

453 self.window.canvas.transform_mesh_cols = max( 

454 2, int(pending.get("mesh_cols", 4)) 

455 ) 

456 self.window.canvas.transform_mesh_rows = max( 

457 2, int(pending.get("mesh_rows", 4)) 

458 ) 

459 self.window.canvas.transform_mesh_grid = ( 

460 self.window.canvas.transform_mesh_cols 

461 ) 

462 self.window.canvas._active_mesh_cols = ( 

463 self.window.canvas.transform_mesh_cols 

464 ) 

465 self.window.canvas._active_mesh_rows = ( 

466 self.window.canvas.transform_mesh_rows 

467 ) 

468 reference_points = pending.get( 

469 "mesh_reference_points", 

470 [], 

471 ) 

472 if len(reference_points) == ( 

473 self.window.canvas.transform_mesh_cols 

474 * self.window.canvas.transform_mesh_rows 

475 ): 

476 self.window.canvas.transform_mesh_reference_points = [ 

477 QPointF(point) for point in reference_points 

478 ] 

479 else: 

480 self.window.canvas.transform_mesh_reference_points = ( 

481 self.window.canvas._regular_mesh_reference_points( 

482 self.window.canvas.transform_source_rect, 

483 self.window.canvas.transform_mesh_cols, 

484 self.window.canvas.transform_mesh_rows, 

485 ) 

486 ) 

487 start_points = [ 

488 QPointF(point) for point in pending.get("start_points", []) 

489 ] 

490 final_points = [ 

491 QPointF(point) for point in self.window.canvas.transform_points 

492 ] 

493 if ( 493 ↛ 499line 493 didn't jump to line 499 because the condition on line 493 was never true

494 len(start_points) != len(final_points) 

495 or not start_points 

496 or self.window.canvas.transform_original_layer is None 

497 or self.window.canvas.transform_source is None 

498 ): 

499 QMessageBox.warning( 

500 self.window, 

501 tr("トゥイーン"), 

502 tr("変形情報を取得できないため、トゥイーンを確定できません。"), 

503 ) 

504 return 

505 

506 undo_snapshot = pending.get("undo_snapshot") 

507 

508 # トゥイーン開始後に「クオリティ」をON/OFFした場合も、 

509 # 確定時の現在設定をそのまま使用する。 

510 quality_active = bool( 

511 self.window.tools.transform_quality.isChecked() 

512 ) 

513 current_line_threshold = max( 

514 1, 

515 min( 

516 254, 

517 int(self.window.tools.transform_line_width.value()), 

518 ), 

519 ) 

520 current_line_colors = { 

521 tuple(int(channel) for channel in rgb[:3]) 

522 for rgb in self.window.palette.selected_rgbs 

523 if rgb is not None and len(rgb) >= 3 

524 } 

525 

526 self.window.canvas.transform_quality = quality_active 

527 self.window.canvas.transform_quality_active = quality_active 

528 self.window.canvas.transform_apply_all_frames = False 

529 self.window.canvas.transform_line_threshold = current_line_threshold 

530 self.window.canvas.selected_used_color_rgbs = set( 

531 current_line_colors 

532 ) 

533 self.window.canvas.transform_tp_line_colors = tuple( 

534 sorted(current_line_colors) 

535 ) 

536 

537 # 保存済み設定も現在値へ更新し、確定処理の全経路で同じ値を使う。 

538 pending["quality_active"] = quality_active 

539 pending["line_threshold"] = current_line_threshold 

540 pending["line_colors"] = tuple( 

541 sorted(current_line_colors) 

542 ) 

543 

544 self.window.canvas._invalidate_tp_preview_cache() 

545 

546 # クオリティ確定では、通常変形への暗黙フォールバックを許可しない。 

547 # 元画像から色マスクを作り直し、各トゥイーンコマへ確実に適用する。 

548 if quality_active: 548 ↛ 559line 548 didn't jump to line 559 because the condition on line 548 was always true

549 if not self.window.canvas._prepare_tp_transform_masks(): 549 ↛ 550line 549 didn't jump to line 550 because the condition on line 549 was never true

550 QMessageBox.warning( 

551 self.window, 

552 tr("トゥイーン確定"), 

553 tr("TPクオリティ用の色マスクを生成できませんでした。\n" 

554 "Pillowが利用できることと、変形対象に色があることを" 

555 "確認してください。"), 

556 ) 

557 return 

558 

559 original = self.window.canvas.transform_original_layer.copy() 

560 source = self.window.canvas.transform_source 

561 generated_images = [] 

562 total_steps = exposure + 2 

563 progress = create_counter( 

564 self.window, 

565 tr("{name}トゥイーンを確定").format(name=mode_name), 

566 total_steps, 

567 tr("{name}トゥイーン画像を準備しています").format(name=mode_name), 

568 ) 

569 QApplication.setOverrideCursor(Qt.CursorShape.WaitCursor) 

570 try: 

571 update_counter( 

572 progress, 

573 1, 

574 total_steps, 

575 tr("開始キーフレームを準備しています"), 

576 ) 

577 for index in range(exposure): 

578 update_counter( 

579 progress, 

580 index + 1, 

581 total_steps, 

582 tr("{name}の補間コマ {value}/{exposure} を生成しています").format(name=mode_name, value=index + 1, exposure=exposure), 

583 ) 

584 

585 timeline_ratio = ( 

586 index / float(exposure - 1) 

587 ) 

588 transform_ratio = ( 

589 1.0 - timeline_ratio 

590 if reverse_generation 

591 else timeline_ratio 

592 ) 

593 

594 # 変形率0は元画像をそのまま使用する。 

595 # 通常生成では先頭、逆生成ではラストコマが初期形状になる。 

596 if transform_ratio <= 1e-9: 

597 image = original.copy() 

598 else: 

599 self.window.canvas.transform_points = [ 

600 QPointF( 

601 start.x() 

602 + ( 

603 finish.x() 

604 - start.x() 

605 ) * transform_ratio, 

606 start.y() 

607 + ( 

608 finish.y() 

609 - start.y() 

610 ) * transform_ratio, 

611 ) 

612 for start, finish in zip( 

613 start_points, 

614 final_points, 

615 ) 

616 ] 

617 self.window.canvas._invalidate_tp_preview_cache() 

618 if quality_active: 618 ↛ 627line 618 didn't jump to line 627 because the condition on line 618 was always true

619 preview, _target = ( 

620 self.window.canvas._tp_mask_preview_image( 

621 source, 

622 original.width(), 

623 original.height(), 

624 ) 

625 ) 

626 else: 

627 preview, _target = ( 

628 self.window.canvas._project_transform_source( 

629 source, 

630 original.width(), 

631 original.height(), 

632 smooth=False, 

633 ) 

634 ) 

635 if preview is None: 635 ↛ 636line 635 didn't jump to line 636 because the condition on line 635 was never true

636 raise OperationError( 

637 tr("{value}コマ目の変形画像を生成できませんでした。").format(value=index + 1) 

638 ) 

639 image = self.window.canvas._cleared_selection_base( 

640 original 

641 ) 

642 painter = QPainter(image) 

643 painter.setCompositionMode( 

644 QPainter.CompositionMode.CompositionMode_SourceOver 

645 ) 

646 painter.drawImage(0, 0, preview) 

647 painter.end() 

648 

649 generated_images.append(image) 

650 

651 self.window.canvas.transform_points = [ 

652 QPointF(point) for point in final_points 

653 ] 

654 update_counter( 

655 progress, 

656 exposure + 1, 

657 total_steps, 

658 tr("生成した画像をタイムラインへ登録しています"), 

659 ) 

660 self.window.canvas._ensure_frame_count(end_column + 1) 

661 reedit = pending.get("reedit") or {} 

662 tween_id = reedit.get("id") or tween_groups.new_id() 

663 member_numbers = self._member_numbers( 

664 layer_index, key_column, exposure, reedit.get("numbers"), 

665 ) 

666 for offset, image in enumerate(generated_images): 

667 frame_index = key_column + offset 

668 layer = self.window.canvas.frames[frame_index].layers[layer_index] 

669 layer.image = image 

670 layer.has_content = True 

671 layer.is_blank_key = False 

672 layer.exposure = 1 

673 if offset == 0: 

674 layer.tween = tween_groups.make_spec( 

675 tween_id, exposure, transform_mode, reverse_generation, 

676 start_points, final_points, 

677 self.window.canvas.transform_mesh_cols, 

678 self.window.canvas.transform_mesh_rows, 

679 getattr( 

680 self.window.canvas, 

681 "transform_mesh_reference_points", [], 

682 ), 

683 ) 

684 layer.tween_member = None 

685 else: 

686 # 中割りは番号を持つ通常の絵として書き出される。 

687 # 直し直しでは、元の番号をそのまま使う(リテイク対策)。 

688 layer.sequence_number = member_numbers[offset - 1] 

689 layer.sequence_only = False 

690 layer.cell_name = None 

691 layer.tween = None 

692 layer.tween_member = tween_id 

693 

694 if undo_snapshot is None: 694 ↛ 695line 694 didn't jump to line 695 because the condition on line 694 was never true

695 raise OperationError( 

696 tr("トゥイーン開始前のUndo情報を取得できませんでした。") 

697 ) 

698 self.window.canvas.push_undo(DocUndo(undo_snapshot)) 

699 

700 update_counter( 

701 progress, 

702 total_steps, 

703 total_steps, 

704 tr("トゥイーンのキーフレーム化が完了しました"), 

705 ) 

706 except _OPERATION_ERRORS as exc: 

707 log.warning("tween keyframe generation failed: %s", exc, exc_info=True) 

708 # 途中生成に失敗した場合も、開始前の元画像へ戻す。 

709 if undo_snapshot is not None: 

710 self.window.canvas.apply_undo_entry(DocUndo(undo_snapshot)) 

711 else: 

712 self.window.canvas.transform_points = [ 

713 QPointF(point) for point in final_points 

714 ] 

715 self.window.canvas.cancel_selection_transform() 

716 self.window.canvas.tween_pending = None 

717 self.window.tools.set_tween_active(False) 

718 self.window.palette.setProperty("tweenActive", False) 

719 self._close_tween_command_popup() 

720 QMessageBox.warning( 

721 self.window, 

722 tr("トゥイーン確定エラー"), 

723 str(exc), 

724 ) 

725 return 

726 finally: 

727 QApplication.restoreOverrideCursor() 

728 close_counter(progress) 

729 

730 self.window.canvas._reset_selection_transform() 

731 self.window.canvas.transform_apply_all_frames = False 

732 self.window.canvas.tween_pending = None 

733 self.window.canvas.selection_polygon = [] 

734 self.window.canvas.selection_mask_override = None 

735 self.window.canvas.selection_outline_polygons = [] 

736 self.window.canvas.selection_mask_rect = None 

737 self.window.canvas.lasso = [] 

738 self.window.canvas.rect_start = None 

739 self.window.canvas.rect_end = None 

740 self.window.canvas.current_frame = ( 

741 key_column 

742 if reverse_generation 

743 else end_column 

744 ) 

745 self.window.canvas.active_layer_index = layer_index 

746 self.window.canvas._onion_cache.clear() 

747 self.window.canvas._color_filter_cache.clear() 

748 self.window.canvas._color_index_cache.clear() 

749 self.window.canvas._silhouette_cache.clear() 

750 self._close_tween_command_popup() 

751 

752 # 補間では使用色自体は増えないため、全コマ色走査は省略する。 

753 self.window._suppress_used_color_refresh_once = True 

754 self.window.canvas.changed.emit() 

755 self.window.canvas.selectionChanged.emit() 

756 self.window.canvas._update_selection_clear_overlay() 

757 self.window.canvas.update() 

758 quality_note = ( 

759 tr("(TPクオリティ適用)") 

760 if quality_active else "" 

761 ) 

762 direction_note = ( 

763 tr("逆生成(◆側が変形/右端が初期)") 

764 if reverse_generation 

765 else tr("通常生成(先頭が初期/♦側が変形)") 

766 ) 

767 self.window.statusBar().showMessage( 

768 tr("{exposure}コマの{name}トゥイーンを{note}で確定しました。{note2}帯をダブルクリックすると形を直せます。").format(exposure=exposure, name=mode_name, note=direction_note, note2=quality_note), 

769 4200, 

770 )