Implementing Overwrite Editing in Video Editing Software: A Deep Dive into Palmier Pro's Functional Approach
Overwrite editing in Palmier Pro is implemented as a stateless, functional operation using the OverwriteEngine to compute pure action objects that describe how to clear a timeline region without mutating the original data.
Implementing overwrite editing in video editing software requires precise manipulation of timeline clips to make room for new media without leaving gaps. In the Swift-based Palmier Pro video editor, this feature is built on a pure functional architecture defined in Sources/PalmierPro/Editor/OverwriteEngine.swift that separates algorithmic decision-making from UI state management.
Understanding the Core Data Model
All timeline objects in Palmier Pro are represented by the Clip struct defined in Sources/PalmierPro/Models/Timeline.swift. This immutable data structure stores:
id: Unique identifier used by action referencesstartFrameanddurationFrames: Placement and length on the timelinetrimStartFrameandtrimEndFrame: Offsets into the source mediaspeed: Playback speed multiplier affecting how trim offsets map to source frames
Because the overwrite engine operates exclusively on these immutable fields, the algorithm can be expressed as a stateless function that never mutates the original clip array, making it deterministic and thread-safe.
The OverwriteEngine Algorithm
The heart of the implementation resides in Sources/PalmierPro/Editor/OverwriteEngine.swift. The engine analyzes existing clips against a target region and emits a minimal set of actions to clear that region.
Defining Overwrite Actions
The OverwriteEngine defines a pure data representation of mutations through the Action enum:
enum OverwriteEngine {
enum Action {
case remove(clipId: String)
case trimEnd(clipId: String, newDuration: Int)
case trimStart(clipId: String,
newStartFrame: Int,
newTrimStart: Int,
newDuration: Int)
case split(clipId: String,
leftDuration: Int,
rightId: String,
rightStartFrame: Int,
rightTrimStart: Int,
rightDuration: Int)
}
static func computeOverwrite(clips: [Clip],
regionStart: Int,
regionEnd: Int) -> [Action] { … }
}
The computeOverwrite function accepts an array of clips and a half-open region [regionStart, regionEnd), returning an array of Action values that guarantee the region becomes empty when applied.
The Five Overlap Cases
The algorithm evaluates each clip's relationship to the target region and produces specific actions:
- Completely inside (
cs ≥ regionStart && ce ≤ regionEnd): emits.removeto delete the clip entirely - Envelopes the region (
cs < regionStart && ce > regionEnd): emits.splitto create two separate clips with the middle section removed - Overlaps left edge (
cs < regionStart && ce > regionStart): emits.trimEndto shorten the clip so it ends exactly atregionStart - Overlaps right edge (
cs < regionEnd && ce > regionEnd): emits.trimStartto move the clip rightward and adjust its trim offsets - No overlap: produces no action, leaving the clip untouched
Handling Speed and Trim Offsets
When splitting clips that envelope the target region, the engine must calculate correct trimStartFrame values for the right-hand segment to account for playback speed. The calculation appears in Sources/PalmierPro/Editor/OverwriteEngine.swift:
let rightTrimStart = clip.trimStartFrame
+ Int((Double(regionEnd - cs) * clip.speed).rounded())
This ensures that when the right segment begins playback at regionEnd, it starts at the correct frame in the underlying source media.
Executing Overwrite Actions in the Editor
While OverwriteEngine produces pure data, the actions are executed by the view model layer (typically EditorViewModel). The separation maintains clean architecture:
.remove: Drops the clip from the track array.trimEnd: SetsdurationFramesto the new calculated value.trimStart: UpdatesstartFrame,trimStartFrame, anddurationFramessimultaneously.split: Shrinks the original clip toleftDuration, then creates a new clip with the suppliedrightIdand calculated parameters, inserting it atrightStartFrame
This pattern keeps the engine testable and enables undo/redo functionality by storing the action list and applying inverse operations.
Integrating with Ripple Editing
When clips are removed from a track, sync-locked tracks must automatically close the gap. This is handled by RippleEngine in Sources/PalmierPro/Editor/RippleEngine.swift.
The workflow proceeds as follows:
- Extract removed clip IDs from the overwrite actions
- Pass these to
RippleEngine.computeRippleShifts(clips:removedIds:) - Receive
ClipShiftvalues describing frame adjustments for each affected clip - Apply the shifts to sync-locked tracks while maintaining relative timing
This ensures that when you overwrite a section of your timeline, parallel tracks remain synchronized without manual adjustment.
Complete Implementation Example
Here is the complete workflow for implementing overwrite editing in a Palmier Pro environment:
import PalmierPro
// 1. Define the timeline state
let trackClips = [
Fixtures.clip(id: "c1", start: 0, duration: 200), // envelops region
Fixtures.clip(id: "c2", start: 250, duration: 50) // unrelated
]
let regionStart = 50
let regionEnd = 150
// 2. Compute the required actions
let actions = OverwriteEngine.computeOverwrite(
clips: trackClips,
regionStart: regionStart,
regionEnd: regionEnd
)
// 3. Apply actions to mutate the timeline
func apply(_ actions: [OverwriteEngine.Action], to clips: inout [Clip]) {
for action in actions {
switch action {
case .remove(let id):
clips.removeAll { $0.id == id }
case .trimEnd(let id, let newDur):
if let i = clips.firstIndex(where: { $0.id == id }) {
clips[i].durationFrames = newDur
}
case .trimStart(let id, let newStart, let newTrim, let newDur):
if let i = clips.firstIndex(where: { $0.id == id }) {
clips[i].startFrame = newStart
clips[i].trimStartFrame = newTrim
clips[i].durationFrames = newDur
}
case .split(let id, let leftDur, let rightId,
let rightStart, let rightTrim, let rightDur):
if let i = clips.firstIndex(where: { $0.id == id }) {
clips[i].durationFrames = leftDur
var right = clips[i]
right.id = rightId
right.startFrame = rightStart
right.trimStartFrame = rightTrim
right.durationFrames = rightDur
clips.append(right)
}
}
}
clips.sort { $0.startFrame < $1.startFrame }
}
// 4. Handle ripple effects on sync-locked tracks
let removedIds = Set(actions.compactMap {
if case .remove(let id) = $0 { return id } else { return nil }
})
let otherTrackClips: [Clip] = [] // fetch sync-locked track clips
let shifts = RippleEngine.computeRippleShifts(
clips: otherTrackClips,
removedIds: removedIds
)
for shift in shifts {
if let i = otherTrackClips.firstIndex(where: { $0.id == shift.clipId }) {
otherTrackClips[i].startFrame = shift.newStartFrame
}
}
The unit tests in Tests/PalmierProTests/Timeline/OverwriteEngineTests.swift provide exhaustive coverage of every branch, including speed-aware splits and edge-case adjacency scenarios.
Summary
- Pure functional design makes
OverwriteEnginedeterministic and fully testable without mock UI states - Five distinct overlap cases handle every possible clip-to-region relationship, from complete containment to partial edges
- Speed-aware calculations preserve correct source media timing when splitting clips with non-standard playback rates
- Action-based mutation separates algorithmic logic from view model concerns, enabling robust undo/redo systems
- RippleEngine integration automatically maintains synchronization across tracks when overwrite editing removes content
Frequently Asked Questions
What is overwrite editing in video software?
Overwrite editing is a timeline operation where new media replaces existing content in a specific time range rather than pushing it forward. When you drop a clip onto an occupied region, the existing clips are either removed, trimmed, or split to make room, maintaining the overall timeline duration unlike insert editing which lengthens the sequence.
How does Palmier Pro handle clip splitting during overwrite?
When a clip completely envelops the target region, OverwriteEngine.computeOverwrite generates a .split action. This action creates two distinct clips: the left portion retains the original ID and ends at regionStart, while the right portion receives a new UUID and calculated trimStartFrame based on playback speed. The system preserves the underlying media references while adjusting the timeline presentation.
Why use pure actions instead of direct mutation?
The action-based approach treats overwrite editing as a stateless transformation that returns data describing what should happen rather than performing the mutation directly. This architecture enables comprehensive unit testing without UI dependencies, makes the algorithm thread-safe, and simplifies undo/redo implementation by storing action history and applying inverse operations.
How does overwrite editing interact with ripple editing?
When OverwriteEngine produces .remove actions, the affected clip IDs are collected and passed to RippleEngine.computeRippleShifts in Sources/PalmierPro/Editor/RippleEngine.swift. The ripple engine calculates ClipShift values for sync-locked tracks, determining how many frames each clip must move left to close the gap created by the removal. The view model applies these shifts after committing the overwrite actions, ensuring synchronized tracks remain aligned.
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 →