12 KiB
Overlapping Regions Behaviour - Implementation Notes
Overview
Add a preference overlapping-regions-behaviour that controls how region overlaps are handled:
- "clip" (default): Truncate/clip existing regions
- "push-existing": Push existing overlapped regions to track below (incoming wins)
- "keep-existing": Push incoming region to track below (existing wins)
Current State
What's Already Implemented
- Preference in StudioSettings.ts -
editing.overlapping-regions-behaviourwith options["clip", "push-to-new-track"] - Labels in PreferencesPageLabels.ts - UI labels for the preference
- RegionPushResolver.ts - Core logic class (needs revision)
- Export in ui/index.ts - RegionPushResolver is exported
What's NOT Working
The integration into the 6 usage locations is broken. Attempts to move regions using box.regions.refer() after creation don't work properly.
Key Learnings
1. Region Creation and Track Attachment
Regions are attached to tracks during creation via box.regions.refer(targetTrack.regions) inside AudioContentFactory. This happens atomically during the BoxGraph transaction.
2. Moving Regions Between Tracks
The pattern adapter.box.regions.refer(newTrack.box.regions) is used throughout the codebase to move regions. This DOES work when:
- Called inside an
editing.modify()transaction - The region already exists and is fully initialized
3. The Transaction Requirement
All box graph modifications MUST happen inside editing.modify(() => { ... }). Track creation via ProjectApi.createAudioTrack() modifies the graph, so it must be inside the transaction.
4. The Core Problem with Our Approach
When checking for overlaps, we were sometimes:
- Detecting the region being moved as "overlapping with itself"
- Not properly excluding the moving region from sibling track checks
- Creating tracks outside transactions
The 6 Usage Locations
| Location | Operation | Current Behavior |
|---|---|---|
RegionDragAndDrop.ts |
Drop sample onto timeline | Creates region, then clips overlaps |
ProjectApi.ts:duplicateRegion |
Duplicate region (Cmd+D) | Copies region, then clips overlaps |
RegionMoveModifier.ts |
Drag region to move | Moves region, then clips overlaps |
RegionDurationModifier.ts |
Drag region end to resize | Resizes region, then clips overlaps |
RegionStartModifier.ts |
Drag region start to resize | Resizes region, then clips overlaps |
RegionLoopDurationModifier.ts |
Drag loop end | Resizes loop, then clips overlaps |
How RegionClipResolver Works (Current "clip" Behavior)
// Called BEFORE the transaction with read-only state
const solver = RegionClipResolver.fromSelection(modifiedTracks, adapters, strategy, deltaIndex)
// Inside transaction: apply changes, then call solver
editing.modify(() => {
// Apply position/track changes to regions
adapters.forEach(adapter => { ... })
// Solver clips any overlapping regions
solver()
})
Key insight: RegionClipResolver.fromSelection() captures the BEFORE state, computes what needs to be clipped, and returns a function that applies the clips inside the transaction.
Implementation Architecture
Unified Class: RegionOverlapResolver
Single class on Project that handles both "clip" and "push-to-new-track" based on preference.
// In Project:
readonly overlapResolver: RegionOverlapResolver
// Usage in modifiers (simple!):
project.overlapResolver.apply(adapters, strategy, deltaIndex, () => {
// Apply position/track changes
})
// For range-based operations (drop, duplicate):
project.overlapResolver.applyRange(track, from, to, excludeRegion, () => {
// Create region
})
RegionOverlapResolver Class
class RegionOverlapResolver {
readonly #project: Project
constructor(project: Project) {
this.#project = project
}
// For selection-based operations (move, resize)
apply(
adapters: ReadonlyArray<AnyRegionBoxAdapter>,
strategy: RegionModifyStrategies,
deltaIndex: int,
changes: () => void
): void {
const behaviour = StudioPreferences.settings.editing["overlapping-regions-behaviour"]
// Derive tracks from adapters
// Capture overlapped regions before changes
this.#project.editing.modify(() => {
changes()
if (behaviour === "push-to-new-track") {
// Move overlapped regions to tracks below
} else {
// Clip overlapped regions
}
})
}
// For range-based operations (drop, duplicate)
applyRange(
track: TrackBoxAdapter,
from: ppqn,
to: ppqn,
excludeRegion: Optional<AnyRegionBoxAdapter>,
changes: () => void
): void
}
Internal Logic
- Derive tracks from
adapters- no need to pass explicitly - Capture phase (before
editing.modify):- Find regions that will overlap with new bounds
- Exclude the regions being moved/resized
- Execute phase (inside
editing.modify):- Call
changes()lambda - Based on preference:
- "clip": truncate/split overlapped existing regions
- "push-existing": move overlapped existing regions to track below
- "keep-existing": move incoming region(s) to track below
- Call
Critical Details
Track Types
TrackType.Audio- Audio regionsTrackType.Notes- MIDI/Note regionsTrackType.Value- Automation (should NOT use push behavior)
Finding Sibling Tracks
const siblingTracks = audioUnit.tracks.pointerHub.incoming()
.map(vertex => vertex.box as TrackBox)
.filter(trackBox => trackBox.type.getValue() === trackType)
.sort((a, b) => a.index.getValue() - b.index.getValue())
Creating New Tracks
// Must be inside transaction!
projectApi.createAudioTrack(audioUnit, insertIndex) // Returns TrackBox
projectApi.createNoteTrack(audioUnit, insertIndex) // Returns TrackBox
Track Index vs List Index
track.indexField.getValue()- The track's index within its audio unittrack.listIndex- The track's visual index in the timeline (used by TracksManager)
Test Scenarios
- Drop on empty track - No overlap, region stays on target track
- Drop on occupied position - Overlap detected, region goes to new/existing track below
- Move to empty position - No overlap, region moves normally
- Move to occupied position - Overlap detected, region goes to new/existing track
- Move back to original position - Region should return to original track if space available
- Resize into occupied space - Region should be pushed to new track
- Duplicate into occupied space - Duplicate should go to new track
Code Style Guidelines
- Compact code - Minimize verbosity, keep it tight
- Single
editing.modify()call - Both behaviors should use one transaction - Use built-in copy methods - Leverage
region.copyTo()from adapters - Clear code blocks - Distinct if/else for "clip" vs "push-to-new-track"
- Extract if heavy - If logic is complex, extract to
RegionMoveModifierApprover.tsor similar
Target Structure (RegionMoveModifier.approve example)
approve(editing: BoxEditing): void {
// ... validation ...
const behaviour = StudioPreferences.settings.editing["overlapping-regions-behaviour"]
if (behaviour === "push-to-new-track") {
// Clear, self-contained block for push behavior
editing.modify(() => {
// All push logic here
})
} else {
// Clear, self-contained block for clip behavior
editing.modify(() => {
// All clip logic here
})
}
}
Resolved Specification
Three Modes
| Mode | Who moves? | Use case |
|---|---|---|
| clip | Nobody moves, existing gets truncated | Traditional DAW behavior |
| push-existing | Existing regions move down | "My action wins" - incoming stays where I put it |
| keep-existing | Incoming region moves down | "Don't touch my arrangement" - existing stays |
Rules (for push modes)
- Direction: Always push to tracks BELOW
- Track selection: Look at ALL existing tracks of same type below for available space, otherwise create new track directly below
- Entire regions: Push the complete region, no clipping/truncating
- Multiple overlaps: All overlapping regions go to the same target track
- No cascade: Pushed regions go to tracks with guaranteed space (existing with room, or new)
Behavior by Operation (push-existing)
| Operation | What happens |
|---|---|
| Drop sample | Sample lands at drop position, overlapped regions pushed down |
| Move region | Region goes to drag destination, overlapped regions pushed down |
| Resize region | Region extends/contracts, newly overlapped regions pushed down |
| Duplicate | Duplicate lands at target position, overlapped regions pushed down |
| Copy (Cmd+drag) | Copy lands at drag destination, overlapped regions pushed down |
Behavior by Operation (keep-existing)
| Operation | What happens |
|---|---|
| Drop sample | If overlap, sample goes to track below |
| Move region | If overlap at destination, moved region goes to track below |
| Resize region | If overlap after resize, resized region goes to track below |
| Duplicate | If overlap, duplicate goes to track below |
| Copy (Cmd+drag) | If overlap, copy goes to track below |
Manual Test Checklist
Setup
- Open Preferences → Editing section
- Find "Overlapping regions behaviour" option
- Switch between: "Clip existing regions" and "Push to new track"
Test Matrix
1. Drop Sample (RegionDragAndDrop)
- Drop sample onto empty area → Region lands there
- Drop sample on top of existing region in clip mode → Existing region gets truncated
- Drop sample on top of existing region in push mode → Existing region moves to track below
2. Move Region (RegionMoveModifier)
- Drag region to empty position → Moves normally
- Drag region over another region in clip mode → Overlapped region gets truncated
- Drag region over another region in push mode → Overlapped region pushed to track below
3. Resize Region End (RegionDurationModifier)
- Extend region into empty space → Resizes normally
- Extend region into another region in clip mode → Overlapped region gets truncated
- Extend region into another region in push mode → Overlapped region pushed down
4. Resize Region Start (RegionStartModifier)
- Extend region start into empty space → Resizes normally
- Extend region start into another region in clip mode → Overlapped region gets truncated
- Extend region start into another region in push mode → Overlapped region pushed down
5. Resize Loop Duration (RegionLoopDurationModifier)
- Extend loop into empty space → Extends normally
- Extend loop into another region in clip mode → Overlapped region gets truncated
- Extend loop into another region in push mode → Overlapped region pushed down
6. Duplicate Region (Cmd+D)
- Duplicate where there's no overlap → Duplicate lands normally
- Duplicate where it overlaps existing region in clip mode → Existing gets truncated
- Duplicate where it overlaps existing region in push mode → Existing pushed down
Track Reuse Verification (push mode only)
- Track of same type exists below with available space → Region goes there (no new track created)
- No space available on existing tracks → New track is created below
Edge Cases
- Multiple overlapping regions at once
- Cross-track moves (moving region to a different track)
- Undo/Redo after overlap resolution
- Note regions (not just audio regions)
Regression Tests
- Clip mode still works exactly as before
- No overlap scenario works the same in both modes