diff --git a/examples/assets/cup/new.jpg b/examples/assets/cup/new.jpg new file mode 100644 index 00000000..1cee5ee2 Binary files /dev/null and b/examples/assets/cup/new.jpg differ diff --git a/examples/assets/image/texture.jpg b/examples/assets/image/texture.jpg new file mode 100644 index 00000000..17260c64 Binary files /dev/null and b/examples/assets/image/texture.jpg differ diff --git a/examples/flutter/animations/lib/cup.dart b/examples/flutter/animations/lib/cup.dart new file mode 100644 index 00000000..6631ac55 --- /dev/null +++ b/examples/flutter/animations/lib/cup.dart @@ -0,0 +1,114 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:path/path.dart' as p; +import 'package:path_provider/path_provider.dart'; +import 'package:thermion_flutter/thermion_flutter.dart'; + +class Cup extends StatefulWidget { + const Cup({super.key}); + + @override + State createState() => _CupState(); +} + +class _CupState extends State { + bool _isLoading = true; + String? _modelPath, _modelUri; + String _statusMessage = "正在加载本地模型..."; + + @override + void initState() { + super.initState(); + _loadLocalModel(); + } + + // 从应用支持目录加载模型 + Future _loadLocalModel() async { + try { + setState(() { + _statusMessage = "获取本地模型路径..."; + }); + + final supportDir = await getApplicationSupportDirectory(); + final modelFile = File(p.join(supportDir.path, 'cup', 'model.gltf')); + + String modelPath = + File(p.join(supportDir.path, 'cup', 'model.gltf')).absolute.path; + final cupDir = Directory('${supportDir.path}/cup'); + final modelUri = File('${cupDir.path}/model.gltf').uri.toString(); + print('加载本地模modelPath: $modelPath'); + print('加载本地模modelUri: $modelUri'); + + if (await modelFile.exists()) { + setState(() { + _modelPath = modelPath; + _modelUri = modelUri; + + _isLoading = false; + _statusMessage = "本地模型加载完成"; + }); + } else { + setState(() { + _isLoading = false; + _statusMessage = "本地模型文件不存在"; + }); + } + } catch (e) { + print('加载本地模型失败: $e'); + setState(() { + _isLoading = false; + _statusMessage = "加载失败: $e"; + }); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('本地模型查看'), + ), + 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), + Text(_statusMessage), + const SizedBox(height: 16), + ElevatedButton( + onPressed: _loadLocalModel, + child: const Text('重试'), + ), + ], + ), + ) + : ViewerWidget( + // assetPath: "file://$_modelPath", + assetPath: _modelUri, + 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(), + ), + ), + ); + } +} diff --git a/examples/flutter/animations/lib/cup_page.dart b/examples/flutter/animations/lib/cup_page.dart index 5c1cbcbd..3165db00 100644 --- a/examples/flutter/animations/lib/cup_page.dart +++ b/examples/flutter/animations/lib/cup_page.dart @@ -4,6 +4,7 @@ import 'package:flutter/material.dart'; import 'package:path/path.dart' as p; import 'package:path_provider/path_provider.dart'; import 'package:thermion_flutter/thermion_flutter.dart'; +// import 'package:thermion_dart/src/viewer/src/thermion_viewer_base.dart'; class CupPage1 extends StatelessWidget { const CupPage1({super.key}); @@ -42,7 +43,7 @@ class CupPage extends StatefulWidget { class _CupPageState extends State { bool _isLoading = true; - String? _modelPath; + String? _modelPath, modelUri; String _statusMessage = "正在加载本地模型..."; @override @@ -61,9 +62,17 @@ class _CupPageState extends State { final supportDir = await getApplicationSupportDirectory(); final modelFile = File(p.join(supportDir.path, 'cup', 'model.gltf')); + String modelPath = + File(p.join(supportDir.path, 'cup', 'model.gltf')).absolute.path; + final cupDir = Directory('${supportDir.path}/cup'); + final modelUri = File('${cupDir.path}/model.gltf').uri; + print('加载本地模modelPath: $modelPath'); + print('加载本地模modelUri: $modelUri'); + if (await modelFile.exists()) { setState(() { - _modelPath = modelFile.path; + _modelPath = modelPath; + _isLoading = false; _statusMessage = "本地模型加载完成"; }); @@ -117,6 +126,7 @@ class _CupPageState extends State { ) : ViewerWidget( assetPath: "file://$_modelPath", + // assetPath: modelUri, skyboxPath: "assets/default_env_skybox.ktx", iblPath: "assets/default_env_ibl.ktx", transformToUnitCube: true, diff --git a/examples/flutter/animations/lib/main.dart b/examples/flutter/animations/lib/main.dart index 2967b79b..10c0ceba 100644 --- a/examples/flutter/animations/lib/main.dart +++ b/examples/flutter/animations/lib/main.dart @@ -1,10 +1,13 @@ +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 'model_viewer.dart'; +import 'cup.dart'; Future main() async { WidgetsFlutterBinding.ensureInitialized(); @@ -31,7 +34,7 @@ class MyApp extends StatelessWidget { primarySwatch: Colors.blue, visualDensity: VisualDensity.adaptivePlatformDensity, ), - home: const ModelViewerWithTextureReplace(), + home: ModelViewerWithTextureReplace(), ); } } @@ -52,11 +55,11 @@ class _ModelViewerWithTextureReplaceState int _keyValue = 0; String _statusMessage = "正在加载模型..."; bool _isReplacingTexture = false; + File? _selectedImage; @override void initState() { super.initState(); - BinaryManager.readLocalJson(); _load3DModels(); // _loadModel(); } @@ -117,69 +120,101 @@ class _ModelViewerWithTextureReplaceState }); try { - // 使用FilePicker选择文件 + // 选择图片文件 FilePickerResult? result = await FilePicker.platform.pickFiles( type: FileType.image, allowMultiple: false, ); - if (result != null && - result.files.isNotEmpty && - result.files.single.path != null) { - setState(() { - _statusMessage = "正在应用新纹理..."; - }); - - // 获取选中的文件 - PlatformFile file = result.files.first; - File newTexture = File(file.path!); - - // 检查文件是否存在 - if (await newTexture.exists()) { - // 先备份旧纹理文件(可选) - final backupTexturePath = '${_texturePath!}.backup'; - final oldTexture = File(_texturePath!); - if (await oldTexture.exists()) { - await oldTexture.copy(backupTexturePath); - } - - // 复制选中的图片到纹理路径 - await newTexture.copy(_texturePath!); - - // 给渲染器一点时间来处理文件变化 - await Future.delayed(const Duration(milliseconds: 3000)); - - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => LocalModelViewer(), - ), - ); - - // // 强制重建ViewerWidget以应用新纹理 - // setState(() { - // _keyValue++; - // _statusMessage = "纹理已更新"; - // }); - - // ScaffoldMessenger.of(context).showSnackBar( - // const SnackBar(content: Text('纹理已成功更新')), - // ); - } else { - setState(() { - _statusMessage = "文件不存在"; - }); - } - } else { + 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')), ); @@ -192,6 +227,15 @@ class _ModelViewerWithTextureReplaceState // 重置纹理到原始状态 Future _resetTexture() async { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => Cup(), + // builder: (context) => CupPage(), + ), + ); + return; + if (_isReplacingTexture || _texturePath == null) return; setState(() { @@ -285,6 +329,22 @@ class _ModelViewerWithTextureReplaceState ) : 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), @@ -317,21 +377,10 @@ class _ModelViewerWithTextureReplaceState ), ElevatedButton.icon( icon: const Icon(Icons.refresh), - label: const Text('重置纹理'), + label: const Text('3d查看'), onPressed: _isReplacingTexture ? null : _resetTexture, ), - ElevatedButton.icon( - icon: const Icon(Icons.camera), - label: const Text('重置视图'), - onPressed: _isReplacingTexture - ? null - : () { - setState(() { - _keyValue++; - }); - }, - ), ], ), const SizedBox(height: 8), diff --git a/examples/flutter/animations/lib/model_viewer.dart b/examples/flutter/animations/lib/model_viewer.dart index b1d6ccfa..93419576 100644 --- a/examples/flutter/animations/lib/model_viewer.dart +++ b/examples/flutter/animations/lib/model_viewer.dart @@ -1,6 +1,6 @@ +import 'dart:convert'; import 'dart:io'; -import 'package:animations/utils/binary_manager.dart'; import 'package:flutter/material.dart'; import 'package:path/path.dart' as p; import 'package:path_provider/path_provider.dart'; @@ -21,66 +21,84 @@ class _LocalModelViewerState extends State { @override void initState() { super.initState(); - - _initializeViewerAndLoadModel(); + ThermionFlutterPlugin.createViewer().then((viewer) async { + _viewer = viewer; + _loadLocalModel(); // 在 viewer 初始化之后再加载模型 + }); } - Future _initializeViewerAndLoadModel() async { + Future _loadLocalModel() async { try { - // 获取应用支持目录并构建模型文件路径 + setState(() { + _isLoading = true; + _statusMessage = "获取本地模型路径..."; + }); + final supportDir = await getApplicationSupportDirectory(); - final modelFile = File(p.join(supportDir.path, 'cup', 'model.gltf')); + final modelDir = p.join(supportDir.path, 'cup'); + final modelPath = p.join(modelDir, 'model.gltf'); + final modelFile = File(modelPath); - // 检查文件是否存在 - if (!await modelFile.exists()) { - await BinaryManager.migrate3DModels(); - await Future.delayed(const Duration(milliseconds: 3000)); + if (await modelFile.exists()) { + // 检查二进制文件 + final binPath = p.join(modelDir, 'model.bin'); + final binFile = File(binPath); + if (!await binFile.exists()) { + throw Exception("二进制文件不存在: $binPath"); + } - // throw Exception("模型文件不存在于路径: ${modelFile.path}"); + // 检查文件大小 + final binSize = await binFile.length(); + if (binSize == 0) { + throw Exception("二进制文件为空"); + } + + // 检查纹理文件 + final texturePath = p.join(modelDir, 'texture.jpg'); + final textureFile = File(texturePath); + if (!await textureFile.exists()) { + throw Exception("纹理文件不存在: $texturePath"); + } + + setState(() { + _statusMessage = "读取和修改模型数据..."; + }); + + // 读取GLTF文件内容 + String gltfContent = await modelFile.readAsString(); + + // 修改纹理路径(去掉"./"前缀) + gltfContent = gltfContent.replaceAll('"./texture.jpg"', '"new.jpg"'); + + // 转换为字节数组 + Uint8List gltfBytes = Uint8List.fromList(utf8.encode(gltfContent)); + + // 使用绝对路径作为资源URI + final resourceUri = "file://${Directory(modelDir).absolute.path}"; + + setState(() { + _statusMessage = "加载模型中..."; + }); + + final asset = await _viewer?.loadGltfFromBuffer( + gltfBytes, + resourceUri: resourceUri, + addToScene: true, + ); + + setState(() { + _asset = asset; + _isLoading = false; + _statusMessage = "模型加载完成"; + }); + } else { + setState(() { + _isLoading = false; + _statusMessage = "本地模型文件不存在"; + }); } - setState(() { - _statusMessage = "创建3D查看器..."; - }); - - // 创建ThermionViewer实例 - _viewer = await ThermionFlutterPlugin.createViewer(); - - setState(() { - _statusMessage = "获取模型路径..."; - }); - - setState(() { - _statusMessage = "加载环境光和天空盒..."; - }); - - // 加载环境光和天空盒(确保在pubspec.yaml中注册) - await _viewer!.loadSkybox("assets/default_env_skybox.ktx"); - await _viewer!.loadIbl("assets/default_env_ibl.ktx"); - - setState(() { - _statusMessage = "加载3D模型..."; - }); - - // 加载glTF模型 - 使用文件URI格式 - _asset = await _viewer!.loadGltf("file://${modelFile.path}"); - - // 可选:将模型缩放到单位立方体 - await _asset!.transformToUnitCube(); - - // 配置场景 - final camera = await _viewer!.getActiveCamera(); - await camera.lookAt(Vector3(0, 0, 6)); - - // 开始渲染 - await _viewer!.setRendering(true); - - setState(() { - _isLoading = false; - _hasError = false; - _statusMessage = "加载完成"; - }); } catch (e) { - print("加载模型失败: $e"); + print('加载本地模型失败: $e'); setState(() { _isLoading = false; _hasError = true; @@ -115,7 +133,7 @@ class _LocalModelViewerState extends State { Text(_statusMessage, style: TextStyle(color: Colors.red)), SizedBox(height: 16), ElevatedButton( - onPressed: _initializeViewerAndLoadModel, + onPressed: _loadLocalModel, child: Text("重试"), ), ], @@ -164,7 +182,7 @@ class _LocalModelViewerState extends State { @override void dispose() { - deleteModelFile(); + // deleteModelFile(); // 清理资源 _viewer?.dispose(); super.dispose(); diff --git a/examples/flutter/animations/lib/utils/binary_manager.dart b/examples/flutter/animations/lib/utils/binary_manager.dart index d9bc8f10..3c67b433 100644 --- a/examples/flutter/animations/lib/utils/binary_manager.dart +++ b/examples/flutter/animations/lib/utils/binary_manager.dart @@ -114,10 +114,19 @@ class BinaryManager { // 迁移所有3D模型文件 for (final modelFile in model3dFiles) { try { + // if (modelFile == 'model.gltf') { + // // 对于GLTF文件,我们需要修改内容 + // await _extractAndModifyGltfAsset( + // 'assets/cup/$modelFile', + // p.join(models3dDir.path, modelFile), + // ); + // } else { + // 对于其他文件,直接复制 await _extractAsset( 'assets/cup/$modelFile', p.join(models3dDir.path, modelFile), ); + // } } catch (e) { print('迁移3D模型文件失败 $modelFile: $e'); // 继续迁移其他模型文件 @@ -127,6 +136,30 @@ class BinaryManager { print('3D模型文件迁移完成'); } + static Future _extractAndModifyGltfAsset( + String assetPath, String targetPath) async { + try { + // 读取资源文件内容 + final byteData = await rootBundle.load(assetPath); + final buffer = byteData.buffer; + final gltfContent = utf8.decode( + buffer.asUint8List(byteData.offsetInBytes, byteData.lengthInBytes)); + + // 修改纹理路径(去掉"./"前缀) + final modifiedGltfContent = + gltfContent.replaceAll('"./texture.jpg"', '"texture.jpg"'); + + // 写入修改后的内容到目标文件 + final file = File(targetPath); + await file.writeAsBytes(utf8.encode(modifiedGltfContent)); + + print('已修改并迁移: $assetPath -> $targetPath'); + } catch (e) { + print('提取并修改GLTF资源失败: $e'); + rethrow; + } + } + static Future _migrateShellScript() async { print('开始迁移脚本文件...'); diff --git a/examples/flutter/animations/pubspec.yaml b/examples/flutter/animations/pubspec.yaml index 8d3c1e3f..9e3de92f 100644 --- a/examples/flutter/animations/pubspec.yaml +++ b/examples/flutter/animations/pubspec.yaml @@ -47,6 +47,7 @@ flutter: - assets/BusterDrone/ - assets/BusterDrone/textures/ - assets/cup/ + - assets/image/ - assets/bin/mac/ - assets/bin/windows/ - assets/models/