How Palmier Pro's Timeline Ripple Edit Engine Handles Concurrent Clip Mutations

Palmier Pro's timeline ripple edit engine manages concurrent clip mutations through atomic transactions that validate multicam groups, sync-locked tracks, and linked A/V pairs before computing precise shift vectors via the pure-functional RippleEngine utility.

The palmier-pro video editor implements non-destructive ripple editing as a coordinated system of clip mutations that preserves timeline integrity across complex media relationships. In Sources/PalmierPro/Editor/ViewModel/EditorViewModel+Ripple.swift, the engine orchestrates multi-clip operations by discovering dependencies, enforcing constraints, and applying mathematical shift calculations. This architecture ensures that concurrent mutations across linked media, multicam angles, and sync-locked tracks remain consistent, conflict-free, and fully undoable.

Atomic Mutation via withTimelineSwap

All public ripple operations—including rippleTrimClip, rippleDeleteSelectedClips, rippleInsertClips, and range-based deletions—execute inside withTimelineSwap(actionName:refreshVisuals:). This wrapper guarantees atomicity by grouping the entire mutation into a single undo transaction and preventing intermediate state exposure.

withTimelineSwap(actionName: "Ripple Trim", refreshVisuals: true) {
    // Apply resizes, shifts, and track sorting as one unit
    applyShifts(computedShifts)
    updateClipMetadata(clipId, trimStart: newStart, duration: newDuration)
    sortClipsOnTrack(trackIndex)
}

The method registers a timeline undo step via registerTimelineUndo, ensuring users can revert the entire ripple operation with a single undo command.

Target Discovery: Building the Ripple Dependency Graph

Before applying any transformation, the engine discovers every clip that must participate in the ripple to maintain synchronization.

Linked Partners and Multicam Cohorts

The rippleTrimTargets method iteratively expands the target set to include:

  • Linked partners: Audio/video clips sharing a linkGroupId when propagateToLinked is true
  • Multicam cohorts: Clips belonging to the same multicam group gathered via multicamRippleCohort

The discovery routine runs until closure is reached, ensuring no related clip is left behind during the mutation.

Sync-Locked Track Handling

Tracks flagged as syncLocked automatically shift as a block with the lead track. The engine identifies these tracks through the timeline model and computes collective room checks to prevent downstream collisions.

Safety Validation Before Mutation

The engine validates three critical constraints to prevent timeline corruption.

Multicam Atomicity Enforcement

The multicamAtomicityViolation check ensures ripple operations do not split a multicam group across moving and stationary tracks. If a violation is detected, the operation halts immediately via refuseRipple, which emits a UI toast and logs the conflict.

if multicamAtomicityViolation(cohorts: affectedCohorts) {
    refuseRipple(reason: "Multicam group must remain on single track")
    return .refused("Multicam atomicity violated")
}

Sync-Locked Room Checks

When shrinking clips via rippleTrimClip, the engine calculates the minimum "shrink room" across all targets and examines syncLockedLeftRoom to verify that sync-locked tracks possess adequate space to accommodate the shift without crossing frame zero.

Shift Validation Dry-Runs

Before committing changes, validateShifts performs a dry-run on the computed ClipShift array. Any shift that would push a clip before the timeline start or cause overlaps on sync-locked tracks results in immediate rejection.

Computing the Ripple with RippleEngine

The heavy lifting is delegated to RippleEngine (referenced from Sources/PalmierPro/Utilities/RippleEngine.swift), which provides pure functions for shift calculation:

  • computeRipplePush(clips:insertFrame:pushAmount:excludeIds:): Calculates forward pushes for right-edge edits (inserts or right-side trims) while optionally excluding specific clip IDs
  • computeRippleShifts(clips:removedIds:): Determines downstream shifts for deletions on the same track
  • computeRippleShiftsForRanges(clips:removedRanges:): Handles arbitrary frame range deletions using mergeRanges(_:) to consolidate overlapping regions before calculation

These functions return an array of ClipShift structures (clipId paired with newStartFrame), which the main ripple code applies via applyShifts(_:).

// Compute how much to push clips when inserting at frame 120
let shifts = RippleEngine.computeRipplePush(
    clips: trackClips,
    insertFrame: 120,
    pushAmount: newClipDuration,
    excludeIds: insertedClipIds
)

// Apply the computed shifts atomically
applyShifts(shifts)

Result Reporting and Outcome Types

Deletion and insertion operations return a RippleRangesOutcome enumeration that communicates success or failure:

  • .ok(RippleRangesReport): Contains removedFramesCount, clearedTracks, shiftedClipCount, and resultingFragments for UI synchronization
  • .refused(String): Provides a human-readable reason (multicamAtomicityViolation, insufficientSyncLockedRoom, etc.)

This strongly-typed result ensures the UI layer can display precise feedback when constraints prevent a ripple edit.

Concurrency Model and Thread Safety

All ripple edits execute as single-threaded atomic units from the UI perspective. Because withTimelineSwap captures the entire state transformation, the UI thread only observes the final consistent state. The RippleEngine computation functions are pure and side-effect-free, making them safe for background execution if future implementations move calculation off the main actor.

Key Invariants Preserved

The engine maintains four critical timeline invariants during every ripple mutation:

  • Linked A/V synchronization: Linked partners mutate together with pixel-perfect alignment
  • Multicam atomicity: Entire multicam groups shift as monolithic units across tracks
  • Sync-locked integrity: Sync-locked tracks preserve relative offsets and never overlap
  • Temporal validity: No clip may start before frame zero or violate track-specific constraints

Summary

  • Palmier Pro's ripple edit engine uses withTimelineSwap to wrap mutations in atomic, undoable transactions
  • Target discovery expands selections to include linked clips, multicam cohorts, and sync-locked tracks
  • Validation layers enforce multicam atomicity, sync-locked room availability, and shift validity before mutation
  • RippleEngine computes precise shift vectors via pure functions like computeRipplePush and computeRippleShifts
  • Operations return RippleRangesOutcome to communicate detailed results or refusal reasons
  • All mutations preserve four key invariants: linked sync, multicam unity, sync-lock integrity, and temporal bounds

Frequently Asked Questions

What prevents a ripple edit from breaking multicam synchronization?

The engine calls multicamAtomicityViolation before executing any mutation. This function checks whether the proposed shift would distribute a multicam group's clips across both moving and stationary tracks. If the group would split, refuseRipple aborts the operation and alerts the user, ensuring multicam clips remain temporally aligned.

How does the engine handle clips linked across audio and video tracks?

When propagateToLinked is true, the rippleTrimTargets method discovers all clips sharing a linkGroupId with the primary target. These linked partners are included in the mutation set, and the engine validates that sufficient room exists on all linked tracks before applying shifts, preserving exact A/V sync.

Can ripple edits run on background threads?

While current call sites execute on the main actor, the architecture supports background computation. The RippleEngine functions are pure and stateless, receiving immutable clip arrays and returning ClipShift calculations. The actual mutation occurs inside withTimelineSwap, which serializes access to the timeline model, making the system thread-safe for concurrent editing scenarios.

What happens if a ripple edit would push a clip before the timeline start?

The validateShifts function performs a dry-run calculation on the proposed ClipShift array. Any shift producing a newStartFrame less than zero triggers immediate rejection. The operation returns .refused("Shift would violate timeline bounds"), preventing invalid states from reaching the timeline model.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →