Files
cup_edit/examples/flutter/animations/lib/main.dart
jingyun 1c07d576d3 init
2025-08-22 15:14:09 +08:00

522 lines
16 KiB
Dart

import 'dart:io';
import 'package:animations/utils/binary_manager.dart';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart';
import 'model_viewer.dart';
Future<void> 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: const ModelViewerWithTextureReplace(),
);
}
}
class ModelViewerWithTextureReplace extends StatefulWidget {
const ModelViewerWithTextureReplace({super.key});
@override
State<ModelViewerWithTextureReplace> createState() =>
_ModelViewerWithTextureReplaceState();
}
class _ModelViewerWithTextureReplaceState
extends State<ModelViewerWithTextureReplace> {
bool _isLoading = true;
String? _modelPath;
String? _texturePath;
int _keyValue = 0;
String _statusMessage = "正在加载模型...";
bool _isReplacingTexture = false;
@override
void initState() {
super.initState();
BinaryManager.readLocalJson();
_load3DModels();
// _loadModel();
}
// 加载3D模型文件
Future<void> _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<void> _pickNewTexture() async {
if (_isReplacingTexture || _texturePath == null) return;
setState(() {
_isReplacingTexture = true;
_statusMessage = "正在选择纹理文件...";
});
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 {
setState(() {
_statusMessage = "取消选择";
});
}
} catch (e) {
print('更换纹理失败: $e');
setState(() {
_statusMessage = "更换纹理失败: $e";
});
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('更换纹理失败: $e')),
);
} finally {
setState(() {
_isReplacingTexture = false;
});
}
}
// 重置纹理到原始状态
Future<void> _resetTexture() async {
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: [
// 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('重置纹理'),
onPressed:
_isReplacingTexture ? null : _resetTexture,
),
ElevatedButton.icon(
icon: const Icon(Icons.camera),
label: const Text('重置视图'),
onPressed: _isReplacingTexture
? null
: () {
setState(() {
_keyValue++;
});
},
),
],
),
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<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
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 = <String>[];
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<String?>(
value: _loaded,
items: [_droneUri, _cubeUri, null]
.map((uri) => DropdownMenuItem<String?>(
value: uri,
child: Text(
uri ?? "None",
style: const TextStyle(fontSize: 12),
)))
.toList(),
onChanged: _load),
const Text("Animation: "),
DropdownButton<String>(
value: selectedGltfAnimation == -1
? null
: gltfAnimations[selectedGltfAnimation],
items: gltfAnimations
.map((animation) => DropdownMenuItem<String>(
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))
]))),
]);
}
}
*/