| // Copyright 2014 The Flutter Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| import 'dart:math' as math; |
| import 'dart:typed_data'; |
| |
| import 'package:meta/meta.dart'; |
| import 'package:pool/pool.dart'; |
| import 'package:process/process.dart'; |
| |
| import '../../artifacts.dart'; |
| import '../../base/error_handling_io.dart'; |
| import '../../base/file_system.dart'; |
| import '../../base/io.dart'; |
| import '../../base/logger.dart'; |
| import '../../convert.dart'; |
| import '../../devfs.dart'; |
| import '../build_system.dart'; |
| |
| /// A wrapper around [SceneImporter] to support hot reload of 3D models. |
| class DevelopmentSceneImporter { |
| DevelopmentSceneImporter({ |
| required SceneImporter sceneImporter, |
| required FileSystem fileSystem, |
| @visibleForTesting math.Random? random, |
| }) : _sceneImporter = sceneImporter, |
| _fileSystem = fileSystem, |
| _random = random ?? math.Random(); |
| |
| final SceneImporter _sceneImporter; |
| final FileSystem _fileSystem; |
| final Pool _compilationPool = Pool(4); |
| final math.Random _random; |
| |
| /// Recompile the input ipscene and return a devfs content that should be |
| /// synced to the attached device in its place. |
| Future<DevFSContent?> reimportScene(DevFSContent inputScene) async { |
| final File output = _fileSystem.systemTempDirectory.childFile('${_random.nextDouble()}.temp'); |
| late File inputFile; |
| bool cleanupInput = false; |
| Uint8List result; |
| PoolResource? resource; |
| try { |
| resource = await _compilationPool.request(); |
| if (inputScene is DevFSFileContent) { |
| inputFile = inputScene.file as File; |
| } else { |
| inputFile = _fileSystem.systemTempDirectory.childFile('${_random.nextDouble()}.temp'); |
| inputFile.writeAsBytesSync(await inputScene.contentsAsBytes()); |
| cleanupInput = true; |
| } |
| final bool success = await _sceneImporter.importScene( |
| input: inputFile, |
| outputPath: output.path, |
| fatal: false, |
| ); |
| if (!success) { |
| return null; |
| } |
| result = output.readAsBytesSync(); |
| } finally { |
| resource?.release(); |
| ErrorHandlingFileSystem.deleteIfExists(output); |
| if (cleanupInput) { |
| ErrorHandlingFileSystem.deleteIfExists(inputFile); |
| } |
| } |
| return DevFSByteContent(result); |
| } |
| } |
| |
| /// A class the wraps the functionality of the Impeller Scene importer scenec. |
| class SceneImporter { |
| SceneImporter({ |
| required ProcessManager processManager, |
| required Logger logger, |
| required FileSystem fileSystem, |
| required Artifacts artifacts, |
| }) : _processManager = processManager, |
| _logger = logger, |
| _fs = fileSystem, |
| _artifacts = artifacts; |
| |
| final ProcessManager _processManager; |
| final Logger _logger; |
| final FileSystem _fs; |
| final Artifacts _artifacts; |
| |
| /// The [Source] inputs that targets using this should depend on. |
| /// |
| /// See [Target.inputs]. |
| static const List<Source> inputs = <Source>[ |
| Source.pattern( |
| '{FLUTTER_ROOT}/packages/flutter_tools/lib/src/build_system/targets/scene_importer.dart'), |
| Source.hostArtifact(HostArtifact.scenec), |
| ]; |
| |
| /// Calls scenec, which transforms the [input] 3D model into an imported |
| /// ipscene at [outputPath]. |
| /// |
| /// All parameters are required. |
| /// |
| /// If the scene importer subprocess fails, it will print the stdout and |
| /// stderr to the log and throw a [SceneImporterException]. Otherwise, it |
| /// will return true. |
| Future<bool> importScene({ |
| required File input, |
| required String outputPath, |
| bool fatal = true, |
| }) async { |
| final File scenec = _fs.file( |
| _artifacts.getHostArtifact(HostArtifact.scenec), |
| ); |
| if (!scenec.existsSync()) { |
| throw SceneImporterException._( |
| 'The scenec utility is missing at "${scenec.path}". ' |
| 'Run "flutter doctor".', |
| ); |
| } |
| |
| final List<String> cmd = <String>[ |
| scenec.path, |
| '--input=${input.path}', |
| '--output=$outputPath', |
| ]; |
| _logger.printTrace('scenec command: $cmd'); |
| final Process scenecProcess = await _processManager.start(cmd); |
| final int code = await scenecProcess.exitCode; |
| if (code != 0) { |
| final String stdout = await utf8.decodeStream(scenecProcess.stdout); |
| final String stderr = await utf8.decodeStream(scenecProcess.stderr); |
| _logger.printTrace(stdout); |
| _logger.printError(stderr); |
| if (fatal) { |
| throw SceneImporterException._( |
| 'Scene import of "${input.path}" to "$outputPath" ' |
| 'failed with exit code $code.\n' |
| 'scenec stdout:\n$stdout\n' |
| 'scenec stderr:\n$stderr', |
| ); |
| } |
| return false; |
| } |
| return true; |
| } |
| } |
| |
| class SceneImporterException implements Exception { |
| SceneImporterException._(this.message); |
| |
| final String message; |
| |
| @override |
| String toString() => 'SceneImporterException: $message\n\n'; |
| } |