Files
jingyun 1c07d576d3 init
2025-08-22 15:14:09 +08:00

285 lines
8.8 KiB
Dart
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<ModelViewerScreen> createState() => _ModelViewerScreenState();
}
class _ModelViewerScreenState extends State<ModelViewerScreen> {
bool _isLoading = true;
String? _modelPath;
String? _texturePath;
int _keyValue = 0;
String _statusMessage = "正在加载模型...";
@override
void initState() {
super.initState();
_loadModel();
}
// 加载模型文件到临时目录
Future<void> _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<void> _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,
),
),
],
),
),
],
),
);
}
}