From 1f2812c64d50228d59a4f6b7509e163b24b1c697 Mon Sep 17 00:00:00 2001 From: th
+
{{ t('properties.lockedLayer') }}
{{ t('properties.noLayer') }}
@@ -971,4 +1033,13 @@ input { background: rgba(255, 122, 122, 0.18); border-color: var(--danger); } + +.multi-banner { + padding: 6px 8px; + font-size: 11px; + background: var(--accent-soft); + color: var(--accent); + border-radius: var(--radius-sm); + border: 1px solid var(--accent); +} diff --git a/src/components/Timeline.vue b/src/components/Timeline.vue index 09a9c49..92e84c7 100644 --- a/src/components/Timeline.vue +++ b/src/components/Timeline.vue @@ -617,36 +617,58 @@ function onLaneClick(e: MouseEvent) { } const canCreateKf = computed(() => { - const id = store.selectedLayerId - if (!id) return false - const layer = store.activeLayers.find((l) => l.id === id) - if (!layer) return false - if (layer.locked) return false - return !lockedGroupChildIds.value.has(id) + // Button is available as long as at least one selected layer is + // unlocked — the multi-select handler skips the locked ones per + // id, so a partially-locked selection still writes the usable + // frames. + const ids = store.selectedLayerIds.length + ? store.selectedLayerIds + : store.selectedLayerId + ? [store.selectedLayerId] + : [] + if (!ids.length) return false + for (const id of ids) { + const layer = store.activeLayers.find((l) => l.id === id) + if (!layer) continue + if (layer.locked) continue + if (lockedGroupChildIds.value.has(id)) continue + return true + } + return false }) function addKeyframeForSelected() { - const id = store.selectedLayerId - if (!id) return - const layer = store.activeLayers.find((l) => l.id === id) - if (!layer || isLayerIdLocked(id)) return + // Fan out across the whole multi-selection so the user can pin a + // frame on every grouped object at once. Each layer gets its own + // sampled snapshot so the keyframe matches the current pose. + // Falls back to the single-selection anchor when no multi is set. + const ids = store.selectedLayerIds.length + ? store.selectedLayerIds + : store.selectedLayerId + ? [store.selectedLayerId] + : [] + if (!ids.length) return // Snap the playhead itself to the nearest keyframe before adding — // the current playhead position is NOT a candidate (it's the value // we're rounding). const time = snapTime(store.playhead, { ignorePlayhead: true }) - const sampled = sampleLayer(layer, time) - const props: Partial