refactor: writer.py vira pacote, um módulo por assunto
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>
This commit is contained in:
co-authored by
Claude Opus 5
parent
1bebee4359
commit
4f5cf94443
@@ -0,0 +1,125 @@
|
||||
"""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)
|
||||
|
||||
# ========================================================================
|
||||
Reference in New Issue
Block a user