1.2.3: track-name shift/ctrl select, polygon level-1/level-2, cross-scope polygon ops

- Track-name click semantics in the Timeline:
  - plain → single-select that layer
  - shift → range-select every parent layer between anchor and click
    (locked + locked-group children are filtered out)
  - ctrl/cmd → toggle the clicked layer in/out of the selection
  Anchor moves to the just-clicked id like a file manager.
- Polygon / freeform selection now lands at "level 1" — the regular
  size + rotation transformer attaches like any other shape. A
  double-click promotes to level 2 (vertex handles + edge-insert
  dbl-click + tangent handles); selecting another layer drops back
  to level 1. Same UX inside the SVG sub-editor.
- New layersUsingAsset(project, assetId) helper walks every layer
  collection in the project (scenes, clips, button assets, svg
  assets). All six polygon-mutation actions (addPolygonPoint,
  removePolygonPoint, setPolygonPointCount, reversePolygonPoints,
  movePolygonVertex, insertPolygonPointOnEdge) now use it, so
  changes to the asset's points propagate to kf.props.points on
  layers in any scope. Fixes "no node added on dblclick of a
  freeform line inside an SVG asset".
- SvgLayerPanel buttons reset padding:0 — the global button reset
  was eating the 22×22 row-button area with box-sizing:border-box
  and clipping the Phosphor icons out of view.
This commit is contained in:
th committed 2026-04-25 23:39:19 +02:00
1 parent c17e2e8a9f
commit 127812d3bf
7 files changed
+581 -17

No files matched your search

+45 -8
View File
@@ -1757,10 +1757,10 @@ interface VertexHandle {
}
const polygonVertexHandles = computed<VertexHandle[]>(() => {
// Vertex handles only apply to a SOLO polygon selection — in a
// mixed multi-selection the shared transformer bounding box owns
// scale / rotate, and per-vertex handles would visually fight it.
if (store.selectedLayerIds.length > 1) return []
// Vertex handles only apply to a SOLO polygon selection in
// *vertex-edit mode* (entered via double-click). Level-1
// selection of a polygon shows the regular transformer instead.
if (!polygonInVertexEdit.value) return []
const id = store.selectedLayerId
if (!id) return []
const layer = store.activeLayers.find((l) => l.id === id)
@@ -2085,10 +2085,26 @@ const selectedPolygonLayer = computed(() => {
return { layer, asset }
})
/** Layer id of the polygon currently in vertex-edit mode. A
* polygon's first selection lands at "level 1" — the regular
* transformer (size + rotation) attaches like any other shape.
* Double-click promotes the polygon to "level 2" / vertex edit:
* per-vertex handles + tangent handles + edge-insert dbl-clicks
* light up. Cleared on selection change, scope change, or Escape. */
const polygonVertexEditId = ref<string | null>(null)
const polygonInVertexEdit = computed(() => {
if (store.selectedLayerIds.length > 1) return false
const id = store.selectedLayerId
if (!id) return false
return polygonVertexEditId.value === id
})
watch(
() => store.selectedLayerId,
() => {
selectedVertexIndices.value = new Set()
polygonVertexEditId.value = null
closeVertexMenu()
},
)
@@ -2313,8 +2329,10 @@ function attachTransformer(retriesLeft = 3) {
// Multi-select: wrap every selected layer node in one transformer
// so the bounding box + scale / rotate handles span the group.
// A SOLE polygon selection still bypasses the transformer because
// a single polygon is edited via vertex handles; mixed selections
// A SOLE polygon selection bypasses the transformer ONLY when
// the user has promoted it to vertex-edit mode (double-click);
// in level-1 / single-click selection the polygon gets the
// regular transformer like any other shape. Mixed selections
// that happen to include a polygon fold it into the shared bbox.
const ids = store.selectedLayerIds.length
? store.selectedLayerIds
@@ -2333,7 +2351,13 @@ function attachTransformer(retriesLeft = 3) {
const layer = store.activeLayers.find((l) => l.id === id)
if (!layer) continue
const asset = assetMap.value.get(layer.assetId)
if (asset?.kind === 'polygon' && single) continue
if (
asset?.kind === 'polygon' &&
single &&
polygonInVertexEdit.value
) {
continue
}
const node = stage.findOne<Konva.Node>(`#${id}`)
if (!node) {
anyMissing = true
@@ -2359,6 +2383,7 @@ watch(
store.selectedClipInstanceId,
stackItems.value,
store.playhead,
polygonVertexEditId.value,
] as const,
async () => {
await nextTick()
@@ -2700,7 +2725,9 @@ function handleStageMouseDown(e: Konva.KonvaEventObject<MouseEvent>) {
// selection — inside a multi-select the box always targets
// layers so new picks add to the existing group.
const mode: 'vertex' | 'layer' =
selectedPolygonLayer.value && store.selectedLayerIds.length <= 1
selectedPolygonLayer.value &&
store.selectedLayerIds.length <= 1 &&
polygonInVertexEdit.value
? 'vertex'
: 'layer'
boxSelect.value = {
@@ -2894,6 +2921,16 @@ function onContainerDblClick(e: MouseEvent) {
return
}
if (asset.kind !== 'polygon') return
// First dbl-click on a level-1-selected polygon → enter
// vertex-edit mode (level 2). The transformer detaches in the
// attachTransformer watcher, vertex handles light up. Subsequent
// dbl-clicks (already in vertex-edit mode) fall through to the
// edge-insert / open-polygon resume gestures below. SVG-edit
// scope skips this beat since polygons are always level-2 there.
if (!polygonInVertexEdit.value) {
polygonVertexEditId.value = layer.id
return
}
const sampled = sampleLayer(layer, store.playhead)
const pts = sampled.points ?? asset.points
if (pts.length < 1) return