Source code for camtasia.operations.layout
from __future__ import annotations
from camtasia.timing import seconds_to_ticks, ticks_to_seconds
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from camtasia.timeline.track import Track
[docs]
def pack_track(track: Track, gap_seconds: float = 0.0) -> None:
"""Remove gaps between clips, packing them end-to-end.
Sorts clips by start time, then repositions each to start
immediately after the previous clip (plus optional gap).
"""
medias = track._data.get('medias', [])
if not medias:
return
track._data['transitions'] = []
medias.sort(key=lambda m: m.get('start', 0))
gap_ticks = seconds_to_ticks(gap_seconds)
cursor = 0
for m in medias:
m['start'] = cursor
cursor += m.get('duration', 0) + gap_ticks
[docs]
def ripple_insert(track: Track, position_seconds: float, duration_seconds: float) -> None:
"""Shift all clips at or after position forward by duration.
Creates a gap at the insertion point.
"""
pos_ticks = seconds_to_ticks(position_seconds)
shift_ticks = seconds_to_ticks(duration_seconds)
for m in track._data.get('medias', []):
if m.get('start', 0) >= pos_ticks:
m['start'] += shift_ticks
[docs]
def ripple_delete(track: Track, clip_id: int) -> None:
"""Remove a clip and shift subsequent clips backward to close the gap."""
medias = track._data.get('medias', [])
target = None
target_idx = None
for i, m in enumerate(medias):
if m.get('id') == clip_id:
target = m
target_idx = i
break
if target is None:
available = [m.get('id') for m in medias]
raise KeyError(
f"No clip with id={clip_id} on track index={track.index}. "
f"Available clip IDs: {available}"
)
gap = target.get('duration', 0)
target_start = target.get('start', 0)
medias.pop(target_idx)
transitions = track._data.get('transitions', [])
track._data['transitions'] = [
t for t in transitions
if t.get('leftMedia') != clip_id and t.get('rightMedia') != clip_id
]
for m in medias:
if m.get('start', 0) > target_start:
m['start'] -= gap
[docs]
def snap_to_grid(track: Track, grid_seconds: float = 1.0) -> None:
"""Snap all clip start times to the nearest grid point.
.. warning::
Snapping can move two or more clips to the same grid point,
creating overlapping clips on the track. Callers should check
``track.overlaps()`` afterward and resolve any collisions
(e.g. by calling :func:`pack_track`).
"""
grid_ticks = seconds_to_ticks(grid_seconds)
if grid_ticks <= 0:
raise ValueError(f'Grid must be positive, got {grid_seconds}')
for m in track._data.get('medias', []):
start = m.get('start', 0)
m['start'] = max(0, int(round(start / grid_ticks) * grid_ticks))