MIF_E31231623/android/wisata_app/lib/screens/ar_page.dart

335 lines
9.6 KiB
Dart

import 'dart:async';
import 'package:ar_flutter_plugin_updated/ar_flutter_plugin.dart';
import 'package:ar_flutter_plugin_updated/datatypes/config_planedetection.dart';
import 'package:ar_flutter_plugin_updated/datatypes/node_types.dart';
import 'package:ar_flutter_plugin_updated/managers/ar_anchor_manager.dart';
import 'package:ar_flutter_plugin_updated/managers/ar_location_manager.dart';
import 'package:ar_flutter_plugin_updated/managers/ar_object_manager.dart';
import 'package:ar_flutter_plugin_updated/managers/ar_session_manager.dart';
import 'package:ar_flutter_plugin_updated/models/ar_node.dart';
import 'package:flutter/material.dart';
import 'package:vector_math/vector_math_64.dart' as vector;
import '../services/ar_model_asset_service.dart';
class ARPage extends StatefulWidget {
const ARPage({
super.key,
required this.namaWisata,
required this.modelPath,
});
final String namaWisata;
final String modelPath;
@override
State<ARPage> createState() => _ARPageState();
}
class _ARPageState extends State<ARPage> {
final ArModelAssetService _modelAssetService = const ArModelAssetService();
ARSessionManager? _arSessionManager;
ARObjectManager? _arObjectManager;
ARNode? _modelNode;
bool _sedangMemuat = true;
bool _sedangMenambahkanModel = false;
bool _modelSudahTampil = false;
String? _pesanError;
double _skalaModel = 0.28;
double _rotasiY = 0;
vector.Vector3 _posisiModel = vector.Vector3(0, -0.18, -1.25);
double _skalaAwalGestur = 0.28;
double _rotasiAwalGestur = 0;
Offset? _titikGeserTerakhir;
@override
void dispose() {
final node = _modelNode;
final objectManager = _arObjectManager;
if (node != null && objectManager != null) {
unawaited(objectManager.removeNode(node));
}
_arSessionManager?.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.black,
body: Stack(
children: [
ARView(
onARViewCreated: _onARViewCreated,
planeDetectionConfig: PlaneDetectionConfig.horizontal,
permissionPromptDescription:
'Izin kamera diperlukan untuk menampilkan wisata dalam AR.',
permissionPromptButtonText: 'Izinkan Kamera',
),
Positioned.fill(
child: GestureDetector(
behavior: HitTestBehavior.translucent,
onScaleStart: _mulaiGestur,
onScaleUpdate: _ubahGestur,
),
),
_barAtas(),
if (_sedangMemuat) _indikatorMemuat(),
if (_pesanError != null) _panelError(),
],
),
);
}
void _onARViewCreated(
ARSessionManager sessionManager,
ARObjectManager objectManager,
ARAnchorManager anchorManager,
ARLocationManager locationManager,
) {
_arSessionManager = sessionManager;
_arObjectManager = objectManager;
_arSessionManager!.onInitialize(
showAnimatedGuide: false,
showFeaturePoints: false,
showPlanes: false,
showWorldOrigin: false,
handleTaps: false,
handlePans: false,
handleRotation: false,
);
_arObjectManager!.onInitialize();
_arSessionManager!.onError = _handleArError;
unawaited(_tampilkanModelDiDepanKamera());
}
Future<void> _tampilkanModelDiDepanKamera() async {
if (_sedangMenambahkanModel) return;
_sedangMenambahkanModel = true;
await Future<void>.delayed(const Duration(milliseconds: 1500));
if (!mounted || _modelSudahTampil || _arObjectManager == null) {
_sedangMenambahkanModel = false;
return;
}
try {
final localModelPath =
await _modelAssetService.prepareGlbForAr(widget.modelPath);
if (!mounted) return;
final node = ARNode(
type: NodeType.fileSystemAppFolderGLB,
uri: localModelPath,
position: _posisiModel,
scale: vector.Vector3.all(_skalaModel),
eulerAngles: vector.Vector3(0, _rotasiY, 0),
);
final berhasil = await _arObjectManager!.addNode(node) ?? false;
if (!mounted) return;
if (berhasil) {
setState(() {
_modelNode = node;
_modelSudahTampil = true;
_sedangMemuat = false;
});
} else {
setState(() {
_sedangMemuat = false;
_pesanError =
'Model 3D belum bisa dimuat. Pastikan jalur aset sudah benar: ${widget.modelPath}';
});
}
} on ArModelAssetException catch (exception) {
if (!mounted) return;
setState(() {
_sedangMemuat = false;
_pesanError = exception.message;
});
} catch (error) {
debugPrint('[AR Model] Gagal memuat ${widget.modelPath}: $error');
if (!mounted) return;
setState(() {
_sedangMemuat = false;
_pesanError =
'Model 3D belum bisa dimuat. Pastikan jalur aset sudah benar: ${widget.modelPath}';
});
} finally {
_sedangMenambahkanModel = false;
}
}
void _handleArError(String error) {
debugPrint('[AR Model] Native AR error: $error');
if (!mounted) return;
setState(() {
_sedangMemuat = false;
_pesanError = error;
});
}
void _mulaiGestur(ScaleStartDetails detail) {
_skalaAwalGestur = _skalaModel;
_rotasiAwalGestur = _rotasiY;
_titikGeserTerakhir = detail.focalPoint;
}
void _ubahGestur(ScaleUpdateDetails detail) {
final node = _modelNode;
if (node == null) return;
if (detail.pointerCount >= 2) {
_skalaModel =
(_skalaAwalGestur * detail.scale).clamp(0.08, 1.25).toDouble();
_rotasiY = _rotasiAwalGestur + detail.rotation;
node.scale = vector.Vector3.all(_skalaModel);
node.eulerAngles = vector.Vector3(0, _rotasiY, 0);
return;
}
final titikTerakhir = _titikGeserTerakhir;
if (titikTerakhir == null) {
_titikGeserTerakhir = detail.focalPoint;
return;
}
final delta = detail.focalPoint - titikTerakhir;
_titikGeserTerakhir = detail.focalPoint;
// Geser tipis di sumbu X dan Y agar objek bisa dipindahkan tanpa meloncat.
_posisiModel = vector.Vector3(
(_posisiModel.x + delta.dx * 0.0012).clamp(-1.2, 1.2).toDouble(),
(_posisiModel.y - delta.dy * 0.0012).clamp(-0.8, 0.8).toDouble(),
_posisiModel.z,
);
node.position = _posisiModel;
}
Widget _barAtas() {
return Positioned(
top: 0,
left: 0,
right: 0,
child: SafeArea(
child: Padding(
padding: const EdgeInsets.fromLTRB(12, 10, 12, 0),
child: Row(
children: [
IconButton.filledTonal(
onPressed: () => Navigator.pop(context),
icon: const Icon(Icons.arrow_back_rounded),
),
const SizedBox(width: 10),
Expanded(
child: Text(
widget.namaWisata,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
color: Colors.white,
fontSize: 18,
fontWeight: FontWeight.w800,
shadows: [
Shadow(
blurRadius: 8,
color: Colors.black87,
),
],
),
),
),
IconButton.filledTonal(
tooltip: 'Atur Ulang Model',
onPressed: _resetModel,
icon: const Icon(Icons.refresh_rounded),
),
],
),
),
),
);
}
Widget _indikatorMemuat() {
return Positioned.fill(
child: IgnorePointer(
child: Center(
child: DecoratedBox(
decoration: BoxDecoration(
color: Colors.black.withValues(alpha: 0.62),
borderRadius: BorderRadius.circular(24),
border: Border.all(color: Colors.white.withValues(alpha: 0.18)),
),
child: const Padding(
padding: EdgeInsets.symmetric(horizontal: 24, vertical: 20),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
CircularProgressIndicator(color: Colors.white),
SizedBox(height: 16),
Text(
'Memuat model AR...',
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.w800,
),
),
],
),
),
),
),
),
);
}
Widget _panelError() {
return Positioned(
left: 20,
right: 20,
bottom: 28,
child: SafeArea(
child: DecoratedBox(
decoration: BoxDecoration(
color: Colors.black.withValues(alpha: 0.80),
borderRadius: BorderRadius.circular(20),
border: Border.all(color: Colors.white.withValues(alpha: 0.16)),
),
child: Padding(
padding: const EdgeInsets.all(16),
child: Text(
_pesanError!,
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.w700,
),
),
),
),
),
);
}
void _resetModel() {
final node = _modelNode;
if (node == null) return;
setState(() {
_skalaModel = 0.28;
_rotasiY = 0;
_posisiModel = vector.Vector3(0, -0.18, -1.25);
node.position = _posisiModel;
node.scale = vector.Vector3.all(_skalaModel);
node.eulerAngles = vector.Vector3(0, _rotasiY, 0);
});
}
}