init
This commit is contained in:
284
examples/flutter/animations/lib/main1.dart
Normal file
284
examples/flutter/animations/lib/main1.dart
Normal file
@@ -0,0 +1,284 @@
|
||||
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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user