1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
|
// timeline-format.js - Timeline V2 format I/O
// Handles parsing, serialization, validation for sequence v2 DAG format
export class TimelineFormat {
constructor() {
this.NODE_TYPES = ['u8x4_norm', 'f32x4', 'f16x8', 'depth24', 'compute_f32'];
this.NODE_COLORS = {
u8x4_norm: '#4a90e2',
f32x4: '#e24a90',
f16x8: '#e2904a',
depth24: '#4ae290',
compute_f32: '#904ae2'
};
}
parse(content, defaultBPM = 120) {
const sequences = [];
let currentSequence = null;
let currentPriority = 0;
let bpm = defaultBPM;
const parseTime = (timeStr) => {
if (timeStr.endsWith('s')) return parseFloat(timeStr.slice(0, -1)) * bpm / 60.0;
if (timeStr.endsWith('b')) return parseFloat(timeStr.slice(0, -1));
return parseFloat(timeStr);
};
const stripComment = (line) => {
const idx = line.indexOf('#');
return idx >= 0 ? line.slice(0, idx).trim() : line;
};
for (let line of content.split('\n')) {
line = line.trim();
// BPM header
if (line.startsWith('# BPM ')) {
const m = line.match(/# BPM (\d+)/);
if (m) bpm = parseInt(m[1]);
continue;
}
// Skip empty/comment lines
if (!line || line.startsWith('#')) continue;
line = stripComment(line);
if (!line) continue;
// SEQUENCE line
const seqMatch = line.match(/^SEQUENCE\s+(\S+)\s+(\d+)(?:\s+"([^"]+)")?$/);
if (seqMatch) {
currentSequence = {
type: 'sequence',
startTime: parseTime(seqMatch[1]),
priority: parseInt(seqMatch[2]),
name: seqMatch[3] || '',
nodes: [],
effects: [],
_collapsed: true
};
sequences.push(currentSequence);
currentPriority = -1;
continue;
}
if (!currentSequence) continue;
// NODE declaration
const nodeMatch = line.match(/^NODE\s+(\S+)\s+(\S+)$/);
if (nodeMatch) {
currentSequence.nodes.push({
name: nodeMatch[1],
type: nodeMatch[2].toLowerCase()
});
continue;
}
// EFFECT line with arrow syntax (v2 only)
const effectMatch = line.match(/^EFFECT\s+([+=-])\s+(\w+)\s+(.+)$/);
if (effectMatch) {
const modifier = effectMatch[1];
const className = effectMatch[2];
const remainder = effectMatch[3].split(/\s+/);
// Parse: input1 input2 -> output1 output2 start end [params]
const arrowIdx = remainder.indexOf('->');
if (arrowIdx === -1) {
console.warn(`Effect missing arrow syntax: ${line}`);
continue;
}
const inputs = remainder.slice(0, arrowIdx);
const afterArrow = remainder.slice(arrowIdx + 1);
// Find first numeric token (start time)
let firstTimeIdx = afterArrow.findIndex(s => !isNaN(parseFloat(s)));
if (firstTimeIdx === -1) {
console.warn(`Effect missing times: ${line}`);
continue;
}
const outputs = afterArrow.slice(0, firstTimeIdx);
const startTime = parseTime(afterArrow[firstTimeIdx]);
const endTime = parseTime(afterArrow[firstTimeIdx + 1]);
const params = afterArrow.slice(firstTimeIdx + 2).join(' ');
// Update priority
if (modifier === '+') currentPriority++;
else if (modifier === '-') currentPriority--;
currentSequence.effects.push({
type: 'effect',
className,
inputs,
outputs,
startTime,
endTime,
params,
priority: currentPriority,
priorityModifier: modifier
});
}
}
return { sequences, bpm };
}
serialize(sequences, bpm) {
let output = `# Demo Timeline\n# Generated by Timeline Editor\n# BPM ${bpm}\n\n`;
for (const seq of sequences) {
output += `SEQUENCE ${seq.startTime.toFixed(2)} ${seq.priority}`;
if (seq.name) output += ` "${seq.name}"`;
output += '\n';
// Write NODE declarations
if (seq.nodes && seq.nodes.length > 0) {
for (const node of seq.nodes) {
output += ` NODE ${node.name} ${node.type}\n`;
}
}
// Write EFFECT lines
for (const effect of seq.effects) {
const modifier = effect.priorityModifier || '+';
const inputs = (effect.inputs || ['source']).join(' ');
const outputs = (effect.outputs || ['sink']).join(' ');
const params = effect.params || '';
output += ` EFFECT ${modifier} ${effect.className} ${inputs} -> ${outputs} `;
output += `${effect.startTime.toFixed(2)} ${effect.endTime.toFixed(2)}`;
if (params) output += ` ${params}`;
output += '\n';
}
output += '\n';
}
return output;
}
validateSequence(sequence) {
// Only validate if explicit NODE declarations exist
if (!sequence.nodes || sequence.nodes.length === 0) {
return []; // No validation needed - nodes are auto-inferred
}
const declaredNodes = new Set(['source', 'sink']);
sequence.nodes.forEach(n => declaredNodes.add(n.name));
const errors = [];
sequence.effects.forEach((effect, idx) => {
effect.inputs?.forEach(inp => {
if (!declaredNodes.has(inp)) {
errors.push(`Effect ${idx} (${effect.className}): undeclared input "${inp}"`);
}
});
effect.outputs?.forEach(out => {
if (out !== 'sink' && !declaredNodes.has(out)) {
errors.push(`Effect ${idx} (${effect.className}): undeclared output "${out}"`);
}
});
});
return errors;
}
getNodeColor(type) {
return this.NODE_COLORS[type] || '#999';
}
renderNodesList(nodes) {
if (!nodes || nodes.length === 0) {
return '<div style="color: var(--text-secondary); font-style: italic;">No nodes declared (auto-inferred)</div>';
}
return nodes.map((node, idx) => {
const color = this.getNodeColor(node.type);
return `
<div style="display: flex; align-items: center; gap: 8px; padding: 4px; background: rgba(255,255,255,0.05); border-radius: 3px; margin-bottom: 4px;">
<span style="flex: 1; color: ${color};">
<strong>${node.name}</strong>: ${node.type}
</span>
<button onclick="deleteNode(${idx})" style="padding: 2px 8px; background: var(--accent-red);">×</button>
</div>
`;
}).join('');
}
renderBufferChain(effect) {
const inputs = effect.inputs || ['source'];
const outputs = effect.outputs || ['sink'];
return `${inputs.join(', ')} → [${effect.className}] → ${outputs.join(', ')}`;
}
}
|