import 'dart:convert'; import 'dart:io'; import 'package:animations/utils/binary_manager.dart'; import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; import 'package:path/path.dart' as p; import 'package:path_provider/path_provider.dart'; import 'cup.dart'; Future main() async { WidgetsFlutterBinding.ensureInitialized(); // 初始化3D模型资源 try { await BinaryManager.initializeAppResources(); print('3D模型资源初始化成功'); } catch (e) { print('无法初始化3D模型资源: $e'); } 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: ModelViewerWithTextureReplace(), ); } } class ModelViewerWithTextureReplace extends StatefulWidget { const ModelViewerWithTextureReplace({super.key}); @override State createState() => _ModelViewerWithTextureReplaceState(); } class _ModelViewerWithTextureReplaceState extends State { bool _isLoading = true; String? _modelPath; String? _texturePath; int _keyValue = 0; String _statusMessage = "正在加载模型..."; bool _isReplacingTexture = false; File? _selectedImage; @override void initState() { super.initState(); _load3DModels(); // _loadModel(); } // 加载3D模型文件 Future _load3DModels() async { try { setState(() { _statusMessage = "获取模型路径..."; }); // 使用BinaryManager获取3D模型文件路径 // final supportDir = await getApplicationSupportDirectory(); // final cupDir = Directory('${supportDir.path}/cup'); // final modelFile = File('${cupDir.path}/model.gltf'); final modelPath = await BinaryManager.get3DModelPath("model.gltf"); final texturePath = await BinaryManager.get3DModelPath("texture.jpg"); // print("路径: $modelPath"); // print("文件是否存在: ${await File(modelPath).exists()}"); // print("文件路径: ${await File(modelPath).uri}"); // // setState(() { // _modelPath = File(modelPath).uri.toString(); // 确保是本地路径 // }); // if (!await File(modelPath).exists()) { // print("❌ 模型文件不存在: $modelPath"); // } // if (!await File(texturePath).exists()) { // print("❌ 纹理文件不存在: $texturePath"); // } print('modelPath=${modelPath}\ntexturePath=${texturePath}'); setState(() { _modelPath = modelPath; _texturePath = texturePath; _isLoading = false; _statusMessage = "加载完成"; }); } catch (e) { print('加载3D模型失败: $e'); setState(() { _isLoading = false; _statusMessage = "加载失败: $e"; }); } } // 使用FilePicker选择新纹理 Future _pickNewTexture() async { if (_isReplacingTexture || _texturePath == null) return; setState(() { _isReplacingTexture = true; _statusMessage = "正在选择纹理文件..."; }); try { // 选择图片文件 FilePickerResult? result = await FilePicker.platform.pickFiles( type: FileType.image, allowMultiple: false, ); if (result == null || result.files.isEmpty || result.files.single.path == null) { setState(() { _statusMessage = "取消选择"; }); return; } _selectedImage = File(result.files.single.path!); final selectedFile = result.files.single; final fileExtension = p.extension(selectedFile.path!).toLowerCase(); // 可选:仅允许特定图片格式 if (!['.jpg', '.jpeg', '.png'].contains(fileExtension)) { setState(() { _statusMessage = "仅支持 JPG / PNG 格式纹理"; }); return; } final newTexture = File(selectedFile.path!); final supportDir = await getApplicationSupportDirectory(); final modelDir = p.join(supportDir.path, 'cup'); final modelPath = p.join(modelDir, 'model.gltf'); final modelFile = File(modelPath); if (!await newTexture.exists()) { setState(() { _statusMessage = "所选纹理文件不存在"; }); return; } // 复制新纹理为固定名 new.jpg 到模型目录 final newTexturePath = p.join(modelDir, 'new.jpg'); await newTexture.copy(newTexturePath); _texturePath = newTexturePath; // 修改 GLTF 文件 if (await modelFile.exists()) { String gltfContent = await modelFile.readAsString(); // ✅ 修复 GLTF 中非法负号格式(例如 "- 0.123" => "-0.123") gltfContent = gltfContent.replaceAllMapped( RegExp(r'-\s+(\d+(\.\d+)?)'), (match) => '-${match.group(1)}', ); final gltfJson = jsonDecode(gltfContent); // 安全修改 images[0].uri if (gltfJson['images'] != null && gltfJson['images'].isNotEmpty) { gltfJson['images'][0]['uri'] = './new.jpg'; } // 可选:修改 textures[0].name if (gltfJson['textures'] != null && gltfJson['textures'].isNotEmpty) { gltfJson['textures'][0]['name'] = 'new.jpg'; } // 写回更新后的内容 final updatedContent = const JsonEncoder.withIndent(' ').convert(gltfJson); await modelFile.writeAsString(updatedContent); print('GLTF文件已成功更新'); // 重建模型视图 setState(() { _keyValue++; _statusMessage = "纹理已更新为 new.jpg"; }); ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('纹理已成功更新为 new.jpg')), ); } else { print('GLTF文件不存在: $modelPath'); setState(() { _statusMessage = "GLTF文件不存在"; }); } } catch (e) { print('更换纹理失败: $e'); setState(() { _statusMessage = "更换纹理失败: $e"; }); ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('更换纹理失败: $e')), ); } finally { setState(() { _isReplacingTexture = false; }); } } // 重置纹理到原始状态 Future _resetTexture() async { Navigator.push( context, MaterialPageRoute( builder: (context) => Cup(), // builder: (context) => CupPage(), ), ); return; if (_isReplacingTexture || _texturePath == null) return; setState(() { _isReplacingTexture = true; _statusMessage = "正在重置纹理..."; }); try { // 检查备份文件是否存在 final backupTexturePath = '${_texturePath!}.backup'; final backupFile = File(backupTexturePath); if (await backupFile.exists()) { // 恢复备份文件 await backupFile.copy(_texturePath!); // 给渲染器一点时间来处理文件变化 await Future.delayed(const Duration(milliseconds: 500)); // 强制重建ViewerWidget以应用恢复的纹理 setState(() { _keyValue++; _statusMessage = "纹理已重置"; }); ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('纹理已重置到原始状态')), ); } else { setState(() { _statusMessage = "没有备份文件可恢复"; }); ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('没有备份文件可恢复')), ); } } catch (e) { print('重置纹理失败: $e'); setState(() { _statusMessage = "重置纹理失败: $e"; }); ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('重置纹理失败: $e')), ); } finally { setState(() { _isReplacingTexture = false; }); } } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('3D模型纹理替换'), actions: [ IconButton( icon: const Icon(Icons.texture), onPressed: _isReplacingTexture ? null : _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: _load3DModels, child: const Text('重试'), ), ], ), ) : Column( children: [ if (_selectedImage != null) Container( width: 300, height: 300, decoration: BoxDecoration( border: Border.all(color: Colors.blue, width: 2), borderRadius: BorderRadius.circular(12), ), child: ClipRRect( borderRadius: BorderRadius.circular(10), child: Image.file( _selectedImage!, fit: BoxFit.cover, ), ), ), // Expanded( // child: ViewerWidget( // key: ValueKey(_keyValue), // assetPath: "file://$_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: _isReplacingTexture ? null : _pickNewTexture, ), ElevatedButton.icon( icon: const Icon(Icons.refresh), label: const Text('3d查看'), onPressed: _isReplacingTexture ? null : _resetTexture, ), ], ), const SizedBox(height: 8), Text( _statusMessage, style: TextStyle( color: Colors.grey[600], fontStyle: FontStyle.italic, ), ), ], ), ), ], ), ); } } /* import 'dart:async'; import 'package:animations/cup_page.dart'; import 'package:logging/logging.dart'; import 'package:flutter/material.dart' hide View; import 'package:thermion_flutter/thermion_flutter.dart'; void main() { runApp(const MyApp()); Logger.root.onRecord.listen((record) { print(record); }); } class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) { return MaterialApp( title: 'Thermion Animation Demo', theme: ThemeData( colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple), useMaterial3: true, ), home: const CupPage(), ); } } class MyHomePage extends StatefulWidget { const MyHomePage({super.key, required this.title}); final String title; @override State createState() => _MyHomePageState(); } class _MyHomePageState extends State { late DelegateInputHandler _inputHandler; ThermionViewer? _thermionViewer; ThermionAsset? _asset; final _droneUri = "assets/cup/model.gltf"; final _cubeUri = "assets/cube_with_morph_targets.glb"; String? _loaded; final gltfAnimations = []; int selectedGltfAnimation = -1; Future _load(String? uri) async { if (_asset != null) { await _thermionViewer!.destroyAsset(_asset!); _asset = null; } _loaded = uri; if (uri != null) { _asset = await _thermionViewer!.loadGltf(uri); await _asset!.transformToUnitCube(); final animations = await _asset!.getGltfAnimationNames(); final durations = await Future.wait(List.generate( animations.length, (i) => _asset!.getGltfAnimationDuration(i))); final labels = animations .asMap() .map((index, animation) => MapEntry(index, "$animation (${durations[index]}s")) .values; gltfAnimations.clear(); gltfAnimations.addAll(labels); selectedGltfAnimation = 0; } setState(() {}); } Future _playGltfAnimation() async { if (selectedGltfAnimation == -1) { throw Exception(); } await _asset!.playGltfAnimation(selectedGltfAnimation); } Future _stopGltfAnimation() async { if (selectedGltfAnimation == -1) { throw Exception(); } await _asset!.stopGltfAnimation(selectedGltfAnimation); } @override void initState() { super.initState(); WidgetsBinding.instance.addPostFrameCallback((_) async { _thermionViewer = await ThermionFlutterPlugin.createViewer(); final camera = await _thermionViewer!.getActiveCamera(); await camera.lookAt(Vector3(0, 0, 10)); await _thermionViewer!.loadSkybox("assets/default_env_skybox.ktx"); await _thermionViewer!.loadIbl("assets/default_env_ibl.ktx"); await _thermionViewer!.setPostProcessing(true); await _thermionViewer!.setRendering(true); _inputHandler = DelegateInputHandler.fixedOrbit(_thermionViewer!); await _load(_droneUri); setState(() {}); }); } @override Widget build(BuildContext context) { if (_thermionViewer == null) { return Container(); } return Stack(children: [ Positioned.fill( child: ThermionListenerWidget( inputHandler: _inputHandler, child: ThermionWidget( viewer: _thermionViewer!, ))), Card( child: Padding( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5), child: Row(children: [ const Text("Asset: "), DropdownButton( value: _loaded, items: [_droneUri, _cubeUri, null] .map((uri) => DropdownMenuItem( value: uri, child: Text( uri ?? "None", style: const TextStyle(fontSize: 12), ))) .toList(), onChanged: _load), const Text("Animation: "), DropdownButton( value: selectedGltfAnimation == -1 ? null : gltfAnimations[selectedGltfAnimation], items: gltfAnimations .map((animation) => DropdownMenuItem( value: animation, child: Text( animation, style: const TextStyle(fontSize: 12), ))) .toList(), onChanged: (value) { if (value == null) { selectedGltfAnimation = -1; } else { selectedGltfAnimation = gltfAnimations.indexOf(value); } }), IconButton( onPressed: _playGltfAnimation, icon: const Icon(Icons.play_arrow)), IconButton( onPressed: _stopGltfAnimation, icon: const Icon(Icons.stop)) ]))), ]); } } */