O writer tinha 4.199 linhas, das quais 3.300 numa única classe com dezoito
assuntos dentro. Achar o trecho de zoom exigia rolar por marcadores,
velocidade e legendas.
Agora é o pacote fcpxml/writer/, com um arquivo por assunto e o
FCPXMLModifier montado por composição de mixins. Mixins, e não objetos
separados, porque todas essas operações mexem no mesmo documento e nos
mesmos índices — separá-las em objetos independentes transformaria toda
chamada interna em travessia de fronteira sem nada em troca. A divisão que
importa aqui é de leitura, não de estado.
Nenhuma mudança de comportamento e nenhuma alteração nos ~50 pontos que
importam do writer: o __init__ re-exporta tudo, inclusive os nomes com
underscore que a suíte já usava.
core 723 carga, índices, navegação na spine, save
titles 600 títulos e legendas dinâmicas
cut 333 dividir, cortar faixas, apagar
speed 297 velocidade e zoom
(+ 20 módulos menores)
Único ajuste de chamada: quatro testes faziam patch em
fcpxml.writer.subprocess, que agora mora em writer.document (ver
Engine/docs/05_EXPERIENCIAS.md #23).
Lint zerado, 1441 testes passando.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
127 lines
4.4 KiB
Python
127 lines
4.4 KiB
Python
"""Reordenar clipes e recalcular offsets/duração.
|
|
|
|
Extraído de writer.py — ver fcpxml/writer/__init__.py para o conjunto.
|
|
"""
|
|
|
|
import xml.etree.ElementTree as ET
|
|
from typing import List
|
|
|
|
from ..models import (
|
|
TimeValue,
|
|
)
|
|
from .helpers import SPINE_ELEMENT_TAGS
|
|
|
|
|
|
class ReorderMixin:
|
|
"""Reordenar clipes e recalcular offsets/duração."""
|
|
|
|
# REORDER OPERATIONS
|
|
# ========================================================================
|
|
|
|
def reorder_clips(
|
|
self,
|
|
clip_ids: List[str],
|
|
target_position: str,
|
|
ripple: bool = True
|
|
) -> None:
|
|
"""
|
|
Move clips to a new position in the timeline.
|
|
|
|
Args:
|
|
clip_ids: Clips to move (maintains relative order)
|
|
target_position: 'start', 'end', timecode, or 'after:clip_id'/'before:clip_id'
|
|
ripple: Whether to shift other clips
|
|
"""
|
|
spine = self._get_spine()
|
|
|
|
# Collect clips to move
|
|
clips_to_move = []
|
|
for clip_id in clip_ids:
|
|
clip = self.clips.get(clip_id)
|
|
if clip is not None and clip in list(spine):
|
|
clips_to_move.append(clip)
|
|
|
|
if not clips_to_move:
|
|
raise ValueError(f"No clips found matching: {clip_ids}")
|
|
|
|
# Calculate total duration of moving clips
|
|
total_duration = TimeValue.zero()
|
|
for clip in clips_to_move:
|
|
dur = self._parse_time(clip.get('duration', '0s'))
|
|
total_duration = total_duration + dur
|
|
|
|
# Remove clips from current positions
|
|
for clip in clips_to_move:
|
|
spine.remove(clip)
|
|
|
|
# Determine target offset and insert index
|
|
spine_children = list(spine)
|
|
target_offset, insert_index = self._resolve_insert_position(
|
|
target_position, spine_children
|
|
)
|
|
|
|
# Insert clips at new position
|
|
current_offset = target_offset
|
|
for clip in clips_to_move:
|
|
clip.set('offset', current_offset.to_fcpxml())
|
|
spine.insert(insert_index, clip)
|
|
insert_index += 1
|
|
dur = self._parse_time(clip.get('duration', '0s'))
|
|
current_offset = current_offset + dur
|
|
|
|
# Recalculate all offsets if ripple
|
|
if ripple:
|
|
self._recalculate_offsets(spine)
|
|
|
|
def _recalculate_offsets(self, spine: ET.Element) -> None:
|
|
"""Recalculate all clip offsets sequentially."""
|
|
current_offset = TimeValue.zero()
|
|
|
|
for child in spine:
|
|
if child.tag in SPINE_ELEMENT_TAGS:
|
|
child.set('offset', current_offset.to_fcpxml())
|
|
duration_str = child.get('duration', '0s')
|
|
duration = self._parse_time(duration_str)
|
|
current_offset = current_offset + duration
|
|
|
|
def _timeline_duration(self) -> 'TimeValue':
|
|
"""Return the total timeline duration as a TimeValue.
|
|
|
|
Reads from the ``<sequence>`` element when available, falling back
|
|
to summing all spine element durations. Extracted from
|
|
``add_music_bed`` and ``batch_add_markers`` which both computed
|
|
this independently.
|
|
"""
|
|
sequence = self.root.find('.//sequence')
|
|
if sequence is not None:
|
|
dur_str = sequence.get('duration')
|
|
if dur_str:
|
|
return self._parse_time(dur_str)
|
|
spine = self._get_spine()
|
|
total = TimeValue.zero()
|
|
for child in spine:
|
|
if child.tag in SPINE_ELEMENT_TAGS:
|
|
total = total + self._parse_time(child.get('duration', '0s'))
|
|
return total
|
|
|
|
def _update_sequence_duration(self) -> None:
|
|
"""Recompute the ``<sequence>`` duration from the spine content.
|
|
|
|
Ripple edits (``cut_clip_ranges``, ``delete_clip``, ``split_clip``)
|
|
change the total timeline length without rewriting the sequence
|
|
element, so an exported file kept advertising the pre-edit duration —
|
|
a 326.78s sequence still claimed 326.78s after 71s of silence was
|
|
removed. This helper re-syncs the attribute to the actual spine sum.
|
|
"""
|
|
sequence = self.root.find('.//sequence')
|
|
if sequence is None:
|
|
return
|
|
spine = self._get_spine()
|
|
total = TimeValue.zero()
|
|
for child in spine:
|
|
if child.tag in SPINE_ELEMENT_TAGS:
|
|
total = total + self._parse_time(child.get('duration', '0s'))
|
|
sequence.set('duration', total.to_fcpxml())
|
|
|
|
# ========================================================================
|