import 'dart:io'; import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; import 'package:path_provider/path_provider.dart'; import 'package:thermion_flutter/thermion_flutter.dart'; void main() { runApp(const MyApp()); } class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) { return MaterialApp( title: '3D模型纹理替换', theme: ThemeData( primarySwatch: Colors.blue, visualDensity: VisualDensity.adaptivePlatformDensity, ), home: const ModelViewerScreen(), ); } } class ModelViewerScreen extends StatefulWidget { const ModelViewerScreen({super.key}); @override State createState() => _ModelViewerScreenState(); } class _ModelViewerScreenState extends State { bool _isLoading = true; String? _modelPath; String? _texturePath; int _keyValue = 0; String _statusMessage = "正在加载模型..."; @override void initState() { super.initState(); _loadModel(); } // 加载模型文件到临时目录 Future _loadModel() async { try { setState(() { _statusMessage = "准备模型目录..."; }); final directory = await getApplicationDocumentsDirectory(); final modelDir = Directory('${directory.path}/model'); // 确保目录存在 if (!await modelDir.exists()) { await modelDir.create(recursive: true); } setState(() { _statusMessage = "加载GLTF文件..."; }); // 加载 .gltf 文件并写入本地 final gltfData = await DefaultAssetBundle.of(context).load('assets/cup/model.gltf'); final gltfFile = File('${modelDir.path}/model.gltf'); await gltfFile.writeAsBytes(gltfData.buffer.asUint8List()); /// ✅ 打印本地路径(关键) print('✅ GLTF 本地路径: ${gltfFile.path}'); print('✅ 是否存在: ${await gltfFile.exists()}'); setState(() { _statusMessage = "加载模型数据..."; }); // 加载 .bin 文件并写入本地 final binData = await DefaultAssetBundle.of(context).load('assets/cup/model.bin'); final binFile = File('${modelDir.path}/model.bin'); await binFile.writeAsBytes(binData.buffer.asUint8List()); setState(() { _statusMessage = "加载纹理..."; }); // 加载纹理文件并写入本地 final textureData = await DefaultAssetBundle.of(context).load('assets/cup/texture.jpg'); final textureFile = File('${modelDir.path}/texture.jpg'); await textureFile.writeAsBytes(textureData.buffer.asUint8List()); /// ✅ 打印纹理路径 print('✅ 纹理本地路径: ${textureFile.path}'); print('✅ 是否存在: ${await textureFile.exists()}'); setState(() { _modelPath = gltfFile.path; _texturePath = textureFile.path; _isLoading = false; _statusMessage = "加载完成"; }); } catch (e) { print('❌ 加载模型失败: $e'); setState(() { _isLoading = false; _statusMessage = "加载失败: $e"; }); } } // 使用FilePicker选择新纹理 - 修复配置错误 Future _pickNewTexture() async { try { setState(() { _statusMessage = "正在选择纹理文件..."; }); // 使用FilePicker选择文件 - 修复配置 // 选项1: 使用FileType.image(不能指定allowedExtensions) FilePickerResult? result = await FilePicker.platform.pickFiles( type: FileType.image, allowMultiple: false, ); // 选项2: 如果需要特定扩展名,使用FileType.custom /* FilePickerResult? result = await FilePicker.platform.pickFiles( type: FileType.custom, allowMultiple: false, allowedExtensions: ['jpg', 'jpeg', 'png', 'bmp', 'tga'], ); */ if (result != null && result.files.isNotEmpty && result.files.single.path != null && _texturePath != null) { setState(() { _statusMessage = "正在应用新纹理..."; }); // 获取选中的文件 PlatformFile file = result.files.first; File newTexture = File(file.path!); // 检查文件是否存在 if (await newTexture.exists()) { // 复制选中的图片到纹理路径 await newTexture.copy(_texturePath!); // 强制重建ViewerWidget以应用新纹理 setState(() { _keyValue++; _statusMessage = "纹理已更新"; }); ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('纹理已成功更新')), ); } else { setState(() { _statusMessage = "文件不存在"; }); } } else { setState(() { _statusMessage = "取消选择"; }); } } catch (e) { print('更换纹理失败: $e'); setState(() { _statusMessage = "更换纹理失败"; }); ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('更换纹理失败: $e')), ); } return; } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('3D模型纹理替换'), actions: [ IconButton( icon: const Icon(Icons.texture), onPressed: _pickNewTexture, tooltip: '更换纹理', ), ], ), body: _isLoading ? Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const CircularProgressIndicator(), const SizedBox(height: 16), Text(_statusMessage), ], ), ) : _modelPath == null ? Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const Icon(Icons.error, size: 48, color: Colors.red), const SizedBox(height: 16), const Text('加载模型失败'), const SizedBox(height: 16), ElevatedButton( onPressed: _loadModel, child: const Text('重试'), ), ], ), ) : Column( children: [ Expanded( child: ViewerWidget( key: ValueKey(_keyValue), assetPath: _modelPath!, skyboxPath: "assets/default_env_skybox.ktx", iblPath: "assets/default_env_ibl.ktx", transformToUnitCube: true, initialCameraPosition: Vector3(0, 0, 6), background: Colors.grey[200], manipulatorType: ManipulatorType.ORBIT, initial: const Center( child: CircularProgressIndicator(), ), ), ), Container( padding: const EdgeInsets.all(16), color: Colors.grey[100], child: Column( children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ ElevatedButton.icon( icon: const Icon(Icons.texture), label: const Text('选择纹理图片'), onPressed: _pickNewTexture, ), ElevatedButton.icon( icon: const Icon(Icons.refresh), label: const Text('重置视图'), onPressed: () { setState(() { _keyValue++; }); }, ), ], ), const SizedBox(height: 8), Text( _statusMessage, style: TextStyle( color: Colors.grey[600], fontStyle: FontStyle.italic, ), ), ], ), ), ], ), ); } }