@opendaw/studio-core 0.0.100 → 0.0.102
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/AssetService.js +2 -2
- package/dist/EffectFactories.d.ts +2 -0
- package/dist/EffectFactories.d.ts.map +1 -1
- package/dist/EffectFactories.js +44 -24
- package/dist/Engine.d.ts +5 -0
- package/dist/Engine.d.ts.map +1 -1
- package/dist/EngineFacade.d.ts +5 -0
- package/dist/EngineFacade.d.ts.map +1 -1
- package/dist/EngineFacade.js +23 -2
- package/dist/EngineWorklet.d.ts +5 -0
- package/dist/EngineWorklet.d.ts.map +1 -1
- package/dist/EngineWorklet.js +107 -11
- package/dist/HRClockWorker.d.ts +7 -0
- package/dist/HRClockWorker.d.ts.map +1 -0
- package/dist/HRClockWorker.js +54 -0
- package/dist/OfflineEngineRenderer.d.ts.map +1 -1
- package/dist/OfflineEngineRenderer.js +6 -2
- package/dist/RecordingWorklet.d.ts +2 -1
- package/dist/RecordingWorklet.d.ts.map +1 -1
- package/dist/RecordingWorklet.js +9 -1
- package/dist/Storage.d.ts.map +1 -1
- package/dist/Storage.js +1 -0
- package/dist/StudioPreferences.d.ts +9 -0
- package/dist/StudioPreferences.d.ts.map +1 -1
- package/dist/StudioSettings.d.ts +14 -0
- package/dist/StudioSettings.d.ts.map +1 -1
- package/dist/StudioSettings.js +19 -2
- package/dist/capture/CaptureAudio.d.ts +3 -1
- package/dist/capture/CaptureAudio.d.ts.map +1 -1
- package/dist/capture/CaptureAudio.js +51 -29
- package/dist/capture/MonitoringMode.d.ts +2 -0
- package/dist/capture/MonitoringMode.d.ts.map +1 -0
- package/dist/capture/MonitoringMode.js +1 -0
- package/dist/capture/RecordAudio.d.ts.map +1 -1
- package/dist/capture/RecordAudio.js +1 -0
- package/dist/capture/index.d.ts +1 -0
- package/dist/capture/index.d.ts.map +1 -1
- package/dist/capture/index.js +1 -0
- package/dist/dawproject/DawProjectImporter.d.ts.map +1 -1
- package/dist/dawproject/DawProjectImporter.js +4 -0
- package/dist/processors.js +24 -24
- package/dist/processors.js.map +4 -4
- package/dist/project/Project.d.ts +7 -1
- package/dist/project/Project.d.ts.map +1 -1
- package/dist/project/Project.js +58 -8
- package/dist/project/ProjectApi.d.ts +1 -1
- package/dist/project/ProjectApi.d.ts.map +1 -1
- package/dist/project/ProjectApi.js +17 -3
- package/dist/project/ProjectMigration.d.ts.map +1 -1
- package/dist/project/ProjectMigration.js +3 -2
- package/dist/project/audio/AudioContentFactory.d.ts +5 -0
- package/dist/project/audio/AudioContentFactory.d.ts.map +1 -1
- package/dist/project/audio/AudioContentFactory.js +13 -0
- package/dist/project/migration/MigrateNeuralAmpDeviceBox.d.ts +4 -0
- package/dist/project/migration/MigrateNeuralAmpDeviceBox.d.ts.map +1 -0
- package/dist/project/migration/MigrateNeuralAmpDeviceBox.js +29 -0
- package/dist/project/migration/index.d.ts +1 -0
- package/dist/project/migration/index.d.ts.map +1 -1
- package/dist/project/migration/index.js +1 -0
- package/dist/ui/{generic → clipboard}/ClipboardManager.d.ts +3 -3
- package/dist/ui/clipboard/ClipboardManager.d.ts.map +1 -0
- package/dist/ui/clipboard/ClipboardManager.js +147 -0
- package/dist/ui/clipboard/ClipboardUtils.d.ts +12 -0
- package/dist/ui/clipboard/ClipboardUtils.d.ts.map +1 -0
- package/dist/ui/clipboard/ClipboardUtils.js +94 -0
- package/dist/ui/{generic → clipboard}/ContextMenu.d.ts +1 -1
- package/dist/ui/clipboard/ContextMenu.d.ts.map +1 -0
- package/dist/ui/{generic → clipboard}/ContextMenu.js +1 -1
- package/dist/ui/clipboard/types/AudioUnitsClipboardHandler.d.ts +18 -0
- package/dist/ui/clipboard/types/AudioUnitsClipboardHandler.d.ts.map +1 -0
- package/dist/ui/clipboard/types/AudioUnitsClipboardHandler.js +215 -0
- package/dist/ui/clipboard/types/DevicesClipboardHandler.d.ts +18 -0
- package/dist/ui/clipboard/types/DevicesClipboardHandler.d.ts.map +1 -0
- package/dist/ui/clipboard/types/DevicesClipboardHandler.js +188 -0
- package/dist/ui/clipboard/types/NotesClipboardHandler.d.ts +21 -0
- package/dist/ui/clipboard/types/NotesClipboardHandler.d.ts.map +1 -0
- package/dist/ui/clipboard/types/NotesClipboardHandler.js +72 -0
- package/dist/ui/clipboard/types/RegionsClipboardHandler.d.ts +24 -0
- package/dist/ui/clipboard/types/RegionsClipboardHandler.d.ts.map +1 -0
- package/dist/ui/clipboard/types/RegionsClipboardHandler.js +137 -0
- package/dist/ui/clipboard/types/ValuesClipboardHandler.d.ts +22 -0
- package/dist/ui/clipboard/types/ValuesClipboardHandler.d.ts.map +1 -0
- package/dist/ui/clipboard/types/ValuesClipboardHandler.js +135 -0
- package/dist/ui/index.d.ts +14 -3
- package/dist/ui/index.d.ts.map +1 -1
- package/dist/ui/index.js +14 -3
- package/dist/ui/menu/MenuItems.d.ts.map +1 -0
- package/dist/ui/timeline/RegionClipResolver.d.ts +5 -1
- package/dist/ui/timeline/RegionClipResolver.d.ts.map +1 -1
- package/dist/ui/timeline/RegionClipResolver.js +5 -1
- package/dist/ui/timeline/RegionKeepExistingResolver.d.ts +34 -0
- package/dist/ui/timeline/RegionKeepExistingResolver.d.ts.map +1 -0
- package/dist/ui/timeline/RegionKeepExistingResolver.js +171 -0
- package/dist/ui/timeline/RegionOverlapResolver.d.ts +33 -0
- package/dist/ui/timeline/RegionOverlapResolver.d.ts.map +1 -0
- package/dist/ui/timeline/RegionOverlapResolver.js +79 -0
- package/dist/ui/timeline/RegionPushExistingResolver.d.ts +31 -0
- package/dist/ui/timeline/RegionPushExistingResolver.d.ts.map +1 -0
- package/dist/ui/timeline/RegionPushExistingResolver.js +159 -0
- package/dist/ui/timeline/TimelineFocus.d.ts +14 -0
- package/dist/ui/timeline/TimelineFocus.d.ts.map +1 -0
- package/dist/ui/timeline/TimelineFocus.js +45 -0
- package/dist/ui/timeline/TrackResolver.d.ts +11 -0
- package/dist/ui/timeline/TrackResolver.d.ts.map +1 -0
- package/dist/ui/timeline/TrackResolver.js +5 -0
- package/dist/workers-main.js +1 -1
- package/dist/workers-main.js.map +3 -3
- package/package.json +62 -62
- package/dist/ui/generic/ClipboardManager.d.ts.map +0 -1
- package/dist/ui/generic/ClipboardManager.js +0 -77
- package/dist/ui/generic/ContextMenu.d.ts.map +0 -1
- package/dist/ui/generic/MenuItems.d.ts.map +0 -1
- /package/dist/ui/{generic → menu}/MenuItems.d.ts +0 -0
- /package/dist/ui/{generic → menu}/MenuItems.js +0 -0
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { TrackBoxAdapter, TrackType } from "@opendaw/studio-adapters";
|
|
2
|
+
/**
|
|
3
|
+
* Resolver for "keep-existing" overlap behavior.
|
|
4
|
+
* When overlap is detected, the INCOMING (selected/moved/copied) region moves to a track below.
|
|
5
|
+
* Existing regions stay in place.
|
|
6
|
+
*/
|
|
7
|
+
export class RegionKeepExistingResolver {
|
|
8
|
+
/**
|
|
9
|
+
* For selection-based operations (move, resize, copy).
|
|
10
|
+
* Returns:
|
|
11
|
+
* - postProcess: function to call AFTER changes are applied (handles moves)
|
|
12
|
+
* - trackResolver: function to get target track for copies (use BEFORE creating copy)
|
|
13
|
+
*
|
|
14
|
+
* NOTE: Track resolution is lazy - target tracks are computed when trackResolver is called
|
|
15
|
+
* or when postProcess runs. This is necessary because creating new tracks requires being
|
|
16
|
+
* inside a transaction.
|
|
17
|
+
*/
|
|
18
|
+
static fromSelection(tracks, adapters, strategy, deltaIndex, projectApi, boxAdapters) {
|
|
19
|
+
const isCopy = strategy.showOrigin();
|
|
20
|
+
// Collect overlap info - don't compute target tracks yet (need to be in transaction)
|
|
21
|
+
const copyOverlaps = [];
|
|
22
|
+
const moveOverlaps = [];
|
|
23
|
+
for (const track of tracks) {
|
|
24
|
+
const selectedStrategy = strategy.selectedModifyStrategy();
|
|
25
|
+
for (const adapter of adapters) {
|
|
26
|
+
const adapterTrackIndex = adapter.trackBoxAdapter.unwrap().listIndex + deltaIndex;
|
|
27
|
+
if (adapterTrackIndex !== track.listIndex) {
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
const position = selectedStrategy.readPosition(adapter);
|
|
31
|
+
const complete = selectedStrategy.readComplete(adapter);
|
|
32
|
+
let hasOverlap = false;
|
|
33
|
+
for (const region of track.regions.collection.iterateRange(0, complete)) {
|
|
34
|
+
// For moves: skip the adapter itself (it's being moved)
|
|
35
|
+
// For copies: DON'T skip - the copy might overlap with the original
|
|
36
|
+
if (!isCopy && region === adapter) {
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
// For moves: skip other selected regions (they move together)
|
|
40
|
+
if (!isCopy && region.isSelected) {
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
if (region.complete <= position) {
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
if (region.position >= complete) {
|
|
47
|
+
break;
|
|
48
|
+
}
|
|
49
|
+
hasOverlap = true;
|
|
50
|
+
break;
|
|
51
|
+
}
|
|
52
|
+
if (hasOverlap) {
|
|
53
|
+
const info = { track, position, complete, adapter };
|
|
54
|
+
if (isCopy) {
|
|
55
|
+
copyOverlaps.push(info);
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
moveOverlaps.push(info);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
// Cache for lazily computed target tracks
|
|
64
|
+
const targetTrackCache = new Map();
|
|
65
|
+
// Track which tracks were affected by copy redirects (for dispatchChange)
|
|
66
|
+
const copyAffectedTracks = [];
|
|
67
|
+
const getTargetTrack = (info) => {
|
|
68
|
+
let targetTrack = targetTrackCache.get(info.adapter);
|
|
69
|
+
if (targetTrack === undefined) {
|
|
70
|
+
targetTrack = RegionKeepExistingResolver.#findOrCreateTrackBelow(info.track, info.position, info.complete, projectApi, boxAdapters);
|
|
71
|
+
targetTrackCache.set(info.adapter, targetTrack);
|
|
72
|
+
}
|
|
73
|
+
return targetTrack;
|
|
74
|
+
};
|
|
75
|
+
// Lazy track resolver for copies - called inside the transaction
|
|
76
|
+
const trackResolver = (adapter, defaultTrack) => {
|
|
77
|
+
const info = copyOverlaps.find(overlap => overlap.adapter === adapter);
|
|
78
|
+
if (info !== undefined) {
|
|
79
|
+
const targetTrack = getTargetTrack(info);
|
|
80
|
+
// Track affected tracks for dispatchChange in postProcess
|
|
81
|
+
if (!copyAffectedTracks.includes(targetTrack)) {
|
|
82
|
+
copyAffectedTracks.push(targetTrack);
|
|
83
|
+
}
|
|
84
|
+
if (!copyAffectedTracks.includes(info.track)) {
|
|
85
|
+
copyAffectedTracks.push(info.track);
|
|
86
|
+
}
|
|
87
|
+
return targetTrack;
|
|
88
|
+
}
|
|
89
|
+
return defaultTrack;
|
|
90
|
+
};
|
|
91
|
+
// Post-process - handles moves and dispatches changes for copy-affected tracks
|
|
92
|
+
const postProcess = () => {
|
|
93
|
+
const affectedTracks = [...copyAffectedTracks];
|
|
94
|
+
for (const info of moveOverlaps) {
|
|
95
|
+
const targetTrack = getTargetTrack(info);
|
|
96
|
+
if (!affectedTracks.includes(targetTrack)) {
|
|
97
|
+
affectedTracks.push(targetTrack);
|
|
98
|
+
}
|
|
99
|
+
if (!affectedTracks.includes(info.track)) {
|
|
100
|
+
affectedTracks.push(info.track);
|
|
101
|
+
}
|
|
102
|
+
info.adapter.box.regions.refer(targetTrack.box.regions);
|
|
103
|
+
}
|
|
104
|
+
affectedTracks
|
|
105
|
+
.filter(track => track.listIndex >= 0)
|
|
106
|
+
.forEach(track => track.regions.dispatchChange());
|
|
107
|
+
};
|
|
108
|
+
return { postProcess, trackResolver };
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* For range-based operations (drop, duplicate).
|
|
112
|
+
* Returns the target track to use for creation (may be different from original if overlap exists).
|
|
113
|
+
*/
|
|
114
|
+
static resolveTargetTrack(track, position, complete, projectApi, boxAdapters) {
|
|
115
|
+
if (track.type === TrackType.Value) {
|
|
116
|
+
return track;
|
|
117
|
+
}
|
|
118
|
+
// Check for overlap
|
|
119
|
+
for (const region of track.regions.collection.iterateRange(0, complete)) {
|
|
120
|
+
if (region.complete <= position) {
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
if (region.position >= complete) {
|
|
124
|
+
break;
|
|
125
|
+
}
|
|
126
|
+
// Found overlap, find track below
|
|
127
|
+
return RegionKeepExistingResolver.#findOrCreateTrackBelow(track, position, complete, projectApi, boxAdapters);
|
|
128
|
+
}
|
|
129
|
+
return track;
|
|
130
|
+
}
|
|
131
|
+
static #findOrCreateTrackBelow(sourceTrack, position, complete, projectApi, boxAdapters) {
|
|
132
|
+
const audioUnit = sourceTrack.audioUnit;
|
|
133
|
+
const trackType = sourceTrack.type;
|
|
134
|
+
// Get all tracks of same type in this audio unit, sorted by index
|
|
135
|
+
const siblingTracks = audioUnit.tracks.pointerHub.incoming()
|
|
136
|
+
.map(vertex => vertex.box)
|
|
137
|
+
.filter(trackBox => trackBox.type.getValue() === trackType)
|
|
138
|
+
.sort((boxA, boxB) => boxA.index.getValue() - boxB.index.getValue());
|
|
139
|
+
const sourceIndex = sourceTrack.indexField.getValue();
|
|
140
|
+
// Look for existing track below with space
|
|
141
|
+
for (const trackBox of siblingTracks) {
|
|
142
|
+
if (trackBox.index.getValue() <= sourceIndex) {
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
if (RegionKeepExistingResolver.#hasSpace(trackBox, position, complete)) {
|
|
146
|
+
return boxAdapters.adapterFor(trackBox, TrackBoxAdapter);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
// No suitable existing track found, create new track below source
|
|
150
|
+
const insertIndex = sourceIndex + 1;
|
|
151
|
+
const newTrackBox = trackType === TrackType.Audio
|
|
152
|
+
? projectApi.createAudioTrack(audioUnit, insertIndex)
|
|
153
|
+
: projectApi.createNoteTrack(audioUnit, insertIndex);
|
|
154
|
+
return boxAdapters.adapterFor(newTrackBox, TrackBoxAdapter);
|
|
155
|
+
}
|
|
156
|
+
static #hasSpace(trackBox, position, complete) {
|
|
157
|
+
for (const vertex of trackBox.regions.pointerHub.incoming()) {
|
|
158
|
+
const regionPosition = vertex.box.position.getValue();
|
|
159
|
+
const regionDuration = vertex.box.duration.getValue();
|
|
160
|
+
const regionComplete = regionPosition + regionDuration;
|
|
161
|
+
if (regionComplete <= position) {
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
if (regionPosition >= complete) {
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
169
|
+
return true;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { Exec, int } from "@opendaw/lib-std";
|
|
2
|
+
import { ppqn } from "@opendaw/lib-dsp";
|
|
3
|
+
import { BoxEditing } from "@opendaw/lib-box";
|
|
4
|
+
import { AnyRegionBoxAdapter, BoxAdapters, TrackBoxAdapter } from "@opendaw/studio-adapters";
|
|
5
|
+
import { RegionModifyStrategies } from "./RegionModifyStrategies";
|
|
6
|
+
import { ProjectApi } from "../../project";
|
|
7
|
+
import { TrackResolver } from "./TrackResolver";
|
|
8
|
+
export declare class RegionOverlapResolver {
|
|
9
|
+
#private;
|
|
10
|
+
constructor(editing: BoxEditing, projectApi: ProjectApi, boxAdapters: BoxAdapters);
|
|
11
|
+
/**
|
|
12
|
+
* For selection-based operations (move, resize, copy).
|
|
13
|
+
* @param tracks - The tracks involved in the operation
|
|
14
|
+
* @param adapters - The region adapters being modified
|
|
15
|
+
* @param strategy - The modify strategy (determines if it's a copy, etc.)
|
|
16
|
+
* @param deltaIndex - Track index delta (for cross-track moves)
|
|
17
|
+
* @param changes - Callback that performs the actual changes. Receives a TrackResolver
|
|
18
|
+
* that should be used to determine target tracks for copies.
|
|
19
|
+
*/
|
|
20
|
+
apply(tracks: ReadonlyArray<TrackBoxAdapter>, adapters: ReadonlyArray<AnyRegionBoxAdapter>, strategy: RegionModifyStrategies, deltaIndex: int, changes: (trackResolver: TrackResolver) => void): void;
|
|
21
|
+
/**
|
|
22
|
+
* For range-based operations (drop, duplicate).
|
|
23
|
+
* Returns the target track to use (may differ from input track for keep-existing mode).
|
|
24
|
+
*/
|
|
25
|
+
resolveTargetTrack(track: TrackBoxAdapter, position: ppqn, complete: ppqn): TrackBoxAdapter;
|
|
26
|
+
/**
|
|
27
|
+
* Creates a resolver function for range-based operations (to be called inside an existing transaction).
|
|
28
|
+
* Call this BEFORE creating the region to capture the "before" state.
|
|
29
|
+
* Then call the returned function AFTER creating the region.
|
|
30
|
+
*/
|
|
31
|
+
fromRange(track: TrackBoxAdapter, position: ppqn, complete: ppqn): Exec;
|
|
32
|
+
}
|
|
33
|
+
//# sourceMappingURL=RegionOverlapResolver.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"RegionOverlapResolver.d.ts","sourceRoot":"","sources":["../../../src/ui/timeline/RegionOverlapResolver.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,IAAI,EAAE,GAAG,EAAC,MAAM,kBAAkB,CAAA;AAC1C,OAAO,EAAC,IAAI,EAAC,MAAM,kBAAkB,CAAA;AACrC,OAAO,EAAC,UAAU,EAAC,MAAM,kBAAkB,CAAA;AAC3C,OAAO,EAAC,mBAAmB,EAAE,WAAW,EAAE,eAAe,EAAC,MAAM,0BAA0B,CAAA;AAC1F,OAAO,EAAC,sBAAsB,EAAC,MAAM,0BAA0B,CAAA;AAK/D,OAAO,EAAC,UAAU,EAAC,MAAM,eAAe,CAAA;AACxC,OAAO,EAAC,aAAa,EAAC,MAAM,iBAAiB,CAAA;AAE7C,qBAAa,qBAAqB;;gBAKlB,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,UAAU,EAAE,WAAW,EAAE,WAAW;IAMjF;;;;;;;;OAQG;IACH,KAAK,CAAC,MAAM,EAAE,aAAa,CAAC,eAAe,CAAC,EACtC,QAAQ,EAAE,aAAa,CAAC,mBAAmB,CAAC,EAC5C,QAAQ,EAAE,sBAAsB,EAChC,UAAU,EAAE,GAAG,EACf,OAAO,EAAE,CAAC,aAAa,EAAE,aAAa,KAAK,IAAI,GAAG,IAAI;IA2B5D;;;OAGG;IACH,kBAAkB,CAAC,KAAK,EAAE,eAAe,EAAE,QAAQ,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,GAAG,eAAe;IAS3F;;;;OAIG;IACH,SAAS,CAAC,KAAK,EAAE,eAAe,EAAE,QAAQ,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,GAAG,IAAI;CAW1E"}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { RegionClipResolver } from "./RegionClipResolver";
|
|
2
|
+
import { RegionKeepExistingResolver } from "./RegionKeepExistingResolver";
|
|
3
|
+
import { RegionPushExistingResolver } from "./RegionPushExistingResolver";
|
|
4
|
+
import { StudioPreferences } from "../../StudioPreferences";
|
|
5
|
+
export class RegionOverlapResolver {
|
|
6
|
+
#editing;
|
|
7
|
+
#projectApi;
|
|
8
|
+
#boxAdapters;
|
|
9
|
+
constructor(editing, projectApi, boxAdapters) {
|
|
10
|
+
this.#editing = editing;
|
|
11
|
+
this.#projectApi = projectApi;
|
|
12
|
+
this.#boxAdapters = boxAdapters;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* For selection-based operations (move, resize, copy).
|
|
16
|
+
* @param tracks - The tracks involved in the operation
|
|
17
|
+
* @param adapters - The region adapters being modified
|
|
18
|
+
* @param strategy - The modify strategy (determines if it's a copy, etc.)
|
|
19
|
+
* @param deltaIndex - Track index delta (for cross-track moves)
|
|
20
|
+
* @param changes - Callback that performs the actual changes. Receives a TrackResolver
|
|
21
|
+
* that should be used to determine target tracks for copies.
|
|
22
|
+
*/
|
|
23
|
+
apply(tracks, adapters, strategy, deltaIndex, changes) {
|
|
24
|
+
const behaviour = StudioPreferences.settings.editing["overlapping-regions-behaviour"];
|
|
25
|
+
if (behaviour === "clip") {
|
|
26
|
+
const { postProcess, trackResolver } = RegionClipResolver.fromSelection(tracks, adapters, strategy, deltaIndex);
|
|
27
|
+
this.#editing.modify(() => {
|
|
28
|
+
changes(trackResolver);
|
|
29
|
+
postProcess();
|
|
30
|
+
});
|
|
31
|
+
RegionClipResolver.validateTracks(tracks);
|
|
32
|
+
}
|
|
33
|
+
else if (behaviour === "push-existing") {
|
|
34
|
+
const { postProcess, trackResolver } = RegionPushExistingResolver.fromSelection(tracks, adapters, strategy, deltaIndex, this.#projectApi, this.#boxAdapters);
|
|
35
|
+
this.#editing.modify(() => {
|
|
36
|
+
changes(trackResolver);
|
|
37
|
+
postProcess();
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
// keep-existing
|
|
42
|
+
const { postProcess, trackResolver } = RegionKeepExistingResolver.fromSelection(tracks, adapters, strategy, deltaIndex, this.#projectApi, this.#boxAdapters);
|
|
43
|
+
this.#editing.modify(() => {
|
|
44
|
+
changes(trackResolver);
|
|
45
|
+
postProcess();
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* For range-based operations (drop, duplicate).
|
|
51
|
+
* Returns the target track to use (may differ from input track for keep-existing mode).
|
|
52
|
+
*/
|
|
53
|
+
resolveTargetTrack(track, position, complete) {
|
|
54
|
+
const behaviour = StudioPreferences.settings.editing["overlapping-regions-behaviour"];
|
|
55
|
+
if (behaviour === "keep-existing") {
|
|
56
|
+
return RegionKeepExistingResolver
|
|
57
|
+
.resolveTargetTrack(track, position, complete, this.#projectApi, this.#boxAdapters);
|
|
58
|
+
}
|
|
59
|
+
return track;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Creates a resolver function for range-based operations (to be called inside an existing transaction).
|
|
63
|
+
* Call this BEFORE creating the region to capture the "before" state.
|
|
64
|
+
* Then call the returned function AFTER creating the region.
|
|
65
|
+
*/
|
|
66
|
+
fromRange(track, position, complete) {
|
|
67
|
+
const behaviour = StudioPreferences.settings.editing["overlapping-regions-behaviour"];
|
|
68
|
+
if (behaviour === "clip") {
|
|
69
|
+
return RegionClipResolver.fromRange(track, position, complete);
|
|
70
|
+
}
|
|
71
|
+
else if (behaviour === "push-existing") {
|
|
72
|
+
return RegionPushExistingResolver.fromRange(track, position, complete, this.#projectApi, this.#boxAdapters);
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
// keep-existing: nothing to do after creation - caller should use resolveTargetTrack before creating
|
|
76
|
+
return () => { };
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { Exec, int } from "@opendaw/lib-std";
|
|
2
|
+
import { ppqn } from "@opendaw/lib-dsp";
|
|
3
|
+
import { AnyRegionBoxAdapter, BoxAdapters, TrackBoxAdapter } from "@opendaw/studio-adapters";
|
|
4
|
+
import { RegionModifyStrategies } from "./RegionModifyStrategies";
|
|
5
|
+
import { ProjectApi } from "../../project";
|
|
6
|
+
import { TrackResolver } from "./TrackResolver";
|
|
7
|
+
/**
|
|
8
|
+
* Resolver for "push-existing" overlap behavior.
|
|
9
|
+
* When overlap is detected, the EXISTING (overlapped) regions move to a track below.
|
|
10
|
+
* Incoming regions stay where they were placed.
|
|
11
|
+
*/
|
|
12
|
+
export declare class RegionPushExistingResolver {
|
|
13
|
+
#private;
|
|
14
|
+
/**
|
|
15
|
+
* For selection-based operations (move, resize).
|
|
16
|
+
* Returns:
|
|
17
|
+
* - postProcess: function to call AFTER changes are applied
|
|
18
|
+
* - trackResolver: Identity (incoming regions stay where placed)
|
|
19
|
+
*/
|
|
20
|
+
static fromSelection(tracks: ReadonlyArray<TrackBoxAdapter>, adapters: ReadonlyArray<AnyRegionBoxAdapter>, strategy: RegionModifyStrategies, deltaIndex: int, projectApi: ProjectApi, boxAdapters: BoxAdapters): {
|
|
21
|
+
postProcess: Exec;
|
|
22
|
+
trackResolver: TrackResolver;
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* For range-based operations (drop, duplicate).
|
|
26
|
+
* Returns a function that should be called AFTER the region is created.
|
|
27
|
+
*/
|
|
28
|
+
static fromRange(track: TrackBoxAdapter, position: ppqn, complete: ppqn, projectApi: ProjectApi, boxAdapters: BoxAdapters): Exec;
|
|
29
|
+
private constructor();
|
|
30
|
+
}
|
|
31
|
+
//# sourceMappingURL=RegionPushExistingResolver.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"RegionPushExistingResolver.d.ts","sourceRoot":"","sources":["../../../src/ui/timeline/RegionPushExistingResolver.ts"],"names":[],"mappings":"AAAA,OAAO,EAAY,IAAI,EAAE,GAAG,EAAsB,MAAM,kBAAkB,CAAA;AAC1E,OAAO,EAAC,IAAI,EAAC,MAAM,kBAAkB,CAAA;AACrC,OAAO,EAAC,mBAAmB,EAAE,WAAW,EAAE,eAAe,EAAY,MAAM,0BAA0B,CAAA;AAErG,OAAO,EAAC,sBAAsB,EAAC,MAAM,0BAA0B,CAAA;AAC/D,OAAO,EAAC,UAAU,EAAC,MAAM,eAAe,CAAA;AACxC,OAAO,EAAC,aAAa,EAAC,MAAM,iBAAiB,CAAA;AAE7C;;;;GAIG;AACH,qBAAa,0BAA0B;;IACnC;;;;;OAKG;IACH,MAAM,CAAC,aAAa,CAAC,MAAM,EAAE,aAAa,CAAC,eAAe,CAAC,EACtC,QAAQ,EAAE,aAAa,CAAC,mBAAmB,CAAC,EAC5C,QAAQ,EAAE,sBAAsB,EAChC,UAAU,EAAE,GAAG,EACf,UAAU,EAAE,UAAU,EACtB,WAAW,EAAE,WAAW,GAAG;QAAE,WAAW,EAAE,IAAI,CAAC;QAAC,aAAa,EAAE,aAAa,CAAA;KAAE;IA6BnG;;;OAGG;IACH,MAAM,CAAC,SAAS,CAAC,KAAK,EAAE,eAAe,EACtB,QAAQ,EAAE,IAAI,EACd,QAAQ,EAAE,IAAI,EACd,UAAU,EAAE,UAAU,EACtB,WAAW,EAAE,WAAW,GAAG,IAAI;IAqEhD,OAAO;CAsBV"}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { EmptyExec, isDefined } from "@opendaw/lib-std";
|
|
2
|
+
import { TrackBoxAdapter, TrackType } from "@opendaw/studio-adapters";
|
|
3
|
+
import { TrackResolver } from "./TrackResolver";
|
|
4
|
+
/**
|
|
5
|
+
* Resolver for "push-existing" overlap behavior.
|
|
6
|
+
* When overlap is detected, the EXISTING (overlapped) regions move to a track below.
|
|
7
|
+
* Incoming regions stay where they were placed.
|
|
8
|
+
*/
|
|
9
|
+
export class RegionPushExistingResolver {
|
|
10
|
+
/**
|
|
11
|
+
* For selection-based operations (move, resize).
|
|
12
|
+
* Returns:
|
|
13
|
+
* - postProcess: function to call AFTER changes are applied
|
|
14
|
+
* - trackResolver: Identity (incoming regions stay where placed)
|
|
15
|
+
*/
|
|
16
|
+
static fromSelection(tracks, adapters, strategy, deltaIndex, projectApi, boxAdapters) {
|
|
17
|
+
const resolver = new RegionPushExistingResolver(projectApi, boxAdapters);
|
|
18
|
+
// Capture overlapped regions BEFORE changes
|
|
19
|
+
for (const track of tracks) {
|
|
20
|
+
const selectedStrategy = strategy.selectedModifyStrategy();
|
|
21
|
+
const overlapped = [];
|
|
22
|
+
for (const adapter of adapters) {
|
|
23
|
+
const adapterTrackIndex = adapter.trackBoxAdapter.unwrap().listIndex + deltaIndex;
|
|
24
|
+
if (adapterTrackIndex !== track.listIndex) {
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
const position = selectedStrategy.readPosition(adapter);
|
|
28
|
+
const complete = selectedStrategy.readComplete(adapter);
|
|
29
|
+
for (const region of track.regions.collection.iterateRange(0, complete)) {
|
|
30
|
+
if (region.complete <= position) {
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
if (region.position >= complete) {
|
|
34
|
+
break;
|
|
35
|
+
}
|
|
36
|
+
if (region.isSelected && !strategy.showOrigin()) {
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
if (!overlapped.includes(region)) {
|
|
40
|
+
overlapped.push(region);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
if (overlapped.length > 0) {
|
|
45
|
+
resolver.#overlappedByTrack.set(track, overlapped);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return {
|
|
49
|
+
postProcess: () => resolver.#solve(),
|
|
50
|
+
trackResolver: TrackResolver.Identity
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* For range-based operations (drop, duplicate).
|
|
55
|
+
* Returns a function that should be called AFTER the region is created.
|
|
56
|
+
*/
|
|
57
|
+
static fromRange(track, position, complete, projectApi, boxAdapters) {
|
|
58
|
+
if (track.type === TrackType.Value) {
|
|
59
|
+
return () => { };
|
|
60
|
+
}
|
|
61
|
+
// Find overlapped regions BEFORE changes
|
|
62
|
+
const overlapped = [];
|
|
63
|
+
for (const region of track.regions.collection.iterateRange(0, complete)) {
|
|
64
|
+
if (region.complete <= position) {
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
if (region.position >= complete) {
|
|
68
|
+
break;
|
|
69
|
+
}
|
|
70
|
+
overlapped.push(region);
|
|
71
|
+
}
|
|
72
|
+
if (overlapped.length === 0) {
|
|
73
|
+
return EmptyExec;
|
|
74
|
+
}
|
|
75
|
+
return () => {
|
|
76
|
+
const targetTrack = RegionPushExistingResolver.#findOrCreateTrackBelow(track, overlapped, projectApi, boxAdapters);
|
|
77
|
+
if (isDefined(targetTrack)) {
|
|
78
|
+
for (const region of overlapped) {
|
|
79
|
+
region.box.regions.refer(targetTrack.box.regions);
|
|
80
|
+
}
|
|
81
|
+
track.regions.dispatchChange();
|
|
82
|
+
targetTrack.regions.dispatchChange();
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
static #findOrCreateTrackBelow(sourceTrack, regionsToPlace, projectApi, boxAdapters) {
|
|
87
|
+
const trackType = sourceTrack.type;
|
|
88
|
+
if (trackType === TrackType.Value) {
|
|
89
|
+
return undefined;
|
|
90
|
+
}
|
|
91
|
+
const minPosition = Math.min(...regionsToPlace.map(region => region.position));
|
|
92
|
+
const maxComplete = Math.max(...regionsToPlace.map(region => region.complete));
|
|
93
|
+
const audioUnit = sourceTrack.audioUnit;
|
|
94
|
+
// Get all tracks of same type in this audio unit, sorted by index
|
|
95
|
+
const siblingTracks = audioUnit.tracks.pointerHub.incoming()
|
|
96
|
+
.map(vertex => vertex.box)
|
|
97
|
+
.filter(trackBox => trackBox.type.getValue() === trackType)
|
|
98
|
+
.sort((boxA, boxB) => boxA.index.getValue() - boxB.index.getValue());
|
|
99
|
+
const sourceIndex = sourceTrack.indexField.getValue();
|
|
100
|
+
// Look for existing track below with space
|
|
101
|
+
for (const trackBox of siblingTracks) {
|
|
102
|
+
if (trackBox.index.getValue() <= sourceIndex) {
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
if (RegionPushExistingResolver.#hasSpace(trackBox, minPosition, maxComplete)) {
|
|
106
|
+
return boxAdapters.adapterFor(trackBox, TrackBoxAdapter);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
// No suitable existing track found, create new track below source
|
|
110
|
+
const insertIndex = sourceIndex + 1;
|
|
111
|
+
const newTrackBox = trackType === TrackType.Audio
|
|
112
|
+
? projectApi.createAudioTrack(audioUnit, insertIndex)
|
|
113
|
+
: projectApi.createNoteTrack(audioUnit, insertIndex);
|
|
114
|
+
return boxAdapters.adapterFor(newTrackBox, TrackBoxAdapter);
|
|
115
|
+
}
|
|
116
|
+
static #hasSpace(trackBox, position, complete) {
|
|
117
|
+
for (const vertex of trackBox.regions.pointerHub.incoming()) {
|
|
118
|
+
const regionPosition = vertex.box.position.getValue();
|
|
119
|
+
const regionDuration = vertex.box.duration.getValue();
|
|
120
|
+
const regionComplete = regionPosition + regionDuration;
|
|
121
|
+
if (regionComplete <= position) {
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
if (regionPosition >= complete) {
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
return true;
|
|
130
|
+
}
|
|
131
|
+
#projectApi;
|
|
132
|
+
#boxAdapters;
|
|
133
|
+
#overlappedByTrack;
|
|
134
|
+
constructor(projectApi, boxAdapters) {
|
|
135
|
+
this.#projectApi = projectApi;
|
|
136
|
+
this.#boxAdapters = boxAdapters;
|
|
137
|
+
this.#overlappedByTrack = new Map();
|
|
138
|
+
}
|
|
139
|
+
#solve() {
|
|
140
|
+
const affectedTracks = [];
|
|
141
|
+
for (const [track, overlapped] of this.#overlappedByTrack) {
|
|
142
|
+
const targetTrack = RegionPushExistingResolver.#findOrCreateTrackBelow(track, overlapped, this.#projectApi, this.#boxAdapters);
|
|
143
|
+
if (!isDefined(targetTrack)) {
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
if (!affectedTracks.includes(targetTrack)) {
|
|
147
|
+
affectedTracks.push(targetTrack);
|
|
148
|
+
}
|
|
149
|
+
if (!affectedTracks.includes(track)) {
|
|
150
|
+
affectedTracks.push(track);
|
|
151
|
+
}
|
|
152
|
+
for (const region of overlapped) {
|
|
153
|
+
region.box.regions.refer(targetTrack.box.regions);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
// Dispatch change on all affected tracks to ensure UI updates
|
|
157
|
+
affectedTracks.forEach(track => track.regions.dispatchChange());
|
|
158
|
+
}
|
|
159
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { MutableObservableOption, Terminable } from "@opendaw/lib-std";
|
|
2
|
+
import { AnyRegionBoxAdapter, TrackBoxAdapter } from "@opendaw/studio-adapters";
|
|
3
|
+
export declare class TimelineFocus implements Terminable {
|
|
4
|
+
#private;
|
|
5
|
+
constructor();
|
|
6
|
+
get track(): MutableObservableOption<TrackBoxAdapter>;
|
|
7
|
+
get region(): MutableObservableOption<AnyRegionBoxAdapter>;
|
|
8
|
+
focusTrack(track: TrackBoxAdapter): void;
|
|
9
|
+
focusRegion(region: AnyRegionBoxAdapter): void;
|
|
10
|
+
clearRegionFocus(): void;
|
|
11
|
+
clear(): void;
|
|
12
|
+
terminate(): void;
|
|
13
|
+
}
|
|
14
|
+
//# sourceMappingURL=TimelineFocus.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"TimelineFocus.d.ts","sourceRoot":"","sources":["../../../src/ui/timeline/TimelineFocus.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,uBAAuB,EAAE,UAAU,EAAa,MAAM,kBAAkB,CAAA;AAChF,OAAO,EAAC,mBAAmB,EAAE,eAAe,EAAC,MAAM,0BAA0B,CAAA;AAE7E,qBAAa,aAAc,YAAW,UAAU;;;IAY5C,IAAI,KAAK,IAAI,uBAAuB,CAAC,eAAe,CAAC,CAAqB;IAC1E,IAAI,MAAM,IAAI,uBAAuB,CAAC,mBAAmB,CAAC,CAAsB;IAEhF,UAAU,CAAC,KAAK,EAAE,eAAe,GAAG,IAAI;IAWxC,WAAW,CAAC,MAAM,EAAE,mBAAmB,GAAG,IAAI;IAW9C,gBAAgB,IAAI,IAAI;IAKxB,KAAK,IAAI,IAAI;IAOb,SAAS,IAAI,IAAI;CACpB"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { MutableObservableOption, Terminator } from "@opendaw/lib-std";
|
|
2
|
+
export class TimelineFocus {
|
|
3
|
+
#terminator = new Terminator();
|
|
4
|
+
#trackSubscription = this.#terminator.own(new Terminator());
|
|
5
|
+
#regionSubscription = this.#terminator.own(new Terminator());
|
|
6
|
+
#track;
|
|
7
|
+
#region;
|
|
8
|
+
constructor() {
|
|
9
|
+
this.#track = this.#terminator.own(new MutableObservableOption());
|
|
10
|
+
this.#region = this.#terminator.own(new MutableObservableOption());
|
|
11
|
+
}
|
|
12
|
+
get track() { return this.#track; }
|
|
13
|
+
get region() { return this.#region; }
|
|
14
|
+
focusTrack(track) {
|
|
15
|
+
this.#trackSubscription.terminate();
|
|
16
|
+
this.#trackSubscription.own(track.box.subscribeDeletion(() => {
|
|
17
|
+
console.debug("TimelineFocus: track deleted");
|
|
18
|
+
this.#trackSubscription.terminate();
|
|
19
|
+
this.#track.clear();
|
|
20
|
+
}));
|
|
21
|
+
this.#track.wrap(track);
|
|
22
|
+
this.clearRegionFocus();
|
|
23
|
+
}
|
|
24
|
+
focusRegion(region) {
|
|
25
|
+
region.trackBoxAdapter.ifSome(track => this.focusTrack(track));
|
|
26
|
+
this.#regionSubscription.terminate();
|
|
27
|
+
this.#regionSubscription.own(region.box.subscribeDeletion(() => {
|
|
28
|
+
console.debug("TimelineFocus: region deleted");
|
|
29
|
+
this.#regionSubscription.terminate();
|
|
30
|
+
this.#region.clear();
|
|
31
|
+
}));
|
|
32
|
+
this.#region.wrap(region);
|
|
33
|
+
}
|
|
34
|
+
clearRegionFocus() {
|
|
35
|
+
this.#regionSubscription.terminate();
|
|
36
|
+
this.#region.clear();
|
|
37
|
+
}
|
|
38
|
+
clear() {
|
|
39
|
+
this.#trackSubscription.terminate();
|
|
40
|
+
this.#regionSubscription.terminate();
|
|
41
|
+
this.#track.clear();
|
|
42
|
+
this.#region.clear();
|
|
43
|
+
}
|
|
44
|
+
terminate() { this.#terminator.terminate(); }
|
|
45
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { AnyRegionBoxAdapter, TrackBoxAdapter } from "@opendaw/studio-adapters";
|
|
2
|
+
/**
|
|
3
|
+
* Function to resolve the target track for a region.
|
|
4
|
+
* Used by overlap resolvers to provide track overrides for copy operations.
|
|
5
|
+
*/
|
|
6
|
+
export type TrackResolver = (adapter: AnyRegionBoxAdapter, defaultTrack: TrackBoxAdapter) => TrackBoxAdapter;
|
|
7
|
+
export declare namespace TrackResolver {
|
|
8
|
+
/** Identity resolver - always returns the default track */
|
|
9
|
+
const Identity: TrackResolver;
|
|
10
|
+
}
|
|
11
|
+
//# sourceMappingURL=TrackResolver.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"TrackResolver.d.ts","sourceRoot":"","sources":["../../../src/ui/timeline/TrackResolver.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,mBAAmB,EAAE,eAAe,EAAC,MAAM,0BAA0B,CAAA;AAE7E;;;GAGG;AACH,MAAM,MAAM,aAAa,GAAG,CAAC,OAAO,EAAE,mBAAmB,EAAE,YAAY,EAAE,eAAe,KAAK,eAAe,CAAA;AAE5G,yBAAiB,aAAa,CAAC;IAC3B,2DAA2D;IACpD,MAAM,QAAQ,EAAE,aAAwD,CAAA;CAClF"}
|