Coverage for paintmaskanimator/canvas_timeline_ops.py: 62%
591 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"""Timeline structure editing for PaintCanvas: layers, frames and sequence numbers.
3Split out of ``canvas.py`` as a mixin. These methods change the *shape* of the
4document rather than pixels: adding/deleting layers and frames, shifting and
5moving timeline cells, exposure blocks, and the sequence-number bookkeeping that
6keeps numbered (imported) images consistent with the cells that reference them.
7They run against a live ``PaintCanvas``.
8"""
9# (nothing was used)
10from ._canvas_members import CanvasMembers
11from .cell_numbering import normalization_mapping
12from .models import Layer, make_frame, next_layer_name
13from .timeline import TimelineWidget
14from .utils import blank_image
15from .logging_setup import get_logger
16from .undo_entries import (
17 LayerInsertUndo,
18 LayerRemoveUndo,
19)
21log = get_logger(__name__)
24class TimelineStructureMixin(CanvasMembers):
25 def set_layer_visibility(self, li, on):
26 if li < 0: 26 ↛ 27line 26 didn't jump to line 27 because the condition on line 26 was never true
27 return
28 changed = False
29 for f in self.frames:
30 if li < len(f.layers) and f.layers[li].visible != bool(on): 30 ↛ 29line 30 didn't jump to line 29 because the condition on line 30 was always true
31 f.layers[li].visible = bool(on)
32 changed = True
33 if changed: 33 ↛ exitline 33 didn't return from function 'set_layer_visibility' because the condition on line 33 was always true
34 # Onion signatures include visibility. Keep images for the other
35 # layers (and for switching back); no pixels changed here.
36 self._playback_frame_cache.clear()
37 self.update()
39 def set_layer_opacity(self, li, opacity):
40 """表示用のレイヤー不透明度。画像内のRGBA値は変更しない。"""
41 if li < 0: 41 ↛ 42line 41 didn't jump to line 42 because the condition on line 41 was never true
42 return
43 opacity = max(0.0, min(1.0, float(opacity)))
44 changed = False
45 for frame in self.frames:
46 if li < len(frame.layers) and frame.layers[li].opacity != opacity: 46 ↛ 45line 46 didn't jump to line 45 because the condition on line 46 was always true
47 frame.layers[li].opacity = opacity
48 changed = True
49 if changed: 49 ↛ exitline 49 didn't return from function 'set_layer_opacity' because the condition on line 49 was always true
50 self._playback_frame_cache.clear()
51 self.update()
53 def add_layer(self):
54 # 全フレーム・全画像の文書スナップショットを作らず、
55 # 追加レイヤーだけをUndo対象にして大量コマ時の待ち時間を抑える。
56 old_active = int(self.active_layer_index)
57 insert_index = len(self.layers)
58 name = next_layer_name(layer.name for layer in self.layers)
59 self.push_undo(LayerRemoveUndo(insert_index, old_active))
61 # QImageの暗黙共有を使い、空画像バッファをコマ数分確保しない。
62 shared_blank = blank_image()
63 owner = None
64 for frame in self.frames:
65 added = Layer(
66 name,
67 shared_blank.copy(),
68 visible=True,
69 opacity=1.0,
70 )
71 # 足したレイヤーも、全コマで1つの Track を共有する。
72 if owner is None:
73 owner = added
74 else:
75 added.share_track_with(owner)
76 frame.layers.append(added)
77 self.active_layer_index = insert_index
78 self._onion_cache.clear()
79 self.changed.emit()
80 self.selectionChanged.emit()
81 self.update()
82 def delete_layer(self):
83 if len(self.layers) <= 1: 83 ↛ 84line 83 didn't jump to line 84 because the condition on line 83 was never true
84 return
85 index = int(self.active_layer_index)
86 target_active = max(0, index - 1)
87 stored_layers = [
88 frame.layers[index].clone()
89 for frame in self.frames
90 ]
91 self.push_undo(LayerInsertUndo(index, stored_layers, target_active))
92 for frame in self.frames:
93 frame.layers.pop(index)
94 self.active_layer_index = target_active
95 self._onion_cache.clear()
96 self.changed.emit()
97 self.selectionChanged.emit()
98 self.update()
99 @staticmethod
100 def _clear_timeline_layer_cell(layer):
101 # シート操作。同じ番号の他のコマの絵まで消さないよう、先に共有を切る。
102 layer.detach_cell()
103 layer.image = blank_image()
104 layer.has_content = False
105 layer.is_blank_key = False
106 layer.exposure = 1
107 layer.sequence_number = None
108 layer.sequence_only = False
109 layer.cell_name = None
110 layer.tween = None
111 layer.tween_member = None
113 def timeline_block_at(self, column, layer_index):
114 if not self.frames: 114 ↛ 115line 114 didn't jump to line 115 because the condition on line 114 was never true
115 return None
116 column = int(column)
117 layer_index = int(layer_index)
118 if not (0 <= column < len(self.frames)): 118 ↛ 119line 118 didn't jump to line 119 because the condition on line 118 was never true
119 return None
120 kind, start, exposure = TimelineWidget.timeline_span_at(
121 self.frames,
122 layer_index,
123 column,
124 )
125 if kind not in ("content", "blank") or start is None:
126 return None
127 return kind, int(start), max(1, int(exposure))
129 def _shift_timeline_layer_right(
130 self,
131 layer_index,
132 start_column,
133 amount,
134 ):
135 """指定レイヤーの明示コマを右へ移動し、時間の隙間を作る。"""
136 if not self.frames: 136 ↛ 137line 136 didn't jump to line 137 because the condition on line 136 was never true
137 return
139 layer_index = int(layer_index)
140 start_column = max(0, int(start_column))
141 amount = max(1, int(amount))
142 if not ( 142 ↛ 146line 142 didn't jump to line 146 because the condition on line 142 was never true
143 0 <= layer_index
144 < len(self.frames[0].layers)
145 ):
146 return
148 explicit_cells = []
149 required_count = start_column + amount
150 for column in range(
151 start_column,
152 len(self.frames),
153 ):
154 if ( 154 ↛ 158line 154 didn't jump to line 158 because the condition on line 154 was never true
155 layer_index
156 >= len(self.frames[column].layers)
157 ):
158 continue
159 layer = self.frames[column].layers[
160 layer_index
161 ]
162 if (
163 layer.has_content
164 or getattr(
165 layer,
166 "is_blank_key",
167 False,
168 )
169 ):
170 copied = layer.clone()
171 explicit_cells.append(
172 (column, copied)
173 )
174 required_count = max(
175 required_count,
176 column
177 + amount
178 + max(1, int(copied.exposure)),
179 )
181 self._ensure_frame_count(required_count)
183 # 先に元の明示コマをすべて未使用へ戻してから配置する。
184 # これにより、移動元と移動先が重なっても内容を失わない。
185 for column, _copied in explicit_cells:
186 self._clear_timeline_layer_cell(
187 self.frames[column].layers[
188 layer_index
189 ]
190 )
192 for column, copied in reversed(
193 explicit_cells
194 ):
195 self.frames[
196 column + amount
197 ].layers[layer_index] = copied
199 def _shift_timeline_layer_left(
200 self,
201 layer_index,
202 start_column,
203 amount=1,
204 ):
205 """指定位置以降の明示コマを左へ移動し、削除した時間を詰める。"""
206 if not self.frames: 206 ↛ 207line 206 didn't jump to line 207 because the condition on line 206 was never true
207 return
209 layer_index = int(layer_index)
210 start_column = max(0, int(start_column))
211 amount = max(1, int(amount))
212 if not ( 212 ↛ 216line 212 didn't jump to line 216 because the condition on line 212 was never true
213 0 <= layer_index
214 < len(self.frames[0].layers)
215 ):
216 return
218 explicit_cells = []
219 for column in range(start_column, len(self.frames)):
220 layer = self.frames[column].layers[layer_index]
221 if (
222 layer.has_content
223 or getattr(layer, "is_blank_key", False)
224 ):
225 explicit_cells.append((column, layer.clone()))
227 for column, _copied in explicit_cells:
228 self._clear_timeline_layer_cell(
229 self.frames[column].layers[layer_index]
230 )
232 for column, copied in explicit_cells:
233 target_column = column - amount
234 if target_column >= 0: 234 ↛ 232line 234 didn't jump to line 232 because the condition on line 234 was always true
235 self.frames[target_column].layers[layer_index] = copied
237 def _trim_unused_trailing_frames(self):
238 """全レイヤーで未使用になった末尾の時間列を取り除く。"""
239 while len(self.frames) > 1:
240 last_column = len(self.frames) - 1
241 if any( 241 ↛ 246line 241 didn't jump to line 246 because the condition on line 241 was never true
242 self.timeline_block_at(last_column, layer_index)
243 is not None
244 for layer_index in range(len(self.frames[0].layers))
245 ):
246 break
247 self.frames.pop()
250 def add_frame(self, dup):
251 # 互換用。空追加は選択中コマの直後へ○を挿入する。
252 if not dup:
253 return self.create_blank_key(
254 self.current_frame,
255 self.active_layer_index,
256 )
258 self.push_doc_undo()
259 source_frame = self.frames[self.current_frame]
260 new_frame = source_frame.clone()
261 at = self.current_frame + 1
262 self.frames.insert(at, new_frame)
263 self.current_frame = at
264 self.changed.emit()
265 self.selectionChanged.emit()
266 self.update()
267 return True
269 def delete_frame(self):
270 """選択レイヤーの表示コマを1つ削除する。"""
271 if not self.frames: 271 ↛ 272line 271 didn't jump to line 272 because the condition on line 271 was never true
272 return
273 layer_index = int(self.active_layer_index)
274 current = int(self.current_frame)
275 block = self.timeline_block_at(current, layer_index)
277 self.push_doc_undo()
278 if block is not None: 278 ↛ 279line 278 didn't jump to line 279 because the condition on line 278 was never true
279 _kind, start, exposure = block
280 layer = self.frames[start].layers[layer_index]
281 if layer.has_content and layer.sequence_number is not None:
282 self._sequence_archive[
283 (layer_index, int(layer.sequence_number))
284 ] = layer.clone()
285 if exposure > 1:
286 layer.exposure = exposure - 1
287 self._shift_timeline_layer_left(
288 layer_index,
289 start + exposure,
290 )
291 else:
292 self._clear_timeline_layer_cell(layer)
293 self._shift_timeline_layer_left(
294 layer_index,
295 start + 1,
296 )
297 self.current_frame = min(current, len(self.frames) - 1)
298 elif len(self.frames) > 1: 298 ↛ 306line 298 didn't jump to line 306 because the condition on line 298 was always true
299 # ほかのレイヤーの時間列は動かさず、選択レイヤーだけを詰める。
300 self._shift_timeline_layer_left(
301 layer_index,
302 current + 1,
303 )
304 self.current_frame = current
305 else:
306 if self.undo_stack:
307 self.undo_stack.pop()
308 return
310 self._trim_unused_trailing_frames()
311 self.current_frame = min(self.current_frame, len(self.frames) - 1)
313 self._cell_structure_dirty = True
314 self._onion_cache.clear()
315 self.changed.emit()
316 self.selectionChanged.emit()
317 self.update()
319 def previous_frame(self):
320 if not self.frames:
321 return
322 self.current_frame = max(0, self.current_frame - 1)
323 self.selectionChanged.emit()
324 self.update()
326 def next_frame(self):
327 if not self.frames:
328 return
329 next_column = self.current_frame + 1
330 old_count = len(self.frames)
331 self._ensure_frame_count(next_column + 1)
332 self.current_frame = next_column
333 # 追加されたセルは未使用のまま。○の空キーフレームにはしない。
334 if len(self.frames) != old_count:
335 self._cell_structure_dirty = True
336 self.changed.emit()
337 self.selectionChanged.emit()
338 self.update()
340 def timeline_step_columns(self, layer_index=None):
341 """このレイヤーで有効なコマ開始位置一覧を返す。
343 内容キーフレーム(●)だけでなく、空フレーム(○)も
344 1つのコマ開始位置として扱う。
345 """
346 if not self.frames: 346 ↛ 347line 346 didn't jump to line 347 because the condition on line 346 was never true
347 return []
348 if layer_index is None:
349 layer_index = self.active_layer_index
350 layer_index = int(layer_index)
352 columns = []
353 # A span starts exactly at an explicit content/blank key. Resolving
354 # every column backwards makes navigation quadratic on sparse sheets.
355 for column, frame in enumerate(self.frames):
356 if not 0 <= layer_index < len(frame.layers):
357 continue
358 layer = frame.layers[layer_index]
359 if not layer.sequence_only and (layer.has_content or layer.is_blank_key):
360 columns.append(column)
361 return columns
364 def set_duration(self, d):
365 layer = self.active_layer
366 if not layer.has_content:
367 source = self.resolve_key_frame(self.current_frame, self.active_layer_index)
368 if source is not None:
369 self.frames[source].layers[self.active_layer_index].exposure = max(1, int(d))
370 else:
371 layer.exposure = max(1, int(d))
372 else:
373 layer.exposure = max(1, int(d))
374 self.changed.emit()
376 def current_exposure(self):
377 return self.current_frame
380 def resolve_exposure_block(self, column, layer_index):
381 """●ーーーー|を1つのキーフレーム露出ブロックとして返す。"""
382 if not self.frames:
383 return None
384 column = max(0, min(int(column), len(self.frames) - 1))
385 layer_index = int(layer_index)
386 key_column = self.resolve_key_frame(column, layer_index)
387 if key_column is None:
388 return None
389 if not (
390 0 <= key_column < len(self.frames)
391 and 0 <= layer_index
392 < len(self.frames[key_column].layers)
393 ):
394 return None
395 key_layer = self.frames[key_column].layers[layer_index]
396 exposure = max(1, int(key_layer.exposure))
397 end_column = min(
398 len(self.frames) - 1,
399 key_column + exposure - 1,
400 )
401 return (
402 int(key_column),
403 int(end_column),
404 int(exposure),
405 )
407 def sequence_number_normalization(self, layer_index):
408 """正規化したときの {旧番号: 新番号} を、データを変えずに返す。"""
409 return normalization_mapping(
410 self.frames, self._sequence_archive, layer_index
411 )
413 def normalize_sequence_numbers(self, layer_index=None):
414 """シートの登場順で絵番号を正規化し、連番にも反映する。"""
415 if not self.frames: 415 ↛ 416line 415 didn't jump to line 416 because the condition on line 415 was never true
416 return
417 if layer_index is None:
418 layer_indices = range(len(self.frames[0].layers))
419 else:
420 layer_indices = (int(layer_index),)
421 for target_layer_index in layer_indices:
422 mapping = self.sequence_number_normalization(target_layer_index)
423 next_number = len(mapping) + 1
424 for frame in self.frames:
425 layer = frame.layers[target_layer_index]
426 if layer.sequence_number is not None:
427 layer.sequence_number = mapping[int(layer.sequence_number)]
428 elif layer.has_content and not layer.sequence_only: 428 ↛ 429line 428 didn't jump to line 429 because the condition on line 428 was never true
429 layer.sequence_number = next_number
430 next_number += 1
431 normalized_archive = {}
432 for (archived_layer, old_number), archived in self._sequence_archive.items(): 432 ↛ 433line 432 didn't jump to line 433 because the loop on line 432 never started
433 if int(archived_layer) == target_layer_index:
434 new_number = mapping.get(int(old_number), int(old_number))
435 archived.sequence_number = new_number
436 else:
437 new_number = int(old_number)
438 normalized_archive[(int(archived_layer), new_number)] = archived
439 self._sequence_archive = normalized_archive
441 def sequence_entry_columns(self, layer_index):
442 """絵番号ごとの代表セル位置を番号順で返す。"""
443 entries = {}
444 for column, frame in enumerate(self.frames):
445 if not (0 <= layer_index < len(frame.layers)): 445 ↛ 446line 445 didn't jump to line 446 because the condition on line 445 was never true
446 continue
447 layer = frame.layers[layer_index]
448 number = layer.sequence_number
449 if (
450 number is not None
451 and int(number) >= 1
452 and (layer.has_content or layer.is_blank_key)
453 ):
454 number = int(number)
455 if number not in entries or layer.sequence_only:
456 entries[number] = column
457 return [entries[number] for number in sorted(entries)]
459 def insert_sequence_blank(self, layer_index, after_number):
460 """選択番号の直後へ空画像番号を挿入し、後続番号を送る。"""
461 layer_index = int(layer_index)
462 entries = self.sequence_entry_columns(layer_index)
463 insert_number = (
464 1 if not entries else max(1, int(after_number) + 1)
465 )
466 self.push_doc_undo()
467 # 同じ番号は同じ画像を指すため、シート上の重複参照も含めて
468 # 挿入位置以降を一括で繰り下げる。
469 for frame in self.frames:
470 layer = frame.layers[layer_index]
471 if (
472 layer.sequence_number is not None
473 and int(layer.sequence_number) >= insert_number
474 ):
475 layer.sequence_number = int(layer.sequence_number) + 1
476 shifted_archive = {}
477 for (archived_layer, number), archived in self._sequence_archive.items():
478 if archived_layer == layer_index and int(number) >= insert_number:
479 archived.sequence_number = int(number) + 1
480 number = int(number) + 1
481 shifted_archive[(archived_layer, int(number))] = archived
482 self._sequence_archive = shifted_archive
483 new_column = len(self.frames)
484 self._ensure_frame_count(new_column + 1)
485 target = self.frames[new_column].layers[layer_index]
486 target.image = blank_image()
487 target.has_content = False
488 target.is_blank_key = True
489 target.sequence_number = insert_number
490 target.sequence_only = True
491 target.exposure = 1
492 self.current_frame = new_column
493 self.active_layer_index = layer_index
494 self._cell_structure_dirty = True
495 self.changed.emit()
496 self.selectionChanged.emit()
497 self.update()
498 return True
500 def move_sequence_image(self, layer_index, first_number, second_number):
501 """連番画像を差し込み移動し、間の画像を1コマずつ送る。"""
502 first_number = int(first_number)
503 second_number = int(second_number)
504 if first_number == second_number:
505 return False
506 columns = self.sequence_entry_columns(int(layer_index))
507 by_number = {
508 int(self.frames[column].layers[layer_index].sequence_number): column
509 for column in columns
510 }
511 if first_number not in by_number or second_number not in by_number:
512 return False
513 self.push_doc_undo()
514 low, high = sorted((first_number, second_number))
515 ordered_numbers = list(range(low, high + 1))
516 snapshots = {}
517 for number in ordered_numbers:
518 if number not in by_number:
519 return False
520 layer = self.frames[by_number[number]].layers[layer_index]
521 snapshots[number] = (
522 layer.image.copy(), bool(layer.has_content),
523 bool(layer.is_blank_key), getattr(layer, "cell_name", None)
524 )
525 if first_number < second_number:
526 source_for_number = {
527 number: number + 1 for number in range(first_number, second_number)
528 }
529 else:
530 source_for_number = {
531 number: number - 1 for number in range(second_number + 1, first_number + 1)
532 }
533 source_for_number[second_number] = first_number
534 for frame in self.frames:
535 layer = frame.layers[layer_index]
536 number = layer.sequence_number
537 if number in source_for_number:
538 image, has_content, is_blank, cell_name = snapshots[
539 source_for_number[number]
540 ]
541 layer.image = image.copy()
542 layer.has_content = has_content
543 layer.is_blank_key = is_blank
544 layer.cell_name = cell_name
545 self.changed.emit()
546 self.update()
547 return True
549 def sync_numbered_image_from_cell(self, frame_index, layer_index):
550 """同じレイヤー・同じ絵番号を、1枚の絵(``Cell``)へ結び直す。
552 結んだあとは、どのセルに書き込んでも同じ絵として扱われる
553 (Issue #31 Phase 1)。描く直前に呼んで、ストロークが同じ番号の
554 全コマへ出るようにする。
555 """
556 source = self.frames[int(frame_index)].layers[int(layer_index)]
557 if source.sequence_number is None or not source.has_content:
558 return
559 for index, frame in enumerate(self.frames):
560 if index == int(frame_index):
561 continue
562 target = frame.layers[int(layer_index)]
563 if target.sequence_number == source.sequence_number:
564 target.share_cell_with(source)
565 target.has_content = True
566 target.is_blank_key = False
567 archived = self._sequence_archive.get((
568 int(layer_index), int(source.sequence_number)
569 ))
570 if archived is not None: 570 ↛ 571line 570 didn't jump to line 571 because the condition on line 570 was never true
571 archived.share_cell_with(source)
572 archived.has_content = True
573 archived.is_blank_key = False
575 def link_layer_tracks(self):
576 """同じレイヤーのセルを、1つの ``Track``(表示の設定)へ結ぶ。
578 先頭コマのセルが持つ設定を、そのレイヤーの設定とする。レイヤーの
579 追加・削除・並べ替えや、読み込み・Undo の復元のあとに呼ぶ
580 (Issue #31 Phase 2)。
581 """
582 if not self.frames: 582 ↛ 583line 582 didn't jump to line 583 because the condition on line 582 was never true
583 return
584 owners = list(self.frames[0].layers)
585 for frame in self.frames[1:]:
586 for layer_index, layer in enumerate(frame.layers):
587 if layer_index < len(owners): 587 ↛ 586line 587 didn't jump to line 586 because the condition on line 587 was always true
588 layer.share_track_with(owners[layer_index])
589 for key, archived in self._sequence_archive.items():
590 layer_index = int(key[0])
591 if 0 <= layer_index < len(owners): 591 ↛ 589line 591 didn't jump to line 589 because the condition on line 591 was always true
592 archived.share_track_with(owners[layer_index])
594 def coalesce_numbered_images(self):
595 """同一レイヤー・同一番号のセルを、1枚の絵(``Cell``)へ結ぶ。
597 読み込み・取り込み・Undo の復元で、同じ番号のセルがそれぞれ別の絵を
598 持った状態から呼ぶ。結んだあとは、どのセルへ書き込んでも同じ番号の
599 全コマに出る(Issue #31 Phase 1)。
600 """
601 owners = {}
602 preferred = int(self.current_frame)
603 if 0 <= preferred < len(self.frames): 603 ↛ 608line 603 didn't jump to line 608 because the condition on line 603 was always true
604 for layer_index, layer in enumerate(self.frames[preferred].layers):
605 if layer.has_content and layer.sequence_number is not None:
606 owners[(layer_index, int(layer.sequence_number))] = layer
608 for frame in self.frames:
609 for layer_index, layer in enumerate(frame.layers):
610 if not layer.has_content or layer.sequence_number is None:
611 continue
612 key = (layer_index, int(layer.sequence_number))
613 owner = owners.setdefault(key, layer)
614 layer.share_cell_with(owner)
616 for key, archived in self._sequence_archive.items():
617 owner = owners.get((int(key[0]), int(key[1])))
618 if owner is not None:
619 archived.share_cell_with(owner)
621 def delete_sequence_entry(self, layer_index, number):
622 """連番画像を削除し、シート側の参照セルを未使用へ戻す。"""
623 number = int(number)
624 self.push_doc_undo()
625 found = False
626 for frame in self.frames:
627 layer = frame.layers[int(layer_index)]
628 if layer.sequence_number == number:
629 self._clear_timeline_layer_cell(layer)
630 found = True
631 if not found:
632 self.undo_stack.pop()
633 return False
634 self._trim_unused_trailing_frames()
635 self.current_frame = min(self.current_frame, len(self.frames) - 1)
636 self._cell_structure_dirty = True
637 self.changed.emit()
638 self.selectionChanged.emit()
639 self.update()
640 return True
642 def apply_sequence_only_entries(self):
643 """連番で追加した番号をシートの保持区間、または末尾へ反映する。"""
644 pending = []
645 for column, frame in enumerate(self.frames):
646 for layer_index, layer in enumerate(frame.layers):
647 if layer.sequence_only and layer.sequence_number is not None: 647 ↛ 648line 647 didn't jump to line 648 because the condition on line 647 was never true
648 pending.append((int(layer.sequence_number), layer_index, column, layer.clone()))
649 for number, layer_index, source_column, copied in sorted(pending): 649 ↛ 650line 649 didn't jump to line 650 because the loop on line 649 never started
650 target_exposure = 1
651 previous = None
652 for column, frame in enumerate(self.frames):
653 layer = frame.layers[layer_index]
654 if (
655 not layer.sequence_only
656 and layer.sequence_number == number - 1
657 and (layer.has_content or layer.is_blank_key)
658 ):
659 previous = (column, layer)
660 break
661 target_column = None
662 if previous is not None:
663 start, previous_layer = previous
664 exposure = max(1, int(previous_layer.exposure))
665 if exposure > 1:
666 # 保持区間の長さは変えず、前半を既存番号、後半を
667 # 追加番号へ分配する。
668 target_column = start + max(1, exposure // 2)
669 previous_layer.exposure = target_column - start
670 target_exposure = start + exposure - target_column
671 if target_column is None:
672 visible_columns = [
673 column
674 for column, frame in enumerate(self.frames)
675 if any(
676 (layer.has_content or layer.is_blank_key)
677 and not layer.sequence_only
678 for layer in frame.layers
679 )
680 ]
681 target_column = (max(visible_columns) + 1) if visible_columns else 0
682 self._ensure_frame_count(target_column + 1)
683 target = self.frames[target_column].layers[layer_index]
684 if target.has_content or target.is_blank_key:
685 target_column = len(self.frames)
686 self._ensure_frame_count(target_column + 1)
687 target = self.frames[target_column].layers[layer_index]
688 target.image = copied.image.copy()
689 target.has_content = bool(copied.has_content)
690 target.is_blank_key = bool(
691 copied.is_blank_key and not copied.has_content
692 )
693 target.sequence_number = number
694 target.cell_name = getattr(copied, "cell_name", None)
695 target.sequence_only = False
696 target.exposure = max(1, int(target_exposure))
697 target.visible = copied.visible
698 target.opacity = copied.opacity
699 self._clear_timeline_layer_cell(
700 self.frames[source_column].layers[layer_index]
701 )
702 if pending: 702 ↛ 703line 702 didn't jump to line 703 because the condition on line 702 was never true
703 self._trim_unused_trailing_frames()
704 self.current_frame = min(self.current_frame, len(self.frames) - 1)
705 self._cell_structure_dirty = True
706 self.changed.emit()
707 self.selectionChanged.emit()
708 self.update()
711 def flip_active_layer(self, horizontal=True):
712 if self.active_layer.is_paper:
713 return
714 self.ensure_editable_key()
715 self.push_layer_undo()
716 self.active_layer.image = self.active_layer.image.mirrored(horizontal, not horizontal)
717 self.active_layer.has_content = True
718 self.cellChanged.emit(self.current_frame, self.active_layer_index)
719 self.update()
721 def move_timeline_cell(self, source_frame, source_layer, destination_frame, destination_layer):
722 source_frame = int(source_frame)
723 destination_frame = int(destination_frame)
724 source_layer = int(source_layer)
725 destination_layer = int(destination_layer)
726 if not (0 <= source_frame < len(self.frames)) or destination_frame < 0: 726 ↛ 727line 726 didn't jump to line 727 because the condition on line 726 was never true
727 return False
728 if not (0 <= source_layer < len(self.frames[source_frame].layers)): 728 ↛ 729line 728 didn't jump to line 729 because the condition on line 728 was never true
729 return False
730 source = self.frames[source_frame].layers[source_layer]
731 source_is_blank = bool(
732 getattr(source, "is_blank_key", False)
733 )
734 if not (source.has_content or source_is_blank): 734 ↛ 735line 734 didn't jump to line 735 because the condition on line 734 was never true
735 return False
737 self.push_doc_undo()
738 moving = source.clone()
739 moving.exposure = max(1, int(source.exposure))
741 # 同一レイヤー内で後方へ移動するときは、元セルを抜いた分だけ座標を補正。
742 source_span = max(1, int(source.exposure))
743 # セルの直後にくっついた空セル(✗)は、セルと一緒に動かす。
744 tail_blank = None
745 old_end = source_frame + source_span
746 if (
747 not source_is_blank
748 and source_layer == destination_layer
749 and old_end < len(self.frames)
750 ):
751 tail = self.frames[old_end].layers[source_layer]
752 if getattr(tail, "is_blank_key", False) and not tail.has_content: 752 ↛ 754line 752 didn't jump to line 754 because the condition on line 752 was always true
753 tail_blank = (old_end, old_end + max(1, int(tail.exposure)))
754 self._clear_timeline_layer_cell(source)
756 # 隣接する「●ーー」の直後のキーを移動した場合、抜けた位置は
757 # 空セルにせず直前キーの保持区間(ー)として埋める。
758 previous_key = None
759 for index in range(source_frame - 1, -1, -1):
760 candidate = self.frames[index].layers[source_layer]
761 if (
762 candidate.has_content
763 or getattr(candidate, "is_blank_key", False)
764 ):
765 previous_key = index
766 break
767 if previous_key is not None:
768 previous = self.frames[previous_key].layers[source_layer]
769 previous_end = previous_key + max(1, int(previous.exposure))
770 if previous_end >= source_frame: 770 ↛ 784line 770 didn't jump to line 784 because the condition on line 770 was always true
771 fill_until = source_frame + source_span
772 if ( 772 ↛ 778line 772 didn't jump to line 778 because the condition on line 772 was never true
773 source_layer == destination_layer
774 and destination_frame > source_frame
775 ):
776 # キーを右へずらした距離全体を直前キーの保持で埋める。
777 # 移動元の1コマ分だけ延長すると途中が未使用になる。
778 fill_until = max(fill_until, destination_frame)
779 previous.exposure = max(
780 int(previous.exposure),
781 fill_until - previous_key,
782 )
784 self._ensure_frame_count(destination_frame + 1)
785 if not (0 <= destination_layer < len(self.frames[destination_frame].layers)): 785 ↛ 786line 785 didn't jump to line 786 because the condition on line 785 was never true
786 return False
788 occupying_block = self.timeline_block_at(
789 destination_frame,
790 destination_layer,
791 )
792 if occupying_block is not None:
793 _occupying_kind, key, _occupying_exposure = (
794 occupying_block
795 )
796 if int(key) != source_frame: 796 ↛ 870line 796 didn't jump to line 870 because the condition on line 796 was always true
797 occupied = self.frames[key].layers[
798 destination_layer
799 ]
800 if key < destination_frame: 800 ↛ 802line 800 didn't jump to line 802 because the condition on line 800 was never true
801 # 保持区間上へ落とした場合は落下位置で切る。
802 occupied.exposure = max(
803 1,
804 destination_frame - key,
805 )
806 elif key == destination_frame: 806 ↛ 870line 806 didn't jump to line 870 because the condition on line 806 was always true
807 # 既存の●/○以降を右へ1セル送る。
808 keys = [
809 index
810 for index in range(len(self.frames))
811 if (
812 (
813 self.frames[index]
814 .layers[destination_layer]
815 .has_content
816 )
817 or getattr(
818 self.frames[index]
819 .layers[destination_layer],
820 "is_blank_key",
821 False,
822 )
823 )
824 and index >= destination_frame
825 ]
826 self._ensure_frame_count(
827 len(self.frames) + 1
828 )
829 for index in reversed(keys):
830 target = index + 1
831 self._ensure_frame_count(target + 1)
832 src = self.frames[index].layers[
833 destination_layer
834 ]
835 dst = self.frames[target].layers[
836 destination_layer
837 ]
838 copied = src.clone()
839 # 画素は変わらないので複製しない(暗黙共有で十分)。
840 dst.image = copied.image
841 dst.has_content = bool(
842 copied.has_content
843 )
844 dst.is_blank_key = bool(
845 getattr(
846 copied,
847 "is_blank_key",
848 False,
849 )
850 )
851 dst.exposure = int(copied.exposure)
852 dst.visible = bool(copied.visible)
853 dst.opacity = float(copied.opacity)
854 dst.alpha_locked = bool(
855 copied.alpha_locked
856 )
857 dst.color_filter_enabled = bool(
858 copied.color_filter_enabled
859 )
860 dst.color_filter_rgb = (
861 tuple(copied.color_filter_rgb)
862 if copied.color_filter_rgb is not None
863 else None
864 )
865 dst.sequence_number = copied.sequence_number
866 dst.cell_name = getattr(copied, "cell_name", None)
867 dst.sequence_only = bool(copied.sequence_only)
868 self._clear_timeline_layer_cell(src)
870 destination = self.frames[destination_frame].layers[destination_layer]
871 destination.image = (
872 moving.image
873 if not source_is_blank
874 else blank_image()
875 )
876 destination.has_content = not source_is_blank
877 destination.is_blank_key = source_is_blank
878 destination.sequence_number = (
879 None if source_is_blank else moving.sequence_number
880 )
881 destination.cell_name = (
882 None if source_is_blank else getattr(moving, "cell_name", None)
883 )
884 destination.sequence_only = bool(moving.sequence_only)
885 destination.exposure = moving.exposure
886 destination.visible = moving.visible
887 destination.opacity = moving.opacity
888 destination.color_filter_enabled = moving.color_filter_enabled
889 destination.color_filter_rgb = (
890 tuple(moving.color_filter_rgb)
891 if moving.color_filter_rgb is not None else None
892 )
893 new_end = destination_frame + max(1, int(moving.exposure))
894 if not source_is_blank: 894 ↛ 898line 894 didn't jump to line 898 because the condition on line 894 was always true
895 self._follow_blanks(
896 destination_layer, destination_frame, new_end, tail_blank,
897 )
898 self.current_frame = destination_frame
899 self.active_layer_index = destination_layer
900 self._onion_cache.clear()
901 self.changed.emit()
902 self.selectionChanged.emit()
903 self.update()
904 return True
906 def _follow_blanks(self, layer_index, start, end, tail_blank=None):
907 """動かしたセル(start〜end の手前)に合わせて空セルを付け直す。
909 重なった空セルはセルの終わりまで押し出し(空セルの終わりは動かさない)、
910 元の直後にくっついていた空セルは、セルの新しい終わりから始める。
911 """
912 blank_end = None
913 for column in range(start + 1, min(end, len(self.frames))):
914 layer = self.frames[column].layers[layer_index]
915 if getattr(layer, "is_blank_key", False) and not layer.has_content:
916 blank_end = max(
917 blank_end or 0, column + max(1, int(layer.exposure))
918 )
919 self._clear_timeline_layer_cell(layer)
920 if tail_blank is not None:
921 tail_start, tail_end = tail_blank
922 if tail_start != end: 922 ↛ 927line 922 didn't jump to line 927 because the condition on line 922 was always true
923 tail = self.frames[tail_start].layers[layer_index]
924 if tail_start >= end and getattr(tail, "is_blank_key", False):
925 self._clear_timeline_layer_cell(tail)
926 blank_end = max(blank_end or 0, tail_end)
927 if blank_end is None or blank_end <= end:
928 return
929 # 押し出した先から次のキーまでの間だけを空セルにする。
930 next_key = next(
931 (
932 column for column in range(end, len(self.frames))
933 if self.frames[column].layers[layer_index].has_content
934 or getattr(self.frames[column].layers[layer_index], "is_blank_key", False)
935 ),
936 None,
937 )
938 if next_key is not None and next_key == end: 938 ↛ 939line 938 didn't jump to line 939 because the condition on line 938 was never true
939 return
940 if next_key is not None: 940 ↛ 942line 940 didn't jump to line 942 because the condition on line 940 was always true
941 blank_end = min(blank_end, next_key)
942 self._ensure_frame_count(end + 1)
943 blank = self.frames[end].layers[layer_index]
944 self._clear_timeline_layer_cell(blank)
945 blank.is_blank_key = True
946 blank.exposure = max(1, blank_end - end)
949 def _ensure_frame_count(self, count):
950 if count <= len(self.frames):
951 return
952 names = [layer.name for layer in self.frames[0].layers]
953 template = self.frames[0].layers
954 while len(self.frames) < count:
955 frame = make_frame(names)
956 for index, layer in enumerate(frame.layers):
957 # 表示の設定はレイヤーに1つ。写すのではなく同じ Track を指す
958 # (Issue #31 Phase 2)。
959 layer.share_track_with(template[index])
960 self.frames.append(frame)