macro-clips slice 4d: button canvas runtime + pointer handling

- ButtonInstance gains optional x/y; new instances default to stage
  center and setButtonInstancePosition updates them
- useButtonRuntime composable holds per-instance state/disabled
  overrides (runtime-only, not persisted). getState layers the
  runtime disabled override on top of the persisted initial flag.
- store.actionRuntime's setButtonState/setButtonDisabled now drive
  useButtonRuntime instead of mutating the scene, so script-triggered
  state changes stay ephemeral
- CanvasStage renders each visible (playhead >= startTime) button
  instance inside a <v-group> at (bi.x, bi.y), drawing the current
  state's layer stack with draggable/listening disabled plus a
  transparent hit-rect sized to the clip's hitArea or the bbox of
  the first rect/circle layer. The group wires mouseenter/leave/
  down/up into the state machine and fires onClick via evalAction.
- exportPlayer runtime mirrors the same thing: getEffectiveButtonState,
  per-frame buttonHits array, canvas-level mousemove/down/up handlers
  do their own hit testing in scene space. RUNTIME_VERSION bumped to 5.
- tests cover useButtonRuntime defaults/overrides and the
  store.actionRuntime → runtime-state roundtrip via label resolution.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
thandClaude Opus 4.7 committed 2026-04-20 21:40:02 +02:00
1 parent 024883943b
commit f61f90d613
6 files changed
+412 -8

No files matched your search

+40
View File
@@ -1,5 +1,6 @@
import { createApp, h, nextTick, type Component, type Slot } from 'vue'
import { createBlankScene, useSceneStore } from '@/stores/scene'
import { useButtonRuntime } from '@/composables/useButtonRuntime'
import {
buildMarkerMap,
evalAction,
@@ -3521,6 +3522,45 @@ export function registerAllTests(): void {
})
})
suite('button runtime — state machine + action wiring', () => {
test('default state is normal; set/clear round-trip works', () => {
const rt = useButtonRuntime()
rt.reset()
expect(rt.getState('b1')).toBe('normal')
rt.setState('b1', 'pressed')
expect(rt.getState('b1')).toBe('pressed')
rt.setState('b1', 'normal') // normal uses delete internally
expect(rt.getState('b1')).toBe('normal')
})
test('persisted disabled surfaces as state=disabled until runtime overrides', () => {
const rt = useButtonRuntime()
rt.reset()
expect(rt.getState('b1', true)).toBe('disabled')
rt.setDisabled('b1', false) // runtime enables
expect(rt.getState('b1', true)).toBe('normal')
rt.setDisabled('b1', true)
expect(rt.getState('b1', false)).toBe('disabled')
})
test('store.actionRuntime resolves labels and drives useButtonRuntime', () => {
const s = makeFreshStore()
const rt = useButtonRuntime()
rt.reset()
const bc = s.addButtonClip('B')
const ct = s.addClipTrack()
const inst = s.addButtonInstance(bc.id, ct.id, 0)!
s.setButtonInstanceLabel(inst.id, 'play')
const ar = s.actionRuntime()
ar.setButtonState('play', 'pressed')
expect(rt.getState(inst.id)).toBe('pressed')
ar.setButtonDisabled('play', true)
expect(rt.getState(inst.id)).toBe('disabled')
ar.setButtonDisabled('play', false)
expect(rt.getState(inst.id, inst.disabled)).toBe('pressed')
})
})
suite('scene store — macro model normalization', () => {
test('loadScene initializes new arrays when missing', () => {
const s = makeFreshStore()