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
linkGroupIdwhenpropagateToLinkedis 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 IDscomputeRippleShifts(clips:removedIds:): Determines downstream shifts for deletions on the same trackcomputeRippleShiftsForRanges(clips:removedRanges:): Handles arbitrary frame range deletions usingmergeRanges(_:)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): ContainsremovedFramesCount,clearedTracks,shiftedClipCount, andresultingFragmentsfor 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
withTimelineSwapto 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
RippleEnginecomputes precise shift vectors via pure functions likecomputeRipplePushandcomputeRippleShifts- Operations return
RippleRangesOutcometo 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →