1.1.3: multi-select on the stage — marquee, group transform, panel
- Click-and-drag on the empty stage starts a layer marquee with touch (not enclose) semantics — every unlocked layer whose bbox overlaps the drag rect joins the selection. Tiny clicks still deselect; shift adds to the existing selection on both clicks and marquees. Polygon-vertex lasso stays available when a single polygon is the active selection. - Konva transformer now wraps every selected layer in one shared bounding box (polygons fold in for mixed selections; vertex handles + the vertex lasso suppress to avoid fighting the box). rotateEnabled flips off on multi to skip the ambiguous shared- pivot rotation. Drag of any selected node mirrors the delta onto every sibling Konva node so the whole group travels together; transformend persists each layer's final pose, not just the anchor's. - liveLayerPose became a per-id map (liveLayerPoses) so the reactive shape configs don't snap non-focused siblings back to their sampled pre-transform pose mid-gesture. - Plain click on a member of an existing multi-selection no longer collapses to single-select before the drag begins. - Arrow-key nudge + Timeline + keyframe button + Properties panel all fan out across the multi-selection: every editable layer gets the value, kind-specific panel sections (text, polygon, fill, stroke, blur, shadow, mask, button) collapse to leave just Transform + Opacity, and the trash button labels with the selection count. - Click on empty stage with a polygon selected now deselects the layer too instead of leaving the polygon stuck in the panel. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
289fa770b0
commit
1f2812c64d
8 files changed
+1168
-157
No files matched your search
+44
-22
@@ -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<AnimatableProps> = {
|
||||
scaleX: sampled.scaleX,
|
||||
scaleY: sampled.scaleY,
|
||||
opacity: sampled.opacity,
|
||||
for (const id of ids) {
|
||||
const layer = store.activeLayers.find((l) => l.id === id)
|
||||
if (!layer || isLayerIdLocked(id)) continue
|
||||
const sampled = sampleLayer(layer, time)
|
||||
const props: Partial<AnimatableProps> = {
|
||||
scaleX: sampled.scaleX,
|
||||
scaleY: sampled.scaleY,
|
||||
opacity: sampled.opacity,
|
||||
}
|
||||
if (sampled.fill) props.fill = sampled.fill
|
||||
if (!layer.separated) {
|
||||
props.x = sampled.x
|
||||
props.y = sampled.y
|
||||
props.rotation = sampled.rotation
|
||||
}
|
||||
store.upsertKeyframe(id, time, props)
|
||||
}
|
||||
if (sampled.fill) props.fill = sampled.fill
|
||||
if (!layer.separated) {
|
||||
props.x = sampled.x
|
||||
props.y = sampled.y
|
||||
props.rotation = sampled.rotation
|
||||
}
|
||||
store.upsertKeyframe(id, time, props)
|
||||
}
|
||||
|
||||
function onLaneDblClick(e: MouseEvent, track: TimelineLayerTrack) {
|
||||
|
||||
Reference in new issue
Block a user