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

+48
View File
@@ -0,0 +1,48 @@
import { reactive } from 'vue'
import type { ButtonState } from '@/types/scene'
// Runtime-only per-instance state. Not persisted; resets on page reload.
interface RuntimeState {
// Which state the button is currently showing. Undefined = 'normal'.
state: Map<string, ButtonState>
// Runtime disabled flag — overrides (and can be overridden by) the
// instance's persisted `disabled` field. Undefined = use persisted.
disabled: Map<string, boolean>
}
const runtime: RuntimeState = reactive({
state: new Map<string, ButtonState>(),
disabled: new Map<string, boolean>(),
})
export function useButtonRuntime() {
return {
state: runtime.state,
disabled: runtime.disabled,
getState(id: string, persistedDisabled?: boolean): ButtonState {
const override = runtime.disabled.get(id)
const effectiveDisabled =
override !== undefined ? override : !!persistedDisabled
if (effectiveDisabled) return 'disabled'
return runtime.state.get(id) ?? 'normal'
},
isDisabled(id: string, persistedDisabled?: boolean): boolean {
const override = runtime.disabled.get(id)
return override !== undefined ? override : !!persistedDisabled
},
setState(id: string, next: ButtonState) {
if (next === 'normal') runtime.state.delete(id)
else runtime.state.set(id, next)
},
setDisabled(id: string, next: boolean) {
runtime.disabled.set(id, next)
},
clearDisabled(id: string) {
runtime.disabled.delete(id)
},
reset() {
runtime.state.clear()
runtime.disabled.clear()
},
}
}