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