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 createState() => _ARPageState(); } class _ARPageState extends State { 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 _tampilkanModelDiDepanKamera() async { if (_sedangMenambahkanModel) return; _sedangMenambahkanModel = true; await Future.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); }); } }