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:
thandClaude Opus 4.7 committed 2026-04-20 21:20:15 +02:00
1 parent 94ebbac238
commit fb58cd18f7
5 files changed
+368 -10

No files matched your search

+88 -4
View File
@@ -1,6 +1,6 @@
import type { Scene } from '@/types/scene'
const RUNTIME_VERSION = '3'
const RUNTIME_VERSION = '4'
const ENGINE_SOURCE = `(function(){
if (window.AnimatrPlayer && window.AnimatrPlayer.__v === '${RUNTIME_VERSION}') return;
@@ -199,9 +199,74 @@ const ENGINE_SOURCE = `(function(){
forcePlay: false,
buttonStates: {},
buttonDisabled: {},
buttonLabels: {},
warn: function(m){ if(window.console)window.console.warn('[animatr]',m); }
};
(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){
switch(act.type){
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));
return;
}
case 'setButtonState': runtime.buttonStates[act.instanceId]=act.state; return;
case 'setButtonDisabled': runtime.buttonDisabled[act.instanceId]=act.disabled; return;
case 'script': runtime.warn('Script DSL not implemented'); return;
case 'setButtonState': {
var sid=resolveButtonRef(act.instanceId);
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){