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:
1 parent
024883943b
commit
f61f90d613
6 files changed
+412
-8
No files matched your search
@@ -7,7 +7,19 @@ import { sampleLayer } from '@/lib/interpolate'
|
||||
import { importImageFile, loadImage } from '@/lib/fileLoader'
|
||||
import { useClipartStore } from '@/stores/clipart'
|
||||
import { drawSceneFrame, loadSceneImages } from '@/lib/render'
|
||||
import type { AnimatableProps, Asset, Keyframe, Layer, Point } from '@/types/scene'
|
||||
import type {
|
||||
Action,
|
||||
AnimatableProps,
|
||||
Asset,
|
||||
ButtonClip,
|
||||
ButtonInstance,
|
||||
ButtonState,
|
||||
Keyframe,
|
||||
Layer,
|
||||
Point,
|
||||
} from '@/types/scene'
|
||||
import { useButtonRuntime } from '@/composables/useButtonRuntime'
|
||||
import { evalAction } from '@/lib/actions'
|
||||
import { t } from '@/i18n'
|
||||
import { useKfSelection } from '@/composables/useKfSelection'
|
||||
import { showLocked } from '@/composables/useShowLocked'
|
||||
@@ -181,6 +193,134 @@ const canvasBorderConfig = computed(() => ({
|
||||
|
||||
const zoomPercent = computed(() => Math.round(zoom.value * 100))
|
||||
|
||||
const buttonRt = useButtonRuntime()
|
||||
|
||||
interface ButtonRenderItem {
|
||||
bi: ButtonInstance
|
||||
clip: ButtonClip
|
||||
state: ButtonState
|
||||
disabled: boolean
|
||||
layers: Array<{
|
||||
layer: Layer
|
||||
asset: Asset
|
||||
props: AnimatableProps
|
||||
}>
|
||||
hit: { width: number; height: number; offsetX: number; offsetY: number }
|
||||
}
|
||||
|
||||
const activeButtonInstances = computed<ButtonRenderItem[]>(() => {
|
||||
const out: ButtonRenderItem[] = []
|
||||
const instances = store.scene.buttonInstances ?? []
|
||||
const clips = store.scene.buttonClips ?? []
|
||||
const clipById = new Map(clips.map((c) => [c.id, c]))
|
||||
for (const bi of instances) {
|
||||
if (store.playhead < bi.startTime) continue
|
||||
const clip = clipById.get(bi.buttonClipId)
|
||||
if (!clip) continue
|
||||
const disabled = buttonRt.isDisabled(bi.id, bi.disabled)
|
||||
const state = buttonRt.getState(bi.id, bi.disabled)
|
||||
const layers = clip.states[state] ?? []
|
||||
const rendered: ButtonRenderItem['layers'] = []
|
||||
for (const layer of layers) {
|
||||
const asset = store.scene.assets.find((a) => a.id === layer.assetId)
|
||||
if (!asset) continue
|
||||
rendered.push({ layer, asset, props: sampleLayer(layer, 0) })
|
||||
}
|
||||
// Hit area: explicit hitArea on the clip, or fall back to the bbox of
|
||||
// the first rect-like asset in the normal state.
|
||||
let hit = clip.hitArea
|
||||
? {
|
||||
width: clip.hitArea.width,
|
||||
height: clip.hitArea.height,
|
||||
offsetX: clip.hitArea.offsetX ?? clip.hitArea.width / 2,
|
||||
offsetY: clip.hitArea.offsetY ?? clip.hitArea.height / 2,
|
||||
}
|
||||
: null
|
||||
if (!hit) {
|
||||
for (const l of clip.states.normal) {
|
||||
const a = store.scene.assets.find((x) => x.id === l.assetId)
|
||||
if (!a) continue
|
||||
if (a.kind === 'rect' || a.kind === 'image') {
|
||||
hit = {
|
||||
width: a.width,
|
||||
height: a.height,
|
||||
offsetX: l.offsetX ?? a.width / 2,
|
||||
offsetY: l.offsetY ?? a.height / 2,
|
||||
}
|
||||
break
|
||||
}
|
||||
if (a.kind === 'circle') {
|
||||
hit = {
|
||||
width: a.radius * 2,
|
||||
height: a.radius * 2,
|
||||
offsetX: (l.offsetX ?? 0) + a.radius,
|
||||
offsetY: (l.offsetY ?? 0) + a.radius,
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!hit) hit = { width: 120, height: 40, offsetX: 60, offsetY: 20 }
|
||||
out.push({ bi, clip, state, disabled, layers: rendered, hit })
|
||||
}
|
||||
return out
|
||||
})
|
||||
|
||||
function buttonGroupConfig(bi: ButtonInstance) {
|
||||
return {
|
||||
id: `btn:${bi.id}`,
|
||||
x: bi.x ?? 0,
|
||||
y: bi.y ?? 0,
|
||||
listening: true,
|
||||
}
|
||||
}
|
||||
|
||||
function buttonHitRectConfig(item: ButtonRenderItem) {
|
||||
return {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: item.hit.width,
|
||||
height: item.hit.height,
|
||||
offsetX: item.hit.offsetX,
|
||||
offsetY: item.hit.offsetY,
|
||||
fill: 'rgba(0,0,0,0.001)', // nearly transparent but hittable
|
||||
listening: !item.disabled,
|
||||
name: 'button-hit',
|
||||
}
|
||||
}
|
||||
|
||||
function buttonChildConfig<T extends Record<string, unknown>>(cfg: T): T {
|
||||
return { ...cfg, draggable: false, listening: false }
|
||||
}
|
||||
|
||||
function onButtonMouseEnter(bi: ButtonInstance) {
|
||||
const cur = buttonRt.getState(bi.id, bi.disabled)
|
||||
if (cur === 'normal') buttonRt.setState(bi.id, 'hover')
|
||||
}
|
||||
|
||||
function onButtonMouseLeave(bi: ButtonInstance) {
|
||||
const cur = buttonRt.getState(bi.id, bi.disabled)
|
||||
if (cur === 'hover' || cur === 'pressed') buttonRt.setState(bi.id, 'normal')
|
||||
}
|
||||
|
||||
function onButtonMouseDown(bi: ButtonInstance) {
|
||||
const cur = buttonRt.getState(bi.id, bi.disabled)
|
||||
if (cur === 'hover') buttonRt.setState(bi.id, 'pressed')
|
||||
}
|
||||
|
||||
function onButtonMouseUp(bi: ButtonInstance) {
|
||||
const cur = buttonRt.getState(bi.id, bi.disabled)
|
||||
if (cur !== 'pressed') return
|
||||
buttonRt.setState(bi.id, 'hover')
|
||||
const actions = bi.onClick
|
||||
if (actions && actions.length) runButtonActions(actions)
|
||||
}
|
||||
|
||||
function runButtonActions(actions: Action[]) {
|
||||
const rt = store.actionRuntime()
|
||||
for (const a of actions) evalAction(a, rt)
|
||||
}
|
||||
|
||||
const assetMap = computed(() => {
|
||||
const m = new Map<string, Asset>()
|
||||
for (const a of store.scene.assets) m.set(a.id, a)
|
||||
@@ -2346,6 +2486,42 @@ onBeforeUnmount(() => {
|
||||
/>
|
||||
</template>
|
||||
</template>
|
||||
<template v-for="item in activeButtonInstances" :key="`bi-${item.bi.id}`">
|
||||
<v-group
|
||||
:config="buttonGroupConfig(item.bi)"
|
||||
@mouseenter="onButtonMouseEnter(item.bi)"
|
||||
@mouseleave="onButtonMouseLeave(item.bi)"
|
||||
@mousedown="onButtonMouseDown(item.bi)"
|
||||
@mouseup="onButtonMouseUp(item.bi)"
|
||||
>
|
||||
<v-rect :config="buttonHitRectConfig(item)" />
|
||||
<template
|
||||
v-for="(entry, bli) in item.layers"
|
||||
:key="`${item.bi.id}-${entry.layer.id}-${bli}`"
|
||||
>
|
||||
<v-rect
|
||||
v-if="entry.asset.kind === 'rect'"
|
||||
:config="buttonChildConfig(rectConfig(entry.layer, entry.asset, entry.props))"
|
||||
/>
|
||||
<v-circle
|
||||
v-else-if="entry.asset.kind === 'circle'"
|
||||
:config="buttonChildConfig(circleConfig(entry.layer, entry.asset, entry.props))"
|
||||
/>
|
||||
<v-text
|
||||
v-else-if="entry.asset.kind === 'text'"
|
||||
:config="buttonChildConfig(textConfig(entry.layer, entry.asset, entry.props))"
|
||||
/>
|
||||
<v-line
|
||||
v-else-if="entry.asset.kind === 'polygon'"
|
||||
:config="buttonChildConfig(polygonConfig(entry.layer, entry.asset, entry.props))"
|
||||
/>
|
||||
<v-image
|
||||
v-else-if="entry.asset.kind === 'image' && imageConfig(entry.layer, entry.asset, entry.props)"
|
||||
:config="buttonChildConfig(imageConfig(entry.layer, entry.asset, entry.props)!)"
|
||||
/>
|
||||
</template>
|
||||
</v-group>
|
||||
</template>
|
||||
</v-group>
|
||||
<v-rect :config="canvasBorderConfig" />
|
||||
<v-line v-if="rotationArmConfig" :config="rotationArmConfig" />
|
||||
|
||||
@@ -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()
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
+123
-1
@@ -1,6 +1,6 @@
|
||||
import type { Scene } from '@/types/scene'
|
||||
|
||||
const RUNTIME_VERSION = '4'
|
||||
const RUNTIME_VERSION = '5'
|
||||
|
||||
const ENGINE_SOURCE = `(function(){
|
||||
if (window.AnimatrPlayer && window.AnimatrPlayer.__v === '${RUNTIME_VERSION}') return;
|
||||
@@ -212,6 +212,79 @@ const ENGINE_SOURCE = `(function(){
|
||||
for(var bri=0;bri<bis.length;bri++) if(bis[bri].id===ref) return ref;
|
||||
return runtime.buttonLabels[ref]||null;
|
||||
}
|
||||
var buttonHits=[]; // refreshed per draw()
|
||||
function getEffectiveButtonState(bi){
|
||||
var ovD=runtime.buttonDisabled[bi.id];
|
||||
var isDis=ovD===undefined?!!bi.disabled:ovD;
|
||||
if(isDis) return 'disabled';
|
||||
return runtime.buttonStates[bi.id]||'normal';
|
||||
}
|
||||
function setButtonState(id, st){
|
||||
if(st==='normal') delete runtime.buttonStates[id];
|
||||
else runtime.buttonStates[id]=st;
|
||||
}
|
||||
function pointerToSceneXY(e){
|
||||
var rc=canvas.getBoundingClientRect();
|
||||
var lw=scene.canvas.width, lh=scene.canvas.height;
|
||||
var cw=size.w, ch=size.h;
|
||||
if(!cw||!ch||!rc.width||!rc.height) return null;
|
||||
var sc=Math.min(cw/lw, ch/lh);
|
||||
var ox=(cw-lw*sc)/2, oy=(ch-lh*sc)/2;
|
||||
var cx=(e.clientX-rc.left)/rc.width*cw;
|
||||
var cy=(e.clientY-rc.top)/rc.height*ch;
|
||||
return { x:(cx-ox)/sc, y:(cy-oy)/sc };
|
||||
}
|
||||
function hitButtonAt(p){
|
||||
for(var i=buttonHits.length-1; i>=0; i--){
|
||||
var h=buttonHits[i];
|
||||
var lx=p.x-(h.bi.x||0), ly=p.y-(h.bi.y||0);
|
||||
if(lx>=h.hit.left && lx<=h.hit.right && ly>=h.hit.top && ly<=h.hit.bottom) return h;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
function onStageMove(e){
|
||||
var p=pointerToSceneXY(e);
|
||||
if(!p) return;
|
||||
var hit=hitButtonAt(p);
|
||||
for(var i=0;i<buttonHits.length;i++){
|
||||
var bh=buttonHits[i];
|
||||
var isDis=getEffectiveButtonState(bh.bi)==='disabled';
|
||||
if(isDis) continue;
|
||||
if(hit && hit.bi.id===bh.bi.id){
|
||||
var cur=getEffectiveButtonState(bh.bi);
|
||||
if(cur==='normal') setButtonState(bh.bi.id,'hover');
|
||||
} else {
|
||||
var cur2=getEffectiveButtonState(bh.bi);
|
||||
if(cur2==='hover'||cur2==='pressed') setButtonState(bh.bi.id,'normal');
|
||||
}
|
||||
}
|
||||
}
|
||||
function onStageDown(e){
|
||||
var p=pointerToSceneXY(e);
|
||||
if(!p) return;
|
||||
var hit=hitButtonAt(p);
|
||||
if(!hit) return;
|
||||
if(getEffectiveButtonState(hit.bi)==='disabled') return;
|
||||
setButtonState(hit.bi.id,'pressed');
|
||||
}
|
||||
function onStageUp(e){
|
||||
var p=pointerToSceneXY(e);
|
||||
if(!p) return;
|
||||
for(var i=0;i<buttonHits.length;i++){
|
||||
var bh=buttonHits[i];
|
||||
if(getEffectiveButtonState(bh.bi)!=='pressed') continue;
|
||||
var lx=p.x-(bh.bi.x||0), ly=p.y-(bh.bi.y||0);
|
||||
var inside=lx>=bh.hit.left&&lx<=bh.hit.right&&ly>=bh.hit.top&&ly<=bh.hit.bottom;
|
||||
setButtonState(bh.bi.id, inside?'hover':'normal');
|
||||
if(inside){
|
||||
var acts=bh.bi.onClick||[];
|
||||
for(var ai=0; ai<acts.length; ai++) evalAction(acts[ai]);
|
||||
}
|
||||
}
|
||||
}
|
||||
canvas.addEventListener('mousemove', onStageMove);
|
||||
canvas.addEventListener('mousedown', onStageDown);
|
||||
canvas.addEventListener('mouseup', onStageUp);
|
||||
var BUTTON_STATES=['normal','hover','pressed','disabled'];
|
||||
function parseScriptSource(source){
|
||||
var outActs=[];
|
||||
@@ -646,6 +719,55 @@ const ENGINE_SOURCE = `(function(){
|
||||
ctx.drawImage(topOff,0,0);
|
||||
ctx.restore();
|
||||
}
|
||||
// Draw button instances on top of everything else, and record their
|
||||
// scene-space hit rectangles for pointer handling.
|
||||
buttonHits.length=0;
|
||||
var bInsts=scene.buttonInstances||[];
|
||||
var bClips=scene.buttonClips||[];
|
||||
var bClipById={};
|
||||
for(var bcI=0;bcI<bClips.length;bcI++) bClipById[bClips[bcI].id]=bClips[bcI];
|
||||
for(var biI=0;biI<bInsts.length;biI++){
|
||||
var bi=bInsts[biI];
|
||||
if(time<bi.startTime) continue;
|
||||
var bclp=bClipById[bi.buttonClipId];
|
||||
if(!bclp) continue;
|
||||
var bst=getEffectiveButtonState(bi);
|
||||
var bStack=(bclp.states&&bclp.states[bst])||[];
|
||||
var bx=bi.x||0, by=bi.y||0;
|
||||
ctx.save();
|
||||
ctx.translate(bx, by);
|
||||
for(var blI=0; blI<bStack.length; blI++) drawLayer(bStack[blI], 0);
|
||||
ctx.restore();
|
||||
// Hit rect: explicit hitArea on clip, else bbox of first rect/circle
|
||||
// layer in the normal state.
|
||||
var hit=null;
|
||||
if(bclp.hitArea){
|
||||
var ox2=bclp.hitArea.offsetX==null?(bclp.hitArea.width/2):bclp.hitArea.offsetX;
|
||||
var oy2=bclp.hitArea.offsetY==null?(bclp.hitArea.height/2):bclp.hitArea.offsetY;
|
||||
hit={left:-ox2, top:-oy2, right:bclp.hitArea.width-ox2, bottom:bclp.hitArea.height-oy2};
|
||||
}
|
||||
if(!hit){
|
||||
var normalLs=(bclp.states&&bclp.states.normal)||[];
|
||||
for(var nli=0; nli<normalLs.length; nli++){
|
||||
var nl=normalLs[nli];
|
||||
var na=assetMap[nl.assetId];
|
||||
if(!na) continue;
|
||||
if(na.kind==='rect'||na.kind==='image'){
|
||||
var rox=nl.offsetX==null?na.width/2:nl.offsetX;
|
||||
var roy=nl.offsetY==null?na.height/2:nl.offsetY;
|
||||
hit={left:-rox, top:-roy, right:na.width-rox, bottom:na.height-roy};
|
||||
break;
|
||||
}
|
||||
if(na.kind==='circle'){
|
||||
var cox=nl.offsetX||0, coy=nl.offsetY||0;
|
||||
hit={left:cox-na.radius, top:coy-na.radius, right:cox+na.radius, bottom:coy+na.radius};
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if(!hit) hit={left:-60, top:-20, right:60, bottom:20};
|
||||
buttonHits.push({bi:bi, hit:hit});
|
||||
}
|
||||
ctx.restore();
|
||||
}
|
||||
loadImages(scene.assets).then(function(loaded){
|
||||
|
||||
+21
-6
@@ -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
|
||||
|
||||
@@ -198,6 +198,9 @@ export interface ButtonInstance {
|
||||
buttonClipId: string
|
||||
clipTrackId: string
|
||||
startTime: number
|
||||
/** Stage-space position of the button's origin. */
|
||||
x?: number
|
||||
y?: number
|
||||
/** Unique, user-facing handle used in dropdowns and script DSL. */
|
||||
label?: string
|
||||
onClick?: Action[]
|
||||
|
||||
Reference in new issue
Block a user