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
« 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``.
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.
7A collaborator rather than a mixin -- see ``main_window_export.py`` for why.
8"""
9from typing import TYPE_CHECKING
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
22if TYPE_CHECKING:
23 from .main_window import MainWindow
25log = get_logger(__name__)
28class TweenController:
29 """Owned by ``MainWindow`` as ``window.tween``.
31 A collaborator rather than a mixin -- see main_window_export.py for why.
32 """
34 def __init__(self, window: "MainWindow"):
35 self.window = window
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 )
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()
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()
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
121 reverse = bool(enabled)
122 pending["reverse_generation"] = reverse
123 self._refresh_timeline_tween_marker()
124 self.window.canvas.setFocus()
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 )
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
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
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()
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 )
189 def _start(
190 self, layer_index, key_column, exposure, mode, tween_undo_snapshot,
191 reverse=False, shape=None, reedit=None,
192 ):
193 """キーのコマで変形を始め、トゥイーンの編集中にする。
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
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")
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
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
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()
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 )
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()
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))
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 )
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 )
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()
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 )
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
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
506 undo_snapshot = pending.get("undo_snapshot")
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 }
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 )
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 )
544 self.window.canvas._invalidate_tp_preview_cache()
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
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 )
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 )
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()
649 generated_images.append(image)
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
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))
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)
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()
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 )