Files
cup_edit/examples/flutter/animations/lib/model_viewer.dart
T
2025-08-25 15:09:07 +08:00

191 lines
5.2 KiB
Dart

import 'dart:convert';
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 LocalModelViewer extends StatefulWidget {
@override
_LocalModelViewerState createState() => _LocalModelViewerState();
}
class _LocalModelViewerState extends State<LocalModelViewer> {
ThermionViewer? _viewer;
ThermionAsset? _asset;
bool _isLoading = true;
bool _hasError = false;
String _statusMessage = "初始化中...";
@override
void initState() {
super.initState();
ThermionFlutterPlugin.createViewer().then((viewer) async {
_viewer = viewer;
_loadLocalModel(); // 在 viewer 初始化之后再加载模型
});
}
Future<void> _loadLocalModel() async {
try {
setState(() {
_isLoading = true;
_statusMessage = "获取本地模型路径...";
});
final supportDir = await getApplicationSupportDirectory();
final modelDir = p.join(supportDir.path, 'cup');
final modelPath = p.join(modelDir, 'model.gltf');
final modelFile = File(modelPath);
if (await modelFile.exists()) {
// 检查二进制文件
final binPath = p.join(modelDir, 'model.bin');
final binFile = File(binPath);
if (!await binFile.exists()) {
throw Exception("二进制文件不存在: $binPath");
}
// 检查文件大小
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 = "本地模型文件不存在";
});
}
} catch (e) {
print('加载本地模型失败: $e');
setState(() {
_isLoading = false;
_hasError = true;
_statusMessage = "加载失败: $e";
});
}
}
@override
Widget build(BuildContext context) {
if (_isLoading) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CircularProgressIndicator(),
SizedBox(height: 16),
Text(_statusMessage),
],
),
);
}
if (_hasError || _viewer == null) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.error_outline, color: Colors.red, size: 48),
SizedBox(height: 16),
Text("加载3D模型失败"),
Text(_statusMessage, style: TextStyle(color: Colors.red)),
SizedBox(height: 16),
ElevatedButton(
onPressed: _loadLocalModel,
child: Text("重试"),
),
],
),
);
}
return Scaffold(
appBar: AppBar(
title: const Text('本地模型查看'),
),
body: ThermionListenerWidget(
inputHandler: DelegateInputHandler.fixedOrbit(_viewer!),
child: ThermionWidget(
viewer: _viewer!,
showFpsCounter: true, // 可选:显示FPS计数器
),
),
);
// 使用ThermionWidget显示3D内容
return ThermionListenerWidget(
inputHandler: DelegateInputHandler.fixedOrbit(_viewer!),
child: ThermionWidget(
viewer: _viewer!,
showFpsCounter: true, // 可选:显示FPS计数器
),
);
}
Future<void> deleteModelFile() async {
// 获取应用支持目录
final supportDir = await getApplicationSupportDirectory();
// 构建 model.gltf 的完整路径
final modelFile = File(p.join(supportDir.path, 'cup', 'model.gltf'));
// 检查文件是否存在,然后删除
if (await modelFile.exists()) {
await modelFile.delete();
print('model.gltf 已删除');
} else {
print('model.gltf 文件不存在');
}
}
@override
void dispose() {
// deleteModelFile();
// 清理资源
_viewer?.dispose();
super.dispose();
}
}