0.8.5: multi-select tracks, group + convert-to-clip
Multi-select
- New `selectedLayerIds: string[]` on the scene store — ordered by
click, anchor = last entry, always equal to `selectedLayerId`.
- `toggleLayerSelection(id)` shift-click behaviour: adds or removes,
anchor follows the last click, effectively-locked layers refuse.
- `selectLayer`, `addLayer`, and every selection-reset path keep
the set in lock-step with the anchor.
Timeline
- Shift-click on a layer row's name toggles that layer's
membership in the multi-selection. Every selected row highlights
via `store.selectedLayerIds.includes(track.layerId)`.
- Right-click on a track now preserves an existing multi-selection
if the clicked track is part of it; lands as a single-select
otherwise.
- Track context menu adds two entries when ≥1 layer is selected:
- "Group N selected layers" — calls the new store action and
drops the group into the topmost selected track's stack slot
(not at the very top as a naive `addTrackGroup` would).
- "Convert N selected to clip" — packs the layers into a fresh
clip with `duration = scene.duration`, deep-clones via
`structuredClone(toRaw(l))`, drops a single `ClipInstance` on
a new clipTrack at `startTime = 0`, and positions the
clipTrack at the topmost selected track's slot.
Fixes
- `removeLayer` no longer unconditionally clears
`selectedLayerIds` (bulk-replace artifact from the multi-select
work); the anchor-matched path clears the whole set, the
non-anchor path only splices out that one id.
- `deselectIfLocked` indentation normalised.
Regression tests
- Multi-select store API: selectLayer sync, toggleLayerSelection
add/remove/anchor, lock-filter, empty-selection guards.
- `groupSelectedLayers`: happy-path packing, empty-select null
return, lock-filter, stack-slot pinning.
- `convertSelectedLayersToClip`: packs layers + spawns clipTrack +
instance (deep-clone integrity pinned via unique per-layer names
so the cloned-layer lookup is unambiguous), drops the clipTrack
at the topmost slot, returns null on empty selection, skips
effectively-locked layers.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
af5f99380c
commit
53c0410824
7 files changed
+467
-7
No files matched your search
@@ -1877,12 +1877,27 @@ async function onClipBlockContextMenu(e: MouseEvent, ci: ClipInstance) {
|
||||
const trackMenu = ref<TrackMenuState | null>(null)
|
||||
const trackMenuRef = ref<HTMLDivElement | null>(null)
|
||||
|
||||
function onLayerTrackNameClick(e: MouseEvent, track: TimelineLayerTrack) {
|
||||
if (isTrackEffectivelyLocked(track)) return
|
||||
if (e.shiftKey && track.isParent) {
|
||||
store.toggleLayerSelection(track.layerId)
|
||||
return
|
||||
}
|
||||
store.selectLayer(track.layerId)
|
||||
}
|
||||
|
||||
function onTrackContextMenu(e: MouseEvent, layerId: string) {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
const layer = store.activeLayers.find((l) => l.id === layerId)
|
||||
if (!layer) return
|
||||
store.selectLayer(layerId)
|
||||
// If the right-clicked layer isn't already in the multi-selection,
|
||||
// treat the right-click as a single-select so the menu actions
|
||||
// operate on what the user actually clicked. Multi-selected ids
|
||||
// persist so "Group selected" has a real list to work with.
|
||||
if (!store.selectedLayerIds.includes(layerId)) {
|
||||
store.selectLayer(layerId)
|
||||
}
|
||||
trackMenu.value = {
|
||||
kind: 'layer',
|
||||
trackId: layerId,
|
||||
@@ -1959,6 +1974,16 @@ function toggleSeparated() {
|
||||
trackMenu.value = null
|
||||
}
|
||||
|
||||
function groupSelectedFromMenu() {
|
||||
store.groupSelectedLayers()
|
||||
trackMenu.value = null
|
||||
}
|
||||
|
||||
function convertSelectedToClipFromMenu() {
|
||||
store.convertSelectedLayersToClip()
|
||||
trackMenu.value = null
|
||||
}
|
||||
|
||||
function deleteTrack() {
|
||||
if (!trackMenu.value) return
|
||||
if (trackMenu.value.kind === 'layer') {
|
||||
@@ -2424,12 +2449,13 @@ onBeforeUnmount(() => {
|
||||
<span
|
||||
class="track-name"
|
||||
:class="{
|
||||
selected: track.layerId === store.selectedLayerId && track.isParent,
|
||||
selected:
|
||||
track.isParent && store.selectedLayerIds.includes(track.layerId),
|
||||
parent: track.isParent,
|
||||
sub: !track.isParent,
|
||||
}"
|
||||
:draggable="track.isParent && !isTrackEffectivelyLocked(track)"
|
||||
@click="!isTrackEffectivelyLocked(track) ? store.selectLayer(track.layerId) : null"
|
||||
@click="onLayerTrackNameClick($event, track)"
|
||||
@contextmenu="
|
||||
track.isParent && !isTrackEffectivelyLocked(track)
|
||||
? onTrackContextMenu($event, track.layerId)
|
||||
@@ -2999,6 +3025,22 @@ onBeforeUnmount(() => {
|
||||
class="kf-menu-trailing-check"
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
v-if="store.selectedLayerIds.length >= 2"
|
||||
class="kf-menu-item"
|
||||
@click="groupSelectedFromMenu"
|
||||
>
|
||||
<PhFolderPlus :size="12" class="kf-menu-icon" />
|
||||
{{ t('timeline.groupSelected', { n: store.selectedLayerIds.length }) }}
|
||||
</button>
|
||||
<button
|
||||
v-if="store.selectedLayerIds.length >= 1"
|
||||
class="kf-menu-item"
|
||||
@click="convertSelectedToClipFromMenu"
|
||||
>
|
||||
<PhFilmStrip :size="12" class="kf-menu-icon" />
|
||||
{{ t('timeline.convertToClip', { n: store.selectedLayerIds.length }) }}
|
||||
</button>
|
||||
</template>
|
||||
</template>
|
||||
<div class="kf-menu-divider"></div>
|
||||
|
||||
Reference in new issue
Block a user