Files
gart/code/fcpxml/writer/trim.py
João HenriqueandClaude Opus 5 4f5cf94443 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>
2026-08-19 21:38:49 -04:00

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