fixes external resource handling
This commit is contained in:
@@ -15,12 +15,14 @@ export namespace ClipboardUtils {
|
||||
|
||||
export const serializeBoxes = (boxes: ReadonlyArray<Box>,
|
||||
metadata: ArrayBufferLike = new ArrayBuffer(0)): ArrayBufferLike => {
|
||||
const uniqueBoxes = UUID.newSet<Box>(box => box.address.uuid)
|
||||
boxes.forEach(box => uniqueBoxes.add(box))
|
||||
const typeCounts = new Map<string, number>()
|
||||
boxes.forEach(box => typeCounts.set(box.name, (typeCounts.get(box.name) ?? 0) + 1))
|
||||
uniqueBoxes.forEach(box => typeCounts.set(box.name, (typeCounts.get(box.name) ?? 0) + 1))
|
||||
const types = [...typeCounts.entries()].map(([type, count]) => `${type}: ${count}`).join(", ")
|
||||
const clipboardGraph = new BoxGraph<BoxIO.TypeMap>(Option.wrap(BoxIO.create))
|
||||
clipboardGraph.beginTransaction()
|
||||
boxes.forEach(sourceBox => {
|
||||
uniqueBoxes.forEach(sourceBox => {
|
||||
const input = new ByteArrayInput(sourceBox.toArrayBuffer())
|
||||
clipboardGraph.createBox(sourceBox.name as keyof BoxIO.TypeMap, sourceBox.address.uuid, box => box.read(input))
|
||||
})
|
||||
@@ -50,7 +52,21 @@ export namespace ClipboardUtils {
|
||||
input.readBytes(graphData)
|
||||
const clipboardGraph = new BoxGraph<BoxIO.TypeMap>(Option.wrap(BoxIO.create))
|
||||
clipboardGraph.fromArrayBuffer(graphData.buffer)
|
||||
const sourceBoxes = clipboardGraph.boxes().filter(box => !options.excludeBox?.(box))
|
||||
const skippedExternalUuids = UUID.newSet<UUID.Bytes>(uuid => uuid)
|
||||
clipboardGraph.boxes().forEach(box => {
|
||||
if (box.resource === "external" && targetGraph.findBox(box.address.uuid).nonEmpty()) {
|
||||
skippedExternalUuids.add(box.address.uuid)
|
||||
}
|
||||
})
|
||||
const shouldSkipBox = (box: Box): boolean => {
|
||||
if (options.excludeBox?.(box)) {return true}
|
||||
if (box.resource === "external" && skippedExternalUuids.hasKey(box.address.uuid)) {return true}
|
||||
for (const [, targetAddress] of box.outgoingEdges()) {
|
||||
if (skippedExternalUuids.hasKey(targetAddress.uuid) && !targetAddress.isBox()) {return true}
|
||||
}
|
||||
return false
|
||||
}
|
||||
const sourceBoxes = clipboardGraph.boxes().filter(box => !shouldSkipBox(box))
|
||||
const typeCounts = new Map<string, number>()
|
||||
sourceBoxes.forEach(box => typeCounts.set(box.name, (typeCounts.get(box.name) ?? 0) + 1))
|
||||
console.debug("Clipboard paste:", [...typeCounts.entries()].map(([type, count]) => `${type}: ${count}`).join(", "))
|
||||
@@ -59,6 +75,7 @@ export namespace ClipboardUtils {
|
||||
const isExternal = box.resource === "external"
|
||||
uuidMap.add({source: box.address.uuid, target: isExternal ? box.address.uuid : UUID.generate()})
|
||||
})
|
||||
skippedExternalUuids.forEach(uuid => uuidMap.add({source: uuid, target: uuid}))
|
||||
const result: Array<T> = []
|
||||
PointerField.decodeWith({
|
||||
map: (pointer, address) => {
|
||||
@@ -68,8 +85,6 @@ export namespace ClipboardUtils {
|
||||
return options.mapPointer(pointer, address)
|
||||
}
|
||||
}, () => sourceBoxes.forEach(sourceBox => {
|
||||
const isExternal = sourceBox.resource === "external"
|
||||
if (isExternal && targetGraph.findBox(sourceBox.address.uuid).nonEmpty()) {return}
|
||||
const inputStream = new ByteArrayInput(sourceBox.toArrayBuffer())
|
||||
const targetUuid = uuidMap.get(sourceBox.address.uuid).target
|
||||
targetGraph.createBox(sourceBox.name as keyof BoxIO.TypeMap, targetUuid, box => {
|
||||
|
||||
@@ -60,6 +60,30 @@ subscribeDeletion(listener: Exec): Subscription {
|
||||
}
|
||||
```
|
||||
|
||||
```typescript
|
||||
// packages/lib/box/src/graph.ts
|
||||
readonly #deletionListeners: SortedSet<Readonly<Uint8Array>, { uuid: UUID.Bytes, listeners: Set<Exec> }>
|
||||
|
||||
subscribeDeletion(uuid: UUID.Bytes, listener: Exec): Subscription {
|
||||
const entry = this.#deletionListeners.getOrCreate(uuid, () => ({uuid, listeners: new Set()}))
|
||||
entry.listeners.add(listener)
|
||||
return {
|
||||
terminate: () => {
|
||||
entry.listeners.delete(listener)
|
||||
if (entry.listeners.size === 0) {
|
||||
this.#deletionListeners.removeByKey(uuid)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
unstageBox(box: Box): void {
|
||||
// ... existing code ...
|
||||
this.#deletionListeners.removeByKeyIfExist(box.address.uuid)?.listeners.forEach(listener => listener())
|
||||
// ... rest of method ...
|
||||
}
|
||||
```
|
||||
|
||||
Deletion listeners are stored in `BoxGraph.#deletionListeners` and notified during `unstageBox()`.
|
||||
|
||||
---
|
||||
@@ -381,6 +405,39 @@ const pasteRegions = (
|
||||
- Silently skip entire paste
|
||||
- No partial pastes to avoid orphaned dependency boxes
|
||||
|
||||
### External Resource Handling (AudioFileBox)
|
||||
|
||||
External resources like `AudioFileBox` require special handling during paste:
|
||||
|
||||
1. **UUID Preservation**: External resources keep their original UUID (not regenerated)
|
||||
2. **Skip if exists**: If the external resource already exists in the target graph, don't create a duplicate
|
||||
3. **Skip field-level children**: Boxes that point to fields within an existing external resource (e.g., `TransientMarkerBox` pointing to `AudioFileBox.transientMarkers`) are skipped to avoid duplicates
|
||||
|
||||
```typescript
|
||||
// In ClipboardUtils.deserializeBoxes
|
||||
const skippedExternalUuids = UUID.newSet<UUID.Bytes>(uuid => uuid)
|
||||
clipboardGraph.boxes().forEach(box => {
|
||||
if (box.resource === "external" && targetGraph.findBox(box.address.uuid).nonEmpty()) {
|
||||
skippedExternalUuids.add(box.address.uuid)
|
||||
}
|
||||
})
|
||||
|
||||
const shouldSkipBox = (box: Box): boolean => {
|
||||
if (options.excludeBox?.(box)) {return true}
|
||||
// Skip external resources that already exist
|
||||
if (box.resource === "external" && skippedExternalUuids.hasKey(box.address.uuid)) {return true}
|
||||
// Skip boxes pointing to FIELDS within skipped external resources (children like TransientMarkerBox)
|
||||
for (const [, targetAddress] of box.outgoingEdges()) {
|
||||
if (skippedExternalUuids.hasKey(targetAddress.uuid) && !targetAddress.isBox()) {return true}
|
||||
}
|
||||
return false
|
||||
}
|
||||
```
|
||||
|
||||
The `!targetAddress.isBox()` check distinguishes between:
|
||||
- `AudioRegionBox` → `AudioFileBox` (box-level reference, allowed)
|
||||
- `TransientMarkerBox` → `AudioFileBox.transientMarkers` (field-level reference, skipped)
|
||||
|
||||
### Audio File Not in Project
|
||||
- AudioFileBox created with same UUID
|
||||
- Audio won't play until file is available
|
||||
@@ -429,6 +486,7 @@ packages/studio/core/src/ui/timeline/TimelineFocus.ts
|
||||
packages/studio/core/src/project/Project.ts - added timelineFocus
|
||||
packages/lib/box/src/box.ts - added subscribeDeletion()
|
||||
packages/lib/box/src/graph.ts - added #deletionListeners and subscribeDeletion()
|
||||
packages/studio/core/src/ui/clipboard/ClipboardUtils.ts - external resource handling, skip field-level children
|
||||
packages/app/studio/src/ui/timeline/tracks/audio-unit/regions/RegionsArea.tsx - focus handling, clipboard integration
|
||||
packages/app/studio/src/ui/timeline/tracks/audio-unit/regions/RegionLane.tsx - focus visual feedback
|
||||
packages/app/studio/src/ui/timeline/tracks/audio-unit/regions/RegionLane.sass - .focused styling
|
||||
|
||||
Reference in New Issue
Block a user