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

1"""Timeline structure editing for PaintCanvas: layers, frames and sequence numbers. 

2 

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) 

20 

21log = get_logger(__name__) 

22 

23 

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() 

38 

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() 

52 

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)) 

60 

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 

112 

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)) 

128 

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 

138 

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 

147 

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 ) 

180 

181 self._ensure_frame_count(required_count) 

182 

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 ) 

191 

192 for column, copied in reversed( 

193 explicit_cells 

194 ): 

195 self.frames[ 

196 column + amount 

197 ].layers[layer_index] = copied 

198 

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 

208 

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 

217 

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())) 

226 

227 for column, _copied in explicit_cells: 

228 self._clear_timeline_layer_cell( 

229 self.frames[column].layers[layer_index] 

230 ) 

231 

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 

236 

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() 

248 

249 

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 ) 

257 

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 

268 

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) 

276 

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 

309 

310 self._trim_unused_trailing_frames() 

311 self.current_frame = min(self.current_frame, len(self.frames) - 1) 

312 

313 self._cell_structure_dirty = True 

314 self._onion_cache.clear() 

315 self.changed.emit() 

316 self.selectionChanged.emit() 

317 self.update() 

318 

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() 

325 

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() 

339 

340 def timeline_step_columns(self, layer_index=None): 

341 """このレイヤーで有効なコマ開始位置一覧を返す。 

342 

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) 

351 

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 

362 

363 

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() 

375 

376 def current_exposure(self): 

377 return self.current_frame 

378 

379 

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 ) 

406 

407 def sequence_number_normalization(self, layer_index): 

408 """正規化したときの {旧番号: 新番号} を、データを変えずに返す。""" 

409 return normalization_mapping( 

410 self.frames, self._sequence_archive, layer_index 

411 ) 

412 

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 

440 

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)] 

458 

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 

499 

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 

548 

549 def sync_numbered_image_from_cell(self, frame_index, layer_index): 

550 """同じレイヤー・同じ絵番号を、1枚の絵(``Cell``)へ結び直す。 

551 

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 

574 

575 def link_layer_tracks(self): 

576 """同じレイヤーのセルを、1つの ``Track``(表示の設定)へ結ぶ。 

577 

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]) 

593 

594 def coalesce_numbered_images(self): 

595 """同一レイヤー・同一番号のセルを、1枚の絵(``Cell``)へ結ぶ。 

596 

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 

607 

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) 

615 

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) 

620 

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 

641 

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() 

709 

710 

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() 

720 

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 

736 

737 self.push_doc_undo() 

738 moving = source.clone() 

739 moving.exposure = max(1, int(source.exposure)) 

740 

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) 

755 

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 ) 

783 

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 

787 

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) 

869 

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 

905 

906 def _follow_blanks(self, layer_index, start, end, tail_blank=None): 

907 """動かしたセル(start〜end の手前)に合わせて空セルを付け直す。 

908 

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) 

947 

948 

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)