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

+21 -6
View File
@@ -27,6 +27,7 @@ import {
scriptFramesInRange,
type ActionRuntime,
} from '@/lib/actions'
import { useButtonRuntime } from '@/composables/useButtonRuntime'
const uid = () => Math.random().toString(36).slice(2, 10)
@@ -1215,10 +1216,18 @@ export const useSceneStore = defineStore('scene', {
buttonClipId,
clipTrackId,
startTime: Math.max(0, Math.min(this.scene.duration, startTime)),
x: this.scene.canvas.width / 2,
y: this.scene.canvas.height / 2,
}
;(this.scene.buttonInstances ||= []).push(inst)
return inst
},
setButtonInstancePosition(id: string, x: number, y: number) {
const target = this.scene.buttonInstances?.find((bi) => bi.id === id)
if (!target) return
target.x = x
target.y = y
},
removeButtonInstance(id: string) {
if (!this.scene.buttonInstances) return
this.scene.buttonInstances = this.scene.buttonInstances.filter((bi) => bi.id !== id)
@@ -1545,6 +1554,7 @@ export const useSceneStore = defineStore('scene', {
if (byId.has(ref)) return ref
return byLabel.get(ref) ?? null
}
const btnRt = useButtonRuntime()
return {
fps: self.scene.fps,
playbackIn: self.playbackIn,
@@ -1560,18 +1570,23 @@ export const useSceneStore = defineStore('scene', {
pause() {
self.pause()
},
setButtonState() {
// No-op in editor for now — runtime-state management lands in
// the export-runtime slice together with pointer handling.
},
setButtonDisabled(ref: string, disabled: boolean) {
setButtonState(ref, state) {
const id = resolve(ref)
if (!id) {
// eslint-disable-next-line no-console
console.warn('[animatr] unknown button ref:', ref)
return
}
self.setButtonInstanceDisabled(id, disabled)
btnRt.setState(id, state)
},
setButtonDisabled(ref, disabled) {
const id = resolve(ref)
if (!id) {
// eslint-disable-next-line no-console
console.warn('[animatr] unknown button ref:', ref)
return
}
btnRt.setDisabled(id, disabled)
},
warn(msg: string) {
// eslint-disable-next-line no-console