resolves overlapping

This commit is contained in:
André Michelle
2026-01-28 12:20:38 +01:00
parent 135fd73cb6
commit 557082d167
3 changed files with 215 additions and 5 deletions
@@ -86,7 +86,7 @@ export const RegionsArea = ({lifecycle, service, manager, scrollModel, scrollCon
const regionLocator = createRegionLocator(manager, regionSelection)
const dragAndDrop = new RegionDragAndDrop(service, capturing, timeline.snapping)
const shortcuts = ShortcutManager.get().createContext(() => element.contains(document.activeElement), "Regions")
const {engine, boxGraph} = project
const {engine, boxGraph, overlapResolver} = project
const clipboardHandler = RegionsClipboard.createHandler({
getEnabled: () => !engine.isPlaying.getValue(),
getPosition: () => engine.position.getValue(),
@@ -96,7 +96,8 @@ export const RegionsArea = ({lifecycle, service, manager, scrollModel, scrollCon
boxGraph,
boxAdapters,
getTracks: () => manager.tracks().map(track => track.trackBoxAdapter),
getSelectedTrack: () => Option.wrap(regionSelection.selected()[0]).flatMap(region => region.trackBoxAdapter)
getSelectedTrack: () => Option.wrap(regionSelection.selected()[0]).flatMap(region => region.trackBoxAdapter),
overlapResolver
})
lifecycle.ownAll(
regionSelection.catchupAndSubscribe({
@@ -1,4 +1,4 @@
import {ByteArrayInput, ByteArrayOutput, Option, Procedure, Provider, Selection, UUID} from "@opendaw/lib-std"
import {ByteArrayInput, ByteArrayOutput, Exec, Option, Procedure, Provider, Selection, UUID} from "@opendaw/lib-std"
import {Box, BoxEditing, BoxGraph} from "@opendaw/lib-box"
import {ppqn} from "@opendaw/lib-dsp"
import {Pointers} from "@opendaw/studio-enums"
@@ -12,6 +12,7 @@ import {
} from "@opendaw/studio-adapters"
import {ClipboardEntry, ClipboardHandler} from "../ClipboardManager"
import {ClipboardUtils} from "../ClipboardUtils"
import {RegionOverlapResolver} from "../../timeline/RegionOverlapResolver"
type ClipboardRegions = ClipboardEntry<"regions">
@@ -38,6 +39,7 @@ export namespace RegionsClipboard {
readonly boxAdapters: BoxAdapters
readonly getTracks: Provider<ReadonlyArray<TrackBoxAdapter>>
readonly getSelectedTrack: Provider<Option<TrackBoxAdapter>>
readonly overlapResolver: RegionOverlapResolver
}
const encodeMetadata = (metadata: RegionsMetadata): ArrayBufferLike => {
@@ -78,7 +80,8 @@ export namespace RegionsClipboard {
boxGraph,
boxAdapters,
getTracks,
getSelectedTrack
getSelectedTrack,
overlapResolver
}: Context): ClipboardHandler<ClipboardRegions> => {
const copyRegions = (): Option<ClipboardRegions> => {
const selected = selection.selected()
@@ -147,6 +150,14 @@ export namespace RegionsClipboard {
}
editing.modify(() => {
selection.deselectAll()
const pastePosition = Math.max(0, position)
const pasteComplete = pastePosition + (metadata.maxPosition - metadata.minPosition)
const overlapSolvers: Exec[] = []
for (const {target} of sourceTrackToTarget.values()) {
if (target !== null) {
overlapSolvers.push(overlapResolver.fromRange(target, pastePosition, pasteComplete))
}
}
const boxes = ClipboardUtils.deserializeBoxes(
entry.data,
boxGraph,
@@ -184,10 +195,11 @@ export namespace RegionsClipboard {
}
}
)
overlapSolvers.forEach(solver => solver())
const regionBoxes = boxes.filter(UnionBoxTypes.isRegionBox)
const adapters = regionBoxes.map(box => RegionAdapters.for(boxAdapters, box))
selection.select(...adapters)
setPosition(Math.max(0, position) + (metadata.maxPosition - metadata.minPosition))
setPosition(pasteComplete)
})
}
}
+197
View File
@@ -0,0 +1,197 @@
# Region and Track Focus System
## Problem
The current copy/paste implementation has two issues:
1. **Selection order is undefined**: `regionSelection.selected()[0]` returns an arbitrary region, not the one the user interacted with
2. **Cut breaks paste**: After cut deletes regions, there's no way to determine the target track for paste
## Concept: Focus vs Selection
| Aspect | Selection | Focus |
|--------|-----------|-------|
| Cardinality | Multiple items | Single item |
| Purpose | Batch operations (delete, move, copy) | Anchor point for operations |
| Persistence | Cleared on click elsewhere | Persists until explicitly changed |
| Visual | Highlight/border | Different indicator (e.g., thicker border, glow) |
## Design
### Focus Hierarchy
```
TrackFocus (always set when in regions area)
└── RegionFocus (optional, set when a region is clicked)
```
- **TrackFocus**: The track that will receive pasted regions (offset 0 anchor)
- **RegionFocus**: The specific region that was last interacted with
### Focus Behavior
| Action | Track Focus | Region Focus |
|--------|-------------|--------------|
| Click on region | Set to region's track | Set to region |
| Click on track (empty area) | Set to track | Clear |
| Click on track header | Set to track | Clear |
| Delete focused region | Keep track focus | Clear |
| Cut regions | Keep track focus | Clear |
| Paste regions | Unchanged | Set to first pasted region |
| Click outside regions area | Clear | Clear |
### Copy/Paste with Focus
**Copy/Cut:**
- Uses `TrackFocus` as the anchor track (offset 0)
- All other tracks get offsets relative to the focused track
- After cut: `TrackFocus` remains, `RegionFocus` is cleared
**Paste:**
- Uses `TrackFocus` to determine target track index
- Regions paste at `focusedTrackIndex + metadata.offset`
- After paste: First pasted region gets focus
---
## Implementation
### New Types
```typescript
// In a new file: packages/studio/core/src/ui/timeline/TimelineFocus.ts
import {DefaultObservableValue, Option, Terminable} from "@opendaw/lib-std"
import {AnyRegionBoxAdapter, TrackBoxAdapter} from "@opendaw/studio-adapters"
export class TimelineFocus implements Terminable {
readonly #track = new DefaultObservableValue<Option<TrackBoxAdapter>>(Option.None)
readonly #region = new DefaultObservableValue<Option<AnyRegionBoxAdapter>>(Option.None)
get track(): Option<TrackBoxAdapter> {return this.#track.getValue()}
get region(): Option<AnyRegionBoxAdapter> {return this.#region.getValue()}
get trackObservable() {return this.#track}
get regionObservable() {return this.#region}
focusTrack(track: TrackBoxAdapter): void {
this.#track.setValue(Option.wrap(track))
this.#region.setValue(Option.None)
}
focusRegion(region: AnyRegionBoxAdapter): void {
region.trackBoxAdapter.ifSome(track => this.#track.setValue(Option.wrap(track)))
this.#region.setValue(Option.wrap(region))
}
clearRegionFocus(): void {
this.#region.setValue(Option.None)
}
clear(): void {
this.#track.setValue(Option.None)
this.#region.setValue(Option.None)
}
terminate(): void {
this.#track.terminate()
this.#region.terminate()
}
}
```
### Integration Points
#### 1. Timeline/Project Level
```typescript
// In Project or Timeline
readonly timelineFocus = new TimelineFocus()
```
#### 2. RegionsArea.tsx - Focus on Click
```typescript
// When clicking on a region (in Dragging.attach or similar)
timelineFocus.focusRegion(target.region)
// When clicking on track empty area
timelineFocus.focusTrack(target.track.trackBoxAdapter)
```
#### 3. Track Header - Focus on Click
```typescript
// In TrackHeader.tsx
onClick={() => timelineFocus.focusTrack(trackBoxAdapter)
```
#### 4. RegionsClipboardHandler - Use Focus
```typescript
// Change context
readonly getFocusedTrack: Provider<Option<TrackBoxAdapter>>
// In copy:
const anchorTrack = getFocusedTrack()
if (anchorTrack.isEmpty()) {return Option.None}
// Calculate offsets relative to anchorTrack
// In paste:
const targetTrack = getFocusedTrack()
if (targetTrack.isEmpty()) {return}
```
#### 5. Region Deletion - Clear Region Focus
```typescript
// When deleting regions
if (timelineFocus.region.contains(deletedRegion)) {
timelineFocus.clearRegionFocus()
}
```
### Visual Feedback
#### Track Focus Indicator
```css
.track[data-focused="true"] {
/* Subtle left border or background tint */
border-left: 2px solid var(--accent-color);
}
```
#### Region Focus Indicator
```css
.region[data-focused="true"] {
/* Distinct from selection - e.g., thicker border or glow */
box-shadow: 0 0 0 2px var(--focus-color);
}
```
---
## Implementation Checklist
- [ ] Create `TimelineFocus` class
- [ ] Add `timelineFocus` to Project or appropriate scope
- [ ] Update `RegionsArea.tsx` to set focus on region click
- [ ] Update `RegionsArea.tsx` to set focus on track click (empty area)
- [ ] Update track headers to set focus on click
- [ ] Update `RegionsClipboardHandler` to use `getFocusedTrack` instead of `getSelectedTrack`
- [ ] Clear region focus when focused region is deleted
- [ ] Add visual indicators for focused track and region
- [ ] Ensure focus persists after cut operation
---
## Files to Create/Modify
### New Files
```
packages/studio/core/src/ui/timeline/TimelineFocus.ts
```
### Modify
```
packages/studio/core/src/ui/index.ts - export TimelineFocus
packages/studio/core/src/project/Project.ts - add timelineFocus
packages/app/studio/src/ui/timeline/tracks/audio-unit/regions/RegionsArea.tsx - set focus on clicks
packages/app/studio/src/ui/timeline/tracks/audio-unit/headers/TrackHeader.tsx - set focus on click
packages/studio/core/src/ui/clipboard/types/RegionsClipboardHandler.ts - use getFocusedTrack
```