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 references
  • startFrame and durationFrames: Placement and length on the timeline
  • trimStartFrame and trimEndFrame: Offsets into the source media
  • speed: 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 .remove to delete the clip entirely
  • Envelopes the region (cs < regionStart && ce > regionEnd): emits .split to create two separate clips with the middle section removed
  • Overlaps left edge (cs < regionStart && ce > regionStart): emits .trimEnd to shorten the clip so it ends exactly at regionStart
  • Overlaps right edge (cs < regionEnd && ce > regionEnd): emits .trimStart to 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: Sets durationFrames to the new calculated value
  • .trimStart: Updates startFrame, trimStartFrame, and durationFrames simultaneously
  • .split: Shrinks the original clip to leftDuration, then creates a new clip with the supplied rightId and calculated parameters, inserting it at rightStartFrame

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:

  1. Extract removed clip IDs from the overwrite actions
  2. Pass these to RippleEngine.computeRippleShifts(clips:removedIds:)
  3. Receive ClipShift values describing frame adjustments for each affected clip
  4. 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 OverwriteEngine deterministic 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:

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 →