buildScenes function

void buildScenes({
  1. required BuildInput buildInput,
  2. required BuildOutputBuilder buildOutput,
  3. List<String>? inputFilePaths,
  4. String outputDirectory = 'build/scenes/',
  5. String discoveryRoot = 'assets/',
  6. SceneAssetMode assetMode = SceneAssetMode.legacyOnly,
  7. bool compressTextures = false,
})

Registers scene assets so an app loads them by source path with loadScene without hand-editing the asset manifest. Discovers three source kinds under discoveryRoot: .glb (converted to .fsceneb), authored .fscene (compiled to .fsceneb, with referenced images embedded and prefab instances intact for runtime compose), and already-built .fsceneb (an editor's imported/ assets, registered as-is). Generated outputs go under outputDirectory (relative to BuildInput.packageRoot); a passthrough .fsceneb is registered in place.

Call this from a consuming app's hook/build.dart:

import 'package:hooks/hooks.dart';
import 'package:flutter_scene/build_hooks.dart';

void main(List<String> args) {
  build(args, (config, output) async {
    buildScenes(
      buildInput: config,
      buildOutput: output,
      assetMode: SceneAssetMode.dataAssetsIfAvailable,
    );
  });
}

When inputFilePaths is omitted, every .glb/.fscene/.fsceneb under discoveryRoot (default assets/, relative to the package root) is discovered; in a DataAssets mode each is registered as a DataAsset (key packages/<package>/flutter_scene/scene/<name>.fsceneb), and each source is declared as a build dependency so changing it retriggers the build (and hot reload). Conversion runs in-process (no subprocess, no native binary).

Implementation

void buildScenes({
  required BuildInput buildInput,
  required BuildOutputBuilder buildOutput,
  List<String>? inputFilePaths,
  String outputDirectory = 'build/scenes/',
  String discoveryRoot = 'assets/',
  SceneAssetMode assetMode = SceneAssetMode.legacyOnly,
  bool compressTextures = false,
}) {
  final dataAssetsAvailable = buildInput.config.buildDataAssets;
  if (assetMode == SceneAssetMode.dataAssetsRequired && !dataAssetsAvailable) {
    throw UnsupportedError(_dataAssetsUnavailableMessage);
  }
  final emitDataAssets =
      assetMode != SceneAssetMode.legacyOnly && dataAssetsAvailable;

  final packageRoot = buildInput.packageRoot;
  final inputs =
      inputFilePaths ??
      discoverSceneSources(packageRoot, discoveryRoot: discoveryRoot);
  if (inputs.isEmpty) {
    return;
  }

  final scenesRoot = packageRoot.resolve(outputDirectory);

  for (final inputFilePath in inputs) {
    final extension = _sceneSourceExtensions.firstWhere(
      inputFilePath.endsWith,
      orElse: () => throw Exception(
        'Scene source must be a .glb, .fscene, or .fsceneb file. Given: '
        '$inputFilePath',
      ),
    );
    if (inputFilePath.startsWith('../') || inputFilePath.contains('/../')) {
      throw Exception(
        'Scene source must be inside the package: $inputFilePath. Place it '
        'under the package (for example in assets/), using a symlink if needed.',
      );
    }

    final sourceUri = packageRoot.resolve(inputFilePath);

    // An already-built `.fsceneb` (an editor's imported asset) is registered
    // as-is, from its source location, with no conversion or output copy.
    if (extension == '.fsceneb') {
      buildOutput.dependencies.add(sourceUri);
      if (emitDataAssets) {
        buildOutput.assets.data.add(
          DataAsset(
            package: buildInput.packageName,
            name: sceneDataAssetName(inputFilePath),
            file: sourceUri,
          ),
        );
      }
      continue;
    }

    // `.glb` and `.fscene` produce a generated `.fsceneb` under the output dir.
    final relativeScenePath =
        '${inputFilePath.substring(0, inputFilePath.length - extension.length)}'
        '.fsceneb';
    final outputSceneUri = scenesRoot.resolve(relativeScenePath);
    Directory.fromUri(outputSceneUri.resolve('.')).createSync(recursive: true);

    // An authored `.fscene` references its imported images by path; read it up
    // front so those files can be embedded into the self-contained `.fsceneb`
    // and tracked as build dependencies (editing a referenced image then
    // retriggers conversion and hot reload). The document is reused for the
    // conversion below when the cache is stale.
    SceneDocument? fsceneDocument;
    List<ExternalImageAsset> imageAssets = const [];
    if (extension == '.fscene') {
      fsceneDocument = readFscene(
        File(sourceUri.toFilePath()).readAsStringSync(),
      );
      imageAssets = resolveExternalImageAssets(fsceneDocument, sourceUri);
    }

    // Skip the work when the source and settings are unchanged since the
    // output was produced, so a hook rerun for an unrelated edit does not
    // reconvert every scene. Set FLUTTER_SCENE_DISABLE_BUILD_CACHE to always
    // run.
    final sourceHash = contentHash(
      File(sourceUri.toFilePath()).readAsBytesSync(),
    );
    // Fold each referenced image's content hash into the stamp, so editing an
    // embedded image (not just the scene text) invalidates the cache and
    // rebuilds the dependent `.fsceneb`.
    final assetStamp =
        (imageAssets
                .map((a) => '${a.key}=${contentHash(a.file.readAsBytesSync())}')
                .toList()
              ..sort())
            .join(',');
    final stamp =
        'rev=$buildCacheRevision scene compress=$compressTextures '
        'kind=$extension src=$sourceHash assets=[$assetStamp]';
    final stampFile = File('${outputSceneUri.toFilePath()}.inputs');
    if (!isBuildCacheFresh(stampFile, stamp, [
      File(outputSceneUri.toFilePath()),
    ])) {
      if (extension == '.glb') {
        importGltfToFsceneb(
          inputFilePath,
          outputSceneUri.toFilePath(),
          workingDirectory: packageRoot.toFilePath(),
          compressTextures: compressTextures,
        );
      } else {
        // `.fscene` (authored text) -> `.fsceneb` (binary), embedding referenced
        // images so the container is self-contained and keeping prefab instances
        // intact for the runtime to compose.
        inlineExternalImageAssets(fsceneDocument!, imageAssets);
        File(
          outputSceneUri.toFilePath(),
        ).writeAsBytesSync(writeFsceneb(fsceneDocument));
      }
      stampFile.writeAsStringSync(stamp);
    }

    buildOutput.dependencies.add(sourceUri);
    // Declare each embedded image as a dependency every run (not only when the
    // cache is stale), so editing it retriggers the hook and the dependent
    // scene's hot reload.
    for (final asset in imageAssets) {
      buildOutput.dependencies.add(asset.file.uri);
    }

    if (emitDataAssets) {
      buildOutput.assets.data.add(
        DataAsset(
          package: buildInput.packageName,
          name: sceneDataAssetName(relativeScenePath),
          file: outputSceneUri,
        ),
      );
    }
  }
}