resolveIncludes function
Resolves #include <path> against sources.
Depth-first, and a file already pulled in is skipped rather than repeated —
the headers include each other (material_maps → surface → color), so
without that the second inclusion redeclares every uniform and the compile
fails on a duplicate rather than on anything real.
A cycle is an error and not a silent stop: the include graph here is a tree by design, and a cycle means somebody changed that without noticing.
Implementation
String resolveIncludes(
String source,
GlslSources sources, {
required String from,
Set<String>? seen,
List<String>? stack,
}) {
final included = seen ?? <String>{};
final path = stack ?? <String>[from];
final out = StringBuffer();
for (final line in source.split('\n')) {
final match = _include.firstMatch(line);
if (match == null) {
out.writeln(line);
continue;
}
final target = match.group(1)!;
if (path.contains(target)) {
throw GlslTranslateException(
'include cycle: ${<String>[...path, target].join(' -> ')}',
);
}
if (!included.add(target)) continue;
final body = sources[target];
if (body == null) {
throw GlslTranslateException(
'$from includes "$target", which is not here',
);
}
// The included text is translated by the same rules, so a header's own
// includes and its version line are handled once rather than per includer.
out.writeln('// --- $target ---');
out.write(
resolveIncludes(
body,
sources,
from: target,
seen: included,
stack: <String>[...path, target],
),
);
}
return out.toString();
}