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>
126 lines
4.4 KiB
Python
126 lines
4.4 KiB
Python
"""Aparar clipes e propagar o ripple pela spine.
|
|
|
|
Extraído de writer.py — ver fcpxml/writer/__init__.py para o conjunto.
|
|
"""
|
|
|
|
import xml.etree.ElementTree as ET
|
|
from typing import Optional
|
|
|
|
from ..models import (
|
|
TimeValue,
|
|
)
|
|
from .helpers import SPINE_ELEMENT_TAGS
|
|
|
|
|
|
class TrimMixin:
|
|
"""Aparar clipes e propagar o ripple pela spine."""
|
|
|
|
# ========================================================================
|
|
# TRIM OPERATIONS
|
|
# ========================================================================
|
|
|
|
def trim_clip(
|
|
self,
|
|
clip_id: str,
|
|
trim_start: Optional[str] = None,
|
|
trim_end: Optional[str] = None,
|
|
ripple: bool = True
|
|
) -> ET.Element:
|
|
"""
|
|
Trim a clip's in-point and/or out-point.
|
|
|
|
Args:
|
|
clip_id: Target clip
|
|
trim_start: New in-point or delta ('+1s', '-10f')
|
|
trim_end: New out-point or delta
|
|
ripple: Whether to shift subsequent clips
|
|
|
|
Returns:
|
|
Modified clip element
|
|
"""
|
|
clip = self._require_clip(clip_id)
|
|
|
|
current_start, current_duration, _ = self._get_clip_times(clip)
|
|
|
|
original_duration = current_duration
|
|
|
|
# Handle trim_start
|
|
if trim_start:
|
|
if trim_start.startswith('+') or trim_start.startswith('-'):
|
|
delta = self._parse_time(trim_start[1:])
|
|
if trim_start.startswith('-'):
|
|
# Extend earlier
|
|
new_start = current_start - delta
|
|
new_duration = current_duration + delta
|
|
else:
|
|
# Trim later
|
|
new_start = current_start + delta
|
|
new_duration = current_duration - delta
|
|
else:
|
|
new_start = self._parse_time(trim_start)
|
|
diff = new_start - current_start
|
|
new_duration = current_duration - diff
|
|
|
|
clip.set('start', new_start.to_fcpxml())
|
|
current_start = new_start
|
|
current_duration = new_duration
|
|
|
|
# Handle trim_end
|
|
if trim_end:
|
|
if trim_end.startswith('+') or trim_end.startswith('-'):
|
|
delta = self._parse_time(trim_end[1:])
|
|
if trim_end.startswith('-'):
|
|
new_duration = current_duration - delta
|
|
else:
|
|
new_duration = current_duration + delta
|
|
else:
|
|
end_point = self._parse_time(trim_end)
|
|
new_duration = end_point - current_start
|
|
|
|
current_duration = new_duration
|
|
|
|
if current_duration <= TimeValue.zero():
|
|
raise ValueError(
|
|
f"Trim would produce non-positive duration "
|
|
f"({current_duration.to_seconds():.3f}s) for clip '{clip_id}'"
|
|
)
|
|
|
|
clip.set('duration', current_duration.to_fcpxml())
|
|
|
|
# Ripple subsequent clips if needed
|
|
if ripple:
|
|
duration_change = current_duration - original_duration
|
|
if duration_change != TimeValue.zero():
|
|
self._ripple_after_clip(clip, duration_change)
|
|
|
|
return clip
|
|
|
|
def _ripple_from_index(
|
|
self, spine: ET.Element, start_index: int, delta: 'TimeValue'
|
|
) -> None:
|
|
"""Shift the offset of every spine element from *start_index* onward by *delta*.
|
|
|
|
Consolidates the ripple loops previously duplicated across
|
|
``_ripple_after_clip``, ``delete_clip``, and ``insert_clip``.
|
|
|
|
Args:
|
|
spine: The primary storyline ``<spine>`` element.
|
|
start_index: First child index to adjust (inclusive).
|
|
delta: Signed time shift (positive = later, negative = earlier).
|
|
"""
|
|
children = list(spine)
|
|
for child in children[start_index:]:
|
|
if child.tag in SPINE_ELEMENT_TAGS:
|
|
current_offset = self._parse_time(child.get('offset', '0s'))
|
|
new_offset = current_offset + delta
|
|
child.set('offset', new_offset.to_fcpxml())
|
|
|
|
def _ripple_after_clip(self, target_clip: ET.Element, delta: TimeValue) -> None:
|
|
"""Shift all clips after the given clip by delta."""
|
|
spine = self._get_spine()
|
|
clip_index = self._find_clip_index(spine, target_clip)
|
|
if clip_index is not None:
|
|
self._ripple_from_index(spine, clip_index + 1, delta)
|
|
|
|
# ========================================================================
|