macro-clips slice 8: script DSL parser + runtime label resolution
- src/lib/scriptDsl.ts: small line-based parser producing Action[] plus
a list of ParseError{line,message}. Handles: play/pause/rewind,
jump <sec> | #<frame> | @<marker>, state btn:<label> <state>,
enable/disable btn:<label>, // comments, skips blanks
- evalAction for type:'script' parses the source and recursively
dispatches sub-actions; errors surface via rt.warn
- ActionRuntime.setButtonState/setButtonDisabled are documented as
taking an instance id OR a label; the store's actionRuntime builds
a label→id map and resolves refs before applying
- exportPlayer template gets a matching parseScriptSource plus
resolveButtonRef; evalAction-in-engine mirrors the TS version
- RUNTIME_VERSION bumped to '4'
- tests: parser covers commands, comments, jump variants, state/enable
errors, unknown commands; two integration tests verify that script
actions dispatch via evalAction and that parse errors don't block
valid actions
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
94ebbac238
commit
fb58cd18f7
5 files changed
+368
-10
No files matched your search
@@ -6,6 +6,7 @@ import {
|
|||||||
scriptFramesInRange,
|
scriptFramesInRange,
|
||||||
type ActionRuntime,
|
type ActionRuntime,
|
||||||
} from '@/lib/actions'
|
} from '@/lib/actions'
|
||||||
|
import { parseScript } from '@/lib/scriptDsl'
|
||||||
import type { Action } from '@/types/scene'
|
import type { Action } from '@/types/scene'
|
||||||
import { sampleLayer, lerpColor } from '@/lib/interpolate'
|
import { sampleLayer, lerpColor } from '@/lib/interpolate'
|
||||||
import { snapToCandidates } from '@/lib/timelineSnap'
|
import { snapToCandidates } from '@/lib/timelineSnap'
|
||||||
@@ -3382,6 +3383,125 @@ export function registerAllTests(): void {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
suite('scriptDsl — parser', () => {
|
||||||
|
test('parses bare commands', () => {
|
||||||
|
const { actions, errors } = parseScript('play\npause\nrewind')
|
||||||
|
expect(errors).toHaveLength(0)
|
||||||
|
expect(actions.map((a) => a.type)).toEqual(['play', 'pause', 'rewindToLoop'])
|
||||||
|
})
|
||||||
|
|
||||||
|
test('skips blank lines and comments', () => {
|
||||||
|
const src = [
|
||||||
|
'// leading comment',
|
||||||
|
'',
|
||||||
|
'play // trailing comment',
|
||||||
|
' ',
|
||||||
|
'pause',
|
||||||
|
].join('\n')
|
||||||
|
const { actions, errors } = parseScript(src)
|
||||||
|
expect(errors).toHaveLength(0)
|
||||||
|
expect(actions.map((a) => a.type)).toEqual(['play', 'pause'])
|
||||||
|
})
|
||||||
|
|
||||||
|
test('jump accepts seconds, #frame, and @marker', () => {
|
||||||
|
const { actions, errors } = parseScript('jump 2.5\njump #60\njump @intro')
|
||||||
|
expect(errors).toHaveLength(0)
|
||||||
|
expect(actions[0]).toEqual({ type: 'jumpTo', time: 2.5 })
|
||||||
|
expect(actions[1]).toEqual({ type: 'jumpToFrame', frame: 60 })
|
||||||
|
expect(actions[2]).toEqual({ type: 'jumpToMarker', name: 'intro' })
|
||||||
|
})
|
||||||
|
|
||||||
|
test('jump reports invalid arguments', () => {
|
||||||
|
const { actions, errors } = parseScript('jump\njump abc\njump #xx\njump @')
|
||||||
|
expect(actions).toHaveLength(0)
|
||||||
|
expect(errors).toHaveLength(4)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('state requires btn:<label> and a valid state', () => {
|
||||||
|
const good = parseScript('state btn:play pressed')
|
||||||
|
expect(good.errors).toHaveLength(0)
|
||||||
|
expect(good.actions[0]).toEqual({
|
||||||
|
type: 'setButtonState',
|
||||||
|
instanceId: 'play',
|
||||||
|
state: 'pressed',
|
||||||
|
})
|
||||||
|
const bad1 = parseScript('state play pressed')
|
||||||
|
expect(bad1.errors).toHaveLength(1)
|
||||||
|
const bad2 = parseScript('state btn:play weird')
|
||||||
|
expect(bad2.errors).toHaveLength(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('enable / disable map to setButtonDisabled', () => {
|
||||||
|
const { actions } = parseScript('enable btn:play\ndisable btn:play')
|
||||||
|
expect(actions[0]).toEqual({
|
||||||
|
type: 'setButtonDisabled',
|
||||||
|
instanceId: 'play',
|
||||||
|
disabled: false,
|
||||||
|
})
|
||||||
|
expect(actions[1]).toEqual({
|
||||||
|
type: 'setButtonDisabled',
|
||||||
|
instanceId: 'play',
|
||||||
|
disabled: true,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('unknown commands report error, other lines still parse', () => {
|
||||||
|
const { actions, errors } = parseScript('play\nwat\npause')
|
||||||
|
expect(errors).toHaveLength(1)
|
||||||
|
expect(errors[0].line).toBe(2)
|
||||||
|
expect(actions.map((a) => a.type)).toEqual(['play', 'pause'])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
suite('actions — script type expands via evalAction', () => {
|
||||||
|
test('script source is parsed and each action dispatched in order', () => {
|
||||||
|
const calls: string[] = []
|
||||||
|
const rt: ActionRuntime = {
|
||||||
|
fps: 60,
|
||||||
|
playbackIn: 0,
|
||||||
|
playbackOut: 10,
|
||||||
|
duration: 10,
|
||||||
|
markers: { intro: 2 },
|
||||||
|
setPlayhead: (t) => calls.push(`seek:${t}`),
|
||||||
|
play: () => calls.push('play'),
|
||||||
|
pause: () => calls.push('pause'),
|
||||||
|
setButtonState: (id, s) => calls.push(`state:${id}=${s}`),
|
||||||
|
setButtonDisabled: (id, d) => calls.push(`disabled:${id}=${d}`),
|
||||||
|
warn: () => {},
|
||||||
|
}
|
||||||
|
evalAction(
|
||||||
|
{
|
||||||
|
type: 'script',
|
||||||
|
source: 'pause\njump @intro\nstate btn:play pressed',
|
||||||
|
},
|
||||||
|
rt,
|
||||||
|
)
|
||||||
|
expect(calls).toEqual(['pause', 'seek:2', 'state:play=pressed'])
|
||||||
|
})
|
||||||
|
|
||||||
|
test('script errors surface via rt.warn, valid actions still fire', () => {
|
||||||
|
const warnings: string[] = []
|
||||||
|
const fired: string[] = []
|
||||||
|
const rt: ActionRuntime = {
|
||||||
|
fps: 60,
|
||||||
|
playbackIn: 0,
|
||||||
|
playbackOut: 10,
|
||||||
|
duration: 10,
|
||||||
|
markers: {},
|
||||||
|
setPlayhead: () => {},
|
||||||
|
play: () => fired.push('play'),
|
||||||
|
pause: () => fired.push('pause'),
|
||||||
|
setButtonState: () => {},
|
||||||
|
setButtonDisabled: () => {},
|
||||||
|
warn: (m) => warnings.push(m),
|
||||||
|
}
|
||||||
|
evalAction({ type: 'script', source: 'play\nwat\npause' }, rt)
|
||||||
|
expect(fired).toEqual(['play', 'pause'])
|
||||||
|
expect(warnings).toHaveLength(1)
|
||||||
|
expect(warnings[0]).toContain('wat')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
suite('scene store — macro model normalization', () => {
|
suite('scene store — macro model normalization', () => {
|
||||||
test('loadScene initializes new arrays when missing', () => {
|
test('loadScene initializes new arrays when missing', () => {
|
||||||
const s = makeFreshStore()
|
const s = makeFreshStore()
|
||||||
|
|||||||
+16
-4
@@ -1,4 +1,5 @@
|
|||||||
import type { Action, ButtonState, Marker } from '@/types/scene'
|
import type { Action, ButtonState, Marker } from '@/types/scene'
|
||||||
|
import { parseScript } from './scriptDsl'
|
||||||
|
|
||||||
export interface ActionRuntime {
|
export interface ActionRuntime {
|
||||||
fps: number
|
fps: number
|
||||||
@@ -9,8 +10,12 @@ export interface ActionRuntime {
|
|||||||
setPlayhead(t: number): void
|
setPlayhead(t: number): void
|
||||||
play(): void
|
play(): void
|
||||||
pause(): void
|
pause(): void
|
||||||
setButtonState(instanceId: string, state: ButtonState): void
|
/**
|
||||||
setButtonDisabled(instanceId: string, disabled: boolean): void
|
* `instanceIdOrLabel` may be a real ButtonInstance id OR a user-facing
|
||||||
|
* label — the runtime resolves via the label map internally.
|
||||||
|
*/
|
||||||
|
setButtonState(instanceIdOrLabel: string, state: ButtonState): void
|
||||||
|
setButtonDisabled(instanceIdOrLabel: string, disabled: boolean): void
|
||||||
warn(msg: string): void
|
warn(msg: string): void
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,11 +61,18 @@ export function evalAction(action: Action, rt: ActionRuntime): void {
|
|||||||
case 'setButtonDisabled':
|
case 'setButtonDisabled':
|
||||||
rt.setButtonDisabled(action.instanceId, action.disabled)
|
rt.setButtonDisabled(action.instanceId, action.disabled)
|
||||||
return
|
return
|
||||||
case 'script':
|
case 'script': {
|
||||||
rt.warn('Script DSL is not implemented yet')
|
const parsed = parseScript(action.source)
|
||||||
|
if (parsed.errors.length) {
|
||||||
|
for (const e of parsed.errors) {
|
||||||
|
rt.warn(`Script line ${e.line}: ${e.message}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const sub of parsed.actions) evalAction(sub, rt)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function clamp(n: number, lo: number, hi: number): number {
|
function clamp(n: number, lo: number, hi: number): number {
|
||||||
return Math.max(lo, Math.min(hi, n))
|
return Math.max(lo, Math.min(hi, n))
|
||||||
|
|||||||
+88
-4
@@ -1,6 +1,6 @@
|
|||||||
import type { Scene } from '@/types/scene'
|
import type { Scene } from '@/types/scene'
|
||||||
|
|
||||||
const RUNTIME_VERSION = '3'
|
const RUNTIME_VERSION = '4'
|
||||||
|
|
||||||
const ENGINE_SOURCE = `(function(){
|
const ENGINE_SOURCE = `(function(){
|
||||||
if (window.AnimatrPlayer && window.AnimatrPlayer.__v === '${RUNTIME_VERSION}') return;
|
if (window.AnimatrPlayer && window.AnimatrPlayer.__v === '${RUNTIME_VERSION}') return;
|
||||||
@@ -199,9 +199,74 @@ const ENGINE_SOURCE = `(function(){
|
|||||||
forcePlay: false,
|
forcePlay: false,
|
||||||
buttonStates: {},
|
buttonStates: {},
|
||||||
buttonDisabled: {},
|
buttonDisabled: {},
|
||||||
|
buttonLabels: {},
|
||||||
warn: function(m){ if(window.console)window.console.warn('[animatr]',m); }
|
warn: function(m){ if(window.console)window.console.warn('[animatr]',m); }
|
||||||
};
|
};
|
||||||
(scene.markers||[]).forEach(function(mk){ runtime.markers[mk.name]=mk.time; });
|
(scene.markers||[]).forEach(function(mk){ runtime.markers[mk.name]=mk.time; });
|
||||||
|
(scene.buttonInstances||[]).forEach(function(bi){
|
||||||
|
if(bi.label) runtime.buttonLabels[bi.label]=bi.id;
|
||||||
|
});
|
||||||
|
function resolveButtonRef(ref){
|
||||||
|
// Try as real instance id first, then as label.
|
||||||
|
var bis=scene.buttonInstances||[];
|
||||||
|
for(var bri=0;bri<bis.length;bri++) if(bis[bri].id===ref) return ref;
|
||||||
|
return runtime.buttonLabels[ref]||null;
|
||||||
|
}
|
||||||
|
var BUTTON_STATES=['normal','hover','pressed','disabled'];
|
||||||
|
function parseScriptSource(source){
|
||||||
|
var outActs=[];
|
||||||
|
var outErrs=[];
|
||||||
|
var lines=String(source||'').split(/\\r?\\n/);
|
||||||
|
for(var pi=0;pi<lines.length;pi++){
|
||||||
|
var ln=lines[pi];
|
||||||
|
var ci=ln.indexOf('//');
|
||||||
|
if(ci>=0) ln=ln.slice(0,ci);
|
||||||
|
ln=ln.trim();
|
||||||
|
if(!ln) continue;
|
||||||
|
var toks=ln.split(/\\s+/).filter(function(x){return x;});
|
||||||
|
var cmd=(toks[0]||'').toLowerCase();
|
||||||
|
var parsed=null, err=null;
|
||||||
|
if(cmd==='play') parsed={type:'play'};
|
||||||
|
else if(cmd==='pause') parsed={type:'pause'};
|
||||||
|
else if(cmd==='rewind') parsed={type:'rewindToLoop'};
|
||||||
|
else if(cmd==='jump'){
|
||||||
|
if(toks.length<2) err='jump: missing argument';
|
||||||
|
else {
|
||||||
|
var a=toks[1];
|
||||||
|
if(a.charAt(0)==='#'){
|
||||||
|
var fr=Number(a.slice(1));
|
||||||
|
if(!isFinite(fr)) err='jump: invalid frame '+a;
|
||||||
|
else parsed={type:'jumpToFrame',frame:fr};
|
||||||
|
} else if(a.charAt(0)==='@'){
|
||||||
|
var nm=a.slice(1);
|
||||||
|
if(!nm) err='jump: missing marker name';
|
||||||
|
else parsed={type:'jumpToMarker',name:nm};
|
||||||
|
} else {
|
||||||
|
var tm=Number(a);
|
||||||
|
if(!isFinite(tm)) err='jump: invalid time '+a;
|
||||||
|
else parsed={type:'jumpTo',time:tm};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if(cmd==='state'){
|
||||||
|
if(toks.length<3) err='state: expected btn:<label> <state>';
|
||||||
|
else if(toks[1].indexOf('btn:')!==0) err='state: expected btn:<label>';
|
||||||
|
else {
|
||||||
|
var st=toks[2];
|
||||||
|
if(BUTTON_STATES.indexOf(st)<0) err='state: unknown state '+st;
|
||||||
|
else parsed={type:'setButtonState',instanceId:toks[1].slice(4),state:st};
|
||||||
|
}
|
||||||
|
} else if(cmd==='enable'||cmd==='disable'){
|
||||||
|
if(toks.length<2) err=cmd+': missing btn:<label>';
|
||||||
|
else if(toks[1].indexOf('btn:')!==0) err=cmd+': expected btn:<label>';
|
||||||
|
else parsed={type:'setButtonDisabled',instanceId:toks[1].slice(4),disabled:cmd==='disable'};
|
||||||
|
} else {
|
||||||
|
err='unknown command "'+cmd+'"';
|
||||||
|
}
|
||||||
|
if(err) outErrs.push({line:pi+1,message:err});
|
||||||
|
else if(parsed) outActs.push(parsed);
|
||||||
|
}
|
||||||
|
return {actions:outActs, errors:outErrs};
|
||||||
|
}
|
||||||
function evalAction(act){
|
function evalAction(act){
|
||||||
switch(act.type){
|
switch(act.type){
|
||||||
case 'play': runtime.forcePlay=true; runtime.forcePause=false; return;
|
case 'play': runtime.forcePlay=true; runtime.forcePause=false; return;
|
||||||
@@ -215,9 +280,28 @@ const ENGINE_SOURCE = `(function(){
|
|||||||
runtime.jumpTo=Math.max(0,Math.min(runtime.duration,mt));
|
runtime.jumpTo=Math.max(0,Math.min(runtime.duration,mt));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
case 'setButtonState': runtime.buttonStates[act.instanceId]=act.state; return;
|
case 'setButtonState': {
|
||||||
case 'setButtonDisabled': runtime.buttonDisabled[act.instanceId]=act.disabled; return;
|
var sid=resolveButtonRef(act.instanceId);
|
||||||
case 'script': runtime.warn('Script DSL not implemented'); return;
|
if(!sid){ runtime.warn('Unknown button ref: '+act.instanceId); return; }
|
||||||
|
runtime.buttonStates[sid]=act.state;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
case 'setButtonDisabled': {
|
||||||
|
var did=resolveButtonRef(act.instanceId);
|
||||||
|
if(!did){ runtime.warn('Unknown button ref: '+act.instanceId); return; }
|
||||||
|
runtime.buttonDisabled[did]=act.disabled;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
case 'script': {
|
||||||
|
var parsed=parseScriptSource(act.source);
|
||||||
|
if(parsed.errors.length){
|
||||||
|
for(var sei=0; sei<parsed.errors.length; sei++){
|
||||||
|
runtime.warn('Script line '+parsed.errors[sei].line+': '+parsed.errors[sei].message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for(var sai=0; sai<parsed.actions.length; sai++) evalAction(parsed.actions[sai]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function fireFrames(prev, next){
|
function fireFrames(prev, next){
|
||||||
|
|||||||
@@ -0,0 +1,128 @@
|
|||||||
|
import type { Action, ButtonState } from '@/types/scene'
|
||||||
|
|
||||||
|
export interface ParseError {
|
||||||
|
line: number
|
||||||
|
message: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ParseResult {
|
||||||
|
actions: Action[]
|
||||||
|
errors: ParseError[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const BUTTON_STATES = new Set<ButtonState>([
|
||||||
|
'normal',
|
||||||
|
'hover',
|
||||||
|
'pressed',
|
||||||
|
'disabled',
|
||||||
|
])
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse a small line-based DSL into Action[]. Comments start with `//`
|
||||||
|
* and each non-empty line is one command. Known commands:
|
||||||
|
*
|
||||||
|
* play | pause | rewind
|
||||||
|
* jump <seconds> (e.g. `jump 2.5`)
|
||||||
|
* jump #<frame> (e.g. `jump #135`)
|
||||||
|
* jump @<marker> (e.g. `jump @intro`)
|
||||||
|
* state btn:<label> <state> (state ∈ normal|hover|pressed|disabled)
|
||||||
|
* enable btn:<label>
|
||||||
|
* disable btn:<label>
|
||||||
|
*
|
||||||
|
* Button labels are left as-is in `instanceId`. The runtime resolves
|
||||||
|
* labels to real instance IDs at eval time.
|
||||||
|
*/
|
||||||
|
export function parseScript(source: string): ParseResult {
|
||||||
|
const actions: Action[] = []
|
||||||
|
const errors: ParseError[] = []
|
||||||
|
const lines = source.split(/\r?\n/)
|
||||||
|
for (let i = 0; i < lines.length; i++) {
|
||||||
|
const stripped = stripComment(lines[i]).trim()
|
||||||
|
if (!stripped) continue
|
||||||
|
const res = parseLine(stripped)
|
||||||
|
if (res.error) errors.push({ line: i + 1, message: res.error })
|
||||||
|
else if (res.action) actions.push(res.action)
|
||||||
|
}
|
||||||
|
return { actions, errors }
|
||||||
|
}
|
||||||
|
|
||||||
|
function stripComment(line: string): string {
|
||||||
|
const i = line.indexOf('//')
|
||||||
|
return i < 0 ? line : line.slice(0, i)
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseLine(line: string): { action?: Action; error?: string } {
|
||||||
|
const tokens = line.split(/\s+/).filter(Boolean)
|
||||||
|
if (!tokens.length) return {}
|
||||||
|
const cmd = tokens[0].toLowerCase()
|
||||||
|
switch (cmd) {
|
||||||
|
case 'play':
|
||||||
|
return ok({ type: 'play' })
|
||||||
|
case 'pause':
|
||||||
|
return ok({ type: 'pause' })
|
||||||
|
case 'rewind':
|
||||||
|
return ok({ type: 'rewindToLoop' })
|
||||||
|
case 'jump':
|
||||||
|
return parseJump(tokens)
|
||||||
|
case 'state':
|
||||||
|
return parseState(tokens)
|
||||||
|
case 'enable':
|
||||||
|
return parseEnable(tokens, false)
|
||||||
|
case 'disable':
|
||||||
|
return parseEnable(tokens, true)
|
||||||
|
default:
|
||||||
|
return { error: `unknown command "${cmd}"` }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function ok(action: Action): { action: Action } {
|
||||||
|
return { action }
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseJump(tokens: string[]): { action?: Action; error?: string } {
|
||||||
|
if (tokens.length < 2) return { error: 'jump: missing argument' }
|
||||||
|
const arg = tokens[1]
|
||||||
|
if (arg.startsWith('#')) {
|
||||||
|
const frame = Number(arg.slice(1))
|
||||||
|
if (!Number.isFinite(frame)) return { error: `jump: invalid frame "${arg}"` }
|
||||||
|
return ok({ type: 'jumpToFrame', frame })
|
||||||
|
}
|
||||||
|
if (arg.startsWith('@')) {
|
||||||
|
const name = arg.slice(1)
|
||||||
|
if (!name) return { error: 'jump: missing marker name after @' }
|
||||||
|
return ok({ type: 'jumpToMarker', name })
|
||||||
|
}
|
||||||
|
const time = Number(arg)
|
||||||
|
if (!Number.isFinite(time)) return { error: `jump: invalid time "${arg}"` }
|
||||||
|
return ok({ type: 'jumpTo', time })
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseButtonRef(token: string): string | null {
|
||||||
|
if (!token.startsWith('btn:')) return null
|
||||||
|
const label = token.slice(4)
|
||||||
|
return label || null
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseState(tokens: string[]): { action?: Action; error?: string } {
|
||||||
|
if (tokens.length < 3) return { error: 'state: expected `btn:<label> <state>`' }
|
||||||
|
const label = parseButtonRef(tokens[1])
|
||||||
|
if (!label) return { error: `state: expected button ref like btn:<label>, got "${tokens[1]}"` }
|
||||||
|
const state = tokens[2] as ButtonState
|
||||||
|
if (!BUTTON_STATES.has(state)) {
|
||||||
|
return { error: `state: unknown state "${state}"` }
|
||||||
|
}
|
||||||
|
return ok({ type: 'setButtonState', instanceId: label, state })
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseEnable(
|
||||||
|
tokens: string[],
|
||||||
|
disabled: boolean,
|
||||||
|
): { action?: Action; error?: string } {
|
||||||
|
const verb = disabled ? 'disable' : 'enable'
|
||||||
|
if (tokens.length < 2) return { error: `${verb}: missing button ref` }
|
||||||
|
const label = parseButtonRef(tokens[1])
|
||||||
|
if (!label) {
|
||||||
|
return { error: `${verb}: expected button ref like btn:<label>` }
|
||||||
|
}
|
||||||
|
return ok({ type: 'setButtonDisabled', instanceId: label, disabled })
|
||||||
|
}
|
||||||
+16
-2
@@ -1493,6 +1493,14 @@ export const useSceneStore = defineStore('scene', {
|
|||||||
},
|
},
|
||||||
actionRuntime(): ActionRuntime {
|
actionRuntime(): ActionRuntime {
|
||||||
const self = this
|
const self = this
|
||||||
|
const instances = self.scene.buttonInstances ?? []
|
||||||
|
const byId = new Set(instances.map((bi) => bi.id))
|
||||||
|
const byLabel = new Map<string, string>()
|
||||||
|
for (const bi of instances) if (bi.label) byLabel.set(bi.label, bi.id)
|
||||||
|
function resolve(ref: string): string | null {
|
||||||
|
if (byId.has(ref)) return ref
|
||||||
|
return byLabel.get(ref) ?? null
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
fps: self.scene.fps,
|
fps: self.scene.fps,
|
||||||
playbackIn: self.playbackIn,
|
playbackIn: self.playbackIn,
|
||||||
@@ -1512,8 +1520,14 @@ export const useSceneStore = defineStore('scene', {
|
|||||||
// No-op in editor for now — runtime-state management lands in
|
// No-op in editor for now — runtime-state management lands in
|
||||||
// the export-runtime slice together with pointer handling.
|
// the export-runtime slice together with pointer handling.
|
||||||
},
|
},
|
||||||
setButtonDisabled(instanceId: string, disabled: boolean) {
|
setButtonDisabled(ref: string, disabled: boolean) {
|
||||||
self.setButtonInstanceDisabled(instanceId, disabled)
|
const id = resolve(ref)
|
||||||
|
if (!id) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.warn('[animatr] unknown button ref:', ref)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.setButtonInstanceDisabled(id, disabled)
|
||||||
},
|
},
|
||||||
warn(msg: string) {
|
warn(msg: string) {
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
|
|||||||
Reference in new issue
Block a user