fix: masks inside buttons render across all paths

Buttons used to ignore `isMask` on every render path — the mask
shape drew as normal content instead of clipping the button's
other layers. Every entry point now routes button children through
`drawMaskedGroup`:

- `drawLayers` nested-button branch
- new `drawButtonFrame` (used by the mask-preview overlay in
  button-edit mode so the preview reflects the button's own
  stage instead of the main scene)
- `renderButtonThumbnail` so sidebar previews match the stage
- exported-runtime button branch mirrors the split; bumped
  `RUNTIME_VERSION` to '11' so cached players reload

Also:

- Timeline clip-track dropzone hides until the library has at
  least one clip, and the hint stays centred in the visible
  ruler area instead of drifting with horizontal scroll.
- Regression tests pin positive/negative button masks in
  `drawButtonFrame` and verify `drawSceneFrame` applies
  button-internal masks when the button sits on the main stage.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
thandClaude Opus 4.7 committed 2026-04-21 19:43:23 +02:00
1 parent c3764201b8
commit f29dc448fb
6 files changed
+232 -30

No files matched your search

+19 -4
View File
@@ -16,7 +16,7 @@ import { useSceneStore } from '@/stores/scene'
import { isLayerActiveAt, sampleLayer } from '@/lib/interpolate'
import { importImageFile, loadImage } from '@/lib/fileLoader'
import { useClipartStore } from '@/stores/clipart'
import { drawSceneFrame, loadSceneImages } from '@/lib/render'
import { drawButtonFrame, drawSceneFrame, loadSceneImages } from '@/lib/render'
import type {
Action,
AnimatableProps,
@@ -2697,9 +2697,24 @@ function drawPreview() {
ctx.save()
ctx.translate(viewX.value, viewY.value)
ctx.scale(zoom.value, zoom.value)
drawSceneFrame(ctx, store.scene, store.playhead, previewImages.value, {
crop: cropObjects.value,
})
// In button-edit mode the preview should reflect the *button*'s stage,
// not the main scene behind it. `store.playhead` is already pinned to
// BUTTON_STATE_TIMES[activeButtonState] (or to normal in hit mode), so
// we can pass it straight through.
if (store.activeButtonAsset) {
drawButtonFrame(
ctx,
store.activeButtonAsset,
store.scene,
store.playhead,
previewImages.value,
{ background: '#1a1a1f' },
)
} else {
drawSceneFrame(ctx, store.scene, store.playhead, previewImages.value, {
crop: cropObjects.value,
})
}
ctx.restore()
}