augen 0.7.1 copy "augen: ^0.7.1" to clipboard
augen: ^0.7.1 copied to clipboard

A Flutter plugin for building AR applications using ARCore on Android and RealityKit on iOS. Enables pure Dart AR development without native code.

example/lib/main.dart

import 'package:flutter/material.dart';
import 'package:augen/augen.dart' hide AnimationStatus;
import 'package:augen/augen.dart' as augen;
import 'dart:async';
import 'dart:math';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Augen AR Demo - Complete Features',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      home: const ARHomePage(),
    );
  }
}

class ARHomePage extends StatefulWidget {
  const ARHomePage({super.key});

  @override
  State<ARHomePage> createState() => _ARHomePageState();
}

class _ARHomePageState extends State<ARHomePage> with TickerProviderStateMixin {
  AugenController? _controller;
  bool _isInitialized = false;
  List<ARPlane> _detectedPlanes = [];
  List<ARImageTarget> _imageTargets = [];
  List<ARTrackedImage> _trackedImages = [];
  List<ARFace> _trackedFaces = [];
  List<ARCloudAnchor> _cloudAnchors = [];
  int _nodeCounter = 0;
  String _statusMessage = 'Initializing...';
  bool _imageTrackingEnabled = false;
  bool _faceTrackingEnabled = false;
  bool _cloudAnchorsSupported = false;
  String? _currentSessionId;
  int _currentTabIndex = 0;
  late TabController _tabController;

  // Animation controllers
  late AnimationController _animationController;
  late AnimationController _blendController;
  String? _currentAnimation;
  double _blendWeight = 0.5;

  // Stream subscriptions
  StreamSubscription<List<ARPlane>>? _planesSubscription;
  StreamSubscription<List<ARImageTarget>>? _imageTargetsSubscription;
  StreamSubscription<List<ARTrackedImage>>? _trackedImagesSubscription;
  StreamSubscription<List<ARFace>>? _facesSubscription;
  StreamSubscription<List<ARCloudAnchor>>? _cloudAnchorsSubscription;
  StreamSubscription<CloudAnchorStatus>? _cloudAnchorStatusSubscription;
  StreamSubscription<String>? _errorSubscription;
  StreamSubscription<augen.AnimationStatus>? _animationStatusSubscription;
  StreamSubscription<TransitionStatus>? _transitionStatusSubscription;
  StreamSubscription<StateMachineStatus>? _stateMachineStatusSubscription;

  @override
  void initState() {
    super.initState();
    _tabController = TabController(
      length: 4,
      vsync: this,
      initialIndex: _currentTabIndex,
    );
    _animationController = AnimationController(
      duration: const Duration(seconds: 2),
      vsync: this,
    );
    _blendController = AnimationController(
      duration: const Duration(seconds: 1),
      vsync: this,
    );
  }

  @override
  void dispose() {
    _planesSubscription?.cancel();
    _imageTargetsSubscription?.cancel();
    _trackedImagesSubscription?.cancel();
    _facesSubscription?.cancel();
    _cloudAnchorsSubscription?.cancel();
    _cloudAnchorStatusSubscription?.cancel();
    _errorSubscription?.cancel();
    _animationStatusSubscription?.cancel();
    _transitionStatusSubscription?.cancel();
    _stateMachineStatusSubscription?.cancel();
    _tabController.dispose();
    _animationController.dispose();
    _blendController.dispose();
    _controller?.dispose();
    super.dispose();
  }

  void _onARViewCreated(AugenController controller) {
    _controller = controller;
    _initializeAR();
  }

  Future<void> _initializeAR() async {
    if (_controller == null) return;

    try {
      // Check AR support
      final isSupported = await _controller!.isARSupported();
      setState(() {
        _statusMessage = isSupported
            ? 'AR Supported - Initializing...'
            : 'AR Not Supported on this device';
      });

      if (!isSupported) return;

      // Initialize AR session
      await _controller!.initialize(
        const ARSessionConfig(
          planeDetection: true,
          lightEstimation: true,
          depthData: false,
          autoFocus: true,
        ),
      );

      setState(() {
        _isInitialized = true;
        _statusMessage = 'AR Session Active - All features ready!';
      });

      // Set up all stream listeners
      _setupStreamListeners();

      // Add sample image targets
      await _addSampleImageTargets();

      // Check cloud anchor support
      await _checkCloudAnchorSupport();
    } catch (e) {
      setState(() {
        _statusMessage = 'Error: $e';
      });
    }
  }

  void _setupStreamListeners() {
    if (_controller == null) return;

    // Plane detection
    _planesSubscription = _controller!.planesStream.listen((planes) {
      if (!mounted) return;
      setState(() {
        _detectedPlanes = planes;
      });
    });

    // Image targets
    _imageTargetsSubscription = _controller!.imageTargetsStream.listen((
      targets,
    ) {
      if (!mounted) return;
      setState(() {
        _imageTargets = targets;
      });
    });

    // Tracked images
    _trackedImagesSubscription = _controller!.trackedImagesStream.listen((
      tracked,
    ) {
      if (!mounted) return;
      setState(() {
        _trackedImages = tracked;
      });

      // Automatically add content to newly tracked images
      for (final trackedImage in tracked) {
        if (trackedImage.isTracked && trackedImage.isReliable) {
          _addContentToTrackedImage(trackedImage);
        }
      }
    });

    // Tracked faces
    _facesSubscription = _controller!.facesStream.listen((faces) {
      if (!mounted) return;
      setState(() {
        _trackedFaces = faces;
      });
      for (final face in faces) {
        if (face.isTracked && face.isReliable) {
          _addContentToTrackedFace(face);
        }
      }
    });

    _cloudAnchorsSubscription = _controller!.cloudAnchorsStream.listen((
      anchors,
    ) {
      if (!mounted) return;
      setState(() {
        _cloudAnchors = anchors;
      });
    });

    _cloudAnchorStatusSubscription = _controller!.cloudAnchorStatusStream
        .listen((status) {
          if (!mounted) return;
          if (status.isComplete) {
            if (status.isSuccessful) {
              ScaffoldMessenger.of(context).showSnackBar(
                SnackBar(
                  content: Text('Cloud anchor ${status.cloudAnchorId} ready!'),
                ),
              );
            } else {
              ScaffoldMessenger.of(context).showSnackBar(
                SnackBar(
                  content: Text('Cloud anchor failed: ${status.errorMessage}'),
                ),
              );
            }
          }
        });

    // Error handling
    _errorSubscription = _controller!.errorStream.listen((error) {
      if (!mounted) return;
      ScaffoldMessenger.of(
        context,
      ).showSnackBar(SnackBar(content: Text('AR Error: $error')));
    });

    // Animation status
    _animationStatusSubscription = _controller!.animationStatusStream.listen((
      status,
    ) {
      if (!mounted) return;
      // Handle animation status updates
    });

    // Transition status
    _transitionStatusSubscription = _controller!.transitionStatusStream.listen((
      status,
    ) {
      if (!mounted) return;
      // Handle transition status updates
    });

    // State machine status
    _stateMachineStatusSubscription = _controller!.stateMachineStatusStream
        .listen((status) {
          if (!mounted) return;
          // Handle state machine status updates
        });
  }

  Future<void> _addSampleImageTargets() async {
    if (_controller == null) return;

    try {
      // Add sample image targets (using URLs for demonstration)
      final targets = [
        ARImageTarget(
          id: 'poster1',
          name: 'Movie Poster',
          imagePath: 'https://example.com/images/sample_poster.jpg',
          physicalSize: const ImageTargetSize(0.3, 0.4), // 30cm x 40cm
        ),
        ARImageTarget(
          id: 'business_card',
          name: 'Business Card',
          imagePath: 'https://example.com/images/sample_card.jpg',
          physicalSize: const ImageTargetSize(
            0.085,
            0.055,
          ), // Standard business card size
        ),
      ];

      for (final target in targets) {
        await _controller!.addImageTarget(target);
      }

      if (!mounted) return;
      ScaffoldMessenger.of(context).showSnackBar(
        const SnackBar(content: Text('Sample image targets added')),
      );
    } catch (e) {
      if (!mounted) return;
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text('Failed to add image targets: $e')),
      );
    }
  }

  Future<void> _addContentToTrackedImage(ARTrackedImage trackedImage) async {
    if (_controller == null) return;

    try {
      final nodeId = 'content_${trackedImage.id}';

      // Create a 3D model node
      final contentNode = ARNode.fromModel(
        id: nodeId,
        modelPath: 'https://example.com/models/character.glb',
        position: const Vector3(0, 0, 0.1), // 10cm above the image
        rotation: const Quaternion(0, 0, 0, 1),
        scale: const Vector3(0.1, 0.1, 0.1),
      );

      await _controller!.addNodeToTrackedImage(
        nodeId: nodeId,
        trackedImageId: trackedImage.id,
        node: contentNode,
      );

      if (!mounted) return;
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text('Content added to ${trackedImage.targetId}')),
      );
    } catch (e) {
      if (!mounted) return;
      ScaffoldMessenger.of(
        context,
      ).showSnackBar(SnackBar(content: Text('Failed to add content: $e')));
    }
  }

  Future<void> _addContentToTrackedFace(ARFace face) async {
    if (_controller == null) return;

    try {
      final nodeId = 'face_content_${face.id}';

      // Create a 3D model node for the face
      final contentNode = ARNode.fromModel(
        id: nodeId,
        modelPath: 'https://example.com/models/face_model.glb',
        position: const Vector3(0, 0, 0.1), // 10cm in front of the face
        rotation: const Quaternion(0, 0, 0, 1),
        scale: const Vector3(0.1, 0.1, 0.1),
      );

      await _controller!.addNodeToTrackedFace(
        nodeId: nodeId,
        faceId: face.id,
        node: contentNode,
      );

      if (!mounted) return;
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text('Content added to face ${face.id}')),
      );
    } catch (e) {
      if (!mounted) return;
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text('Failed to add content to face: $e')),
      );
    }
  }

  // Cloud Anchor Methods
  Future<void> _checkCloudAnchorSupport() async {
    if (_controller == null) return;

    try {
      _cloudAnchorsSupported = await _controller!.isCloudAnchorsSupported();

      if (!mounted) return;
      setState(() {});

      if (_cloudAnchorsSupported) {
        await _controller!.setCloudAnchorConfig(
          maxCloudAnchors: 10,
          timeout: const Duration(seconds: 30),
          enableSharing: true,
        );
      }
    } catch (e) {
      if (!mounted) return;
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text('Failed to check cloud anchor support: $e')),
      );
    }
  }

  Future<void> _createCloudAnchor() async {
    if (_controller == null || !_cloudAnchorsSupported) return;

    try {
      // Create a local anchor first
      // Create a local anchor at a specific position
      const anchorPosition = Vector3(0, 0, -1);
      final localAnchor = await _controller!.addAnchor(anchorPosition);

      if (localAnchor != null) {
        // Convert to cloud anchor
        final cloudAnchorId = await _controller!.createCloudAnchor(
          localAnchor.id,
        );

        if (!mounted) return;
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(content: Text('Creating cloud anchor: $cloudAnchorId')),
        );
      }
    } catch (e) {
      if (!mounted) return;
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text('Failed to create cloud anchor: $e')),
      );
    }
  }

  Future<void> _shareCloudAnchor() async {
    if (_controller == null || _cloudAnchors.isEmpty) return;

    try {
      final sessionId = await _controller!.shareCloudAnchor(
        _cloudAnchors.first.id,
      );

      if (!mounted) return;
      setState(() {
        _currentSessionId = sessionId;
      });

      ScaffoldMessenger.of(
        context,
      ).showSnackBar(SnackBar(content: Text('Session ID: $sessionId')));
    } catch (e) {
      if (!mounted) return;
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text('Failed to share cloud anchor: $e')),
      );
    }
  }

  Future<void> _joinCloudAnchorSession() async {
    if (_controller == null) return;

    // Show dialog to enter session ID
    final sessionId = await showDialog<String>(
      context: context,
      builder: (context) => AlertDialog(
        title: const Text('Join Cloud Anchor Session'),
        content: TextField(
          decoration: const InputDecoration(
            labelText: 'Session ID',
            hintText: 'Enter session ID to join',
          ),
        ),
        actions: [
          TextButton(
            onPressed: () => Navigator.pop(context),
            child: const Text('Cancel'),
          ),
          TextButton(
            onPressed: () {
              final textController = TextEditingController();
              Navigator.pop(context, textController.text);
            },
            child: const Text('Join'),
          ),
        ],
      ),
    );

    if (sessionId != null && sessionId.isNotEmpty) {
      try {
        await _controller!.joinCloudAnchorSession(sessionId);

        if (!mounted) return;
        setState(() {
          _currentSessionId = sessionId;
        });

        ScaffoldMessenger.of(
          context,
        ).showSnackBar(SnackBar(content: Text('Joined session: $sessionId')));
      } catch (e) {
        if (!mounted) return;
        ScaffoldMessenger.of(
          context,
        ).showSnackBar(SnackBar(content: Text('Failed to join session: $e')));
      }
    }
  }

  // Image Tracking Methods
  Future<void> _toggleImageTracking() async {
    if (_controller == null) return;

    try {
      _imageTrackingEnabled = !_imageTrackingEnabled;
      await _controller!.setImageTrackingEnabled(_imageTrackingEnabled);

      if (!mounted) return;
      setState(() {});

      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(
          content: Text(
            _imageTrackingEnabled
                ? 'Image tracking enabled'
                : 'Image tracking disabled',
          ),
        ),
      );
    } catch (e) {
      if (!mounted) return;
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text('Failed to toggle image tracking: $e')),
      );
    }
  }

  Future<void> _toggleFaceTracking() async {
    if (_controller == null) return;

    try {
      _faceTrackingEnabled = !_faceTrackingEnabled;
      await _controller!.setFaceTrackingEnabled(_faceTrackingEnabled);

      if (!mounted) return;
      setState(() {});

      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(
          content: Text(
            _faceTrackingEnabled
                ? 'Face tracking enabled'
                : 'Face tracking disabled',
          ),
        ),
      );
    } catch (e) {
      if (!mounted) return;
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text('Failed to toggle face tracking: $e')),
      );
    }
  }

  Future<void> _addCustomImageTarget() async {
    if (_controller == null) return;

    try {
      final target = ARImageTarget(
        id: 'custom_${DateTime.now().millisecondsSinceEpoch}',
        name: 'Custom Target',
        imagePath: 'https://example.com/images/custom_target.jpg',
        physicalSize: const ImageTargetSize(0.2, 0.2), // 20cm x 20cm
      );

      await _controller!.addImageTarget(target);

      if (!mounted) return;
      ScaffoldMessenger.of(context).showSnackBar(
        const SnackBar(content: Text('Custom image target added')),
      );
    } catch (e) {
      if (!mounted) return;
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text('Failed to add custom target: $e')),
      );
    }
  }

  // Animation Methods
  Future<void> _playAnimation(String animationName) async {
    if (_controller == null) return;

    try {
      await _controller!.playAnimation(
        nodeId: 'character_node',
        animationId: animationName,
      );
      _currentAnimation = animationName;

      if (!mounted) return;
      setState(() {});

      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text('Playing animation: $animationName')),
      );
    } catch (e) {
      if (!mounted) return;
      ScaffoldMessenger.of(
        context,
      ).showSnackBar(SnackBar(content: Text('Failed to play animation: $e')));
    }
  }

  Future<void> _blendAnimations() async {
    if (_controller == null) return;

    try {
      await _controller!.blendAnimations(
        nodeId: 'character_node',
        animationWeights: {'idle': 1.0 - _blendWeight, 'walk': _blendWeight},
      );

      if (!mounted) return;
      ScaffoldMessenger.of(context).showSnackBar(
        const SnackBar(content: Text('Animation blending applied')),
      );
    } catch (e) {
      if (!mounted) return;
      ScaffoldMessenger.of(
        context,
      ).showSnackBar(SnackBar(content: Text('Failed to blend animations: $e')));
    }
  }

  Future<void> _crossfadeAnimation() async {
    if (_controller == null) return;

    try {
      await _controller!.crossfadeToAnimation(
        nodeId: 'character_node',
        fromAnimationId: 'idle',
        toAnimationId: 'walk',
        duration: 1.0,
      );

      if (!mounted) return;
      ScaffoldMessenger.of(context).showSnackBar(
        const SnackBar(content: Text('Animation crossfade applied')),
      );
    } catch (e) {
      if (!mounted) return;
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text('Failed to crossfade animation: $e')),
      );
    }
  }

  Future<void> _addObjectAtScreenCenter() async {
    if (_controller == null || !_isInitialized) return;

    try {
      // Perform hit test at screen center
      final size = MediaQuery.of(context).size;
      final results = await _controller!.hitTest(
        size.width / 2,
        size.height / 2,
      );

      if (results.isEmpty) {
        if (!mounted) return;
        ScaffoldMessenger.of(context).showSnackBar(
          const SnackBar(
            content: Text(
              'No surface detected. Try moving your device around.',
            ),
          ),
        );
        return;
      }

      // Add a node at the hit position
      final hitResult = results.first;
      final nodeId = 'node_${_nodeCounter++}';

      await _controller!.addNode(
        ARNode(
          id: nodeId,
          type: _getRandomNodeType(),
          position: hitResult.position,
          rotation: hitResult.rotation,
          scale: const Vector3(1, 1, 1),
          properties: {'color': 'blue'},
        ),
      );

      if (!mounted) return;
      ScaffoldMessenger.of(
        context,
      ).showSnackBar(SnackBar(content: Text('Added object: $nodeId')));
    } catch (e) {
      if (!mounted) return;
      ScaffoldMessenger.of(
        context,
      ).showSnackBar(SnackBar(content: Text('Failed to add object: $e')));
    }
  }

  NodeType _getRandomNodeType() {
    final types = [NodeType.sphere, NodeType.cube, NodeType.cylinder];
    return types[Random().nextInt(types.length)];
  }

  Future<void> _addAnchor() async {
    if (_controller == null || !_isInitialized) return;

    try {
      final anchor = await _controller!.addAnchor(
        const Vector3(0, 0, -0.5), // 0.5 meters in front of camera
      );

      if (anchor != null) {
        if (!mounted) return;
        ScaffoldMessenger.of(
          context,
        ).showSnackBar(SnackBar(content: Text('Anchor added: ${anchor.id}')));
      }
    } catch (e) {
      if (!mounted) return;
      ScaffoldMessenger.of(
        context,
      ).showSnackBar(SnackBar(content: Text('Failed to add anchor: $e')));
    }
  }

  Future<void> _resetSession() async {
    if (_controller == null) return;

    try {
      await _controller!.reset();
      if (!mounted) return;
      setState(() {
        _nodeCounter = 0;
        _detectedPlanes.clear();
      });
      ScaffoldMessenger.of(
        context,
      ).showSnackBar(const SnackBar(content: Text('AR Session Reset')));
    } catch (e) {
      if (!mounted) return;
      ScaffoldMessenger.of(
        context,
      ).showSnackBar(SnackBar(content: Text('Failed to reset: $e')));
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        title: const Text('Augen AR Demo - Complete Features'),
        bottom: TabBar(
          controller: _tabController,
          onTap: (index) => setState(() => _currentTabIndex = index),
          tabs: const [
            Tab(icon: Icon(Icons.view_in_ar), text: 'AR View'),
            Tab(icon: Icon(Icons.image_search), text: 'Image Tracking'),
            Tab(icon: Icon(Icons.face), text: 'Face Tracking'),
            Tab(icon: Icon(Icons.cloud), text: 'Cloud Anchors'),
            Tab(icon: Icon(Icons.animation), text: 'Animations'),
            Tab(icon: Icon(Icons.dashboard), text: 'Demo'),
            Tab(icon: Icon(Icons.info), text: 'Status'),
          ],
        ),
      ),
      body: TabBarView(
        children: [
          _buildARView(),
          _buildImageTrackingView(),
          _buildFaceTrackingView(),
          _buildCloudAnchorView(),
          _buildAnimationView(),
          _buildDemoView(),
          _buildStatusView(),
        ],
      ),
    );
  }

  Widget _buildARView() {
    return Stack(
      children: [
        // AR View
        AugenView(
          onViewCreated: _onARViewCreated,
          config: const ARSessionConfig(
            planeDetection: true,
            lightEstimation: true,
            depthData: false,
            autoFocus: true,
          ),
        ),

        // Status overlay
        Positioned(
          top: 16,
          left: 16,
          right: 16,
          child: Container(
            padding: const EdgeInsets.all(12),
            decoration: BoxDecoration(
              color: Colors.black.withValues(alpha: 0.7),
              borderRadius: BorderRadius.circular(8),
            ),
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              mainAxisSize: MainAxisSize.min,
              children: [
                Text(
                  _statusMessage,
                  style: const TextStyle(color: Colors.white, fontSize: 14),
                ),
                if (_detectedPlanes.isNotEmpty) ...[
                  const SizedBox(height: 8),
                  Text(
                    'Detected planes: ${_detectedPlanes.length}',
                    style: const TextStyle(
                      color: Colors.greenAccent,
                      fontSize: 12,
                    ),
                  ),
                ],
                if (_nodeCounter > 0) ...[
                  const SizedBox(height: 4),
                  Text(
                    'Objects placed: $_nodeCounter',
                    style: const TextStyle(
                      color: Colors.blueAccent,
                      fontSize: 12,
                    ),
                  ),
                ],
                if (_imageTrackingEnabled) ...[
                  const SizedBox(height: 4),
                  Text(
                    'Image Tracking: ON',
                    style: const TextStyle(
                      color: Colors.orangeAccent,
                      fontSize: 12,
                    ),
                  ),
                ],
                if (_faceTrackingEnabled) ...[
                  const SizedBox(height: 4),
                  Text(
                    'Face Tracking: ON',
                    style: const TextStyle(
                      color: Colors.pinkAccent,
                      fontSize: 12,
                    ),
                  ),
                ],
              ],
            ),
          ),
        ),

        // Help text
        if (_isInitialized)
          const Positioned(
            bottom: 120,
            left: 16,
            right: 16,
            child: Center(
              child: Text(
                'Tap the + button to place an object',
                style: TextStyle(
                  color: Colors.white,
                  fontSize: 16,
                  fontWeight: FontWeight.bold,
                  shadows: [
                    Shadow(
                      offset: Offset(0, 1),
                      blurRadius: 3.0,
                      color: Colors.black,
                    ),
                  ],
                ),
              ),
            ),
          ),
      ],
    );
  }

  Widget _buildImageTrackingView() {
    return Padding(
      padding: const EdgeInsets.all(16),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Text(
            'Image Tracking',
            style: Theme.of(context).textTheme.headlineSmall,
          ),
          const SizedBox(height: 16),

          // Image tracking toggle
          Card(
            child: ListTile(
              leading: Icon(
                _imageTrackingEnabled ? Icons.visibility : Icons.visibility_off,
              ),
              title: const Text('Image Tracking'),
              subtitle: Text(_imageTrackingEnabled ? 'Enabled' : 'Disabled'),
              trailing: Switch(
                value: _imageTrackingEnabled,
                onChanged: (_) => _toggleImageTracking(),
              ),
            ),
          ),

          const SizedBox(height: 16),

          // Image targets section
          Text(
            'Image Targets (${_imageTargets.length})',
            style: Theme.of(context).textTheme.titleMedium,
          ),
          const SizedBox(height: 8),

          Expanded(
            child: ListView.builder(
              itemCount: _imageTargets.length,
              itemBuilder: (context, index) {
                final target = _imageTargets[index];
                return Card(
                  child: ListTile(
                    leading: const Icon(Icons.image),
                    title: Text(target.name),
                    subtitle: Text(
                      'Size: ${target.physicalSize.width}m x ${target.physicalSize.height}m',
                    ),
                    trailing: Icon(
                      target.isActive ? Icons.check_circle : Icons.cancel,
                      color: target.isActive ? Colors.green : Colors.red,
                    ),
                  ),
                );
              },
            ),
          ),

          // Tracked images section
          Text(
            'Tracked Images (${_trackedImages.length})',
            style: Theme.of(context).textTheme.titleMedium,
          ),
          const SizedBox(height: 8),

          Expanded(
            child: ListView.builder(
              itemCount: _trackedImages.length,
              itemBuilder: (context, index) {
                final tracked = _trackedImages[index];
                return Card(
                  child: ListTile(
                    leading: Icon(
                      tracked.isTracked
                          ? Icons.track_changes
                          : Icons.search_off,
                      color: tracked.isTracked ? Colors.green : Colors.orange,
                    ),
                    title: Text('Target: ${tracked.targetId}'),
                    subtitle: Text(
                      'State: ${tracked.trackingState.name}\n'
                      'Confidence: ${(tracked.confidence * 100).toStringAsFixed(1)}%',
                    ),
                    trailing: Icon(
                      tracked.isReliable ? Icons.verified : Icons.warning,
                      color: tracked.isReliable ? Colors.green : Colors.orange,
                    ),
                  ),
                );
              },
            ),
          ),
        ],
      ),
    );
  }

  Widget _buildFaceTrackingView() {
    return Padding(
      padding: const EdgeInsets.all(16),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Text(
            'Face Tracking',
            style: Theme.of(context).textTheme.headlineSmall,
          ),
          const SizedBox(height: 16),

          // Face tracking toggle
          Card(
            child: ListTile(
              leading: Icon(
                _faceTrackingEnabled ? Icons.face : Icons.face_retouching_off,
              ),
              title: const Text('Face Tracking'),
              subtitle: Text(_faceTrackingEnabled ? 'Enabled' : 'Disabled'),
              trailing: Switch(
                value: _faceTrackingEnabled,
                onChanged: (_) => _toggleFaceTracking(),
              ),
            ),
          ),

          const SizedBox(height: 16),

          // Tracked faces section
          Text(
            'Tracked Faces (${_trackedFaces.length})',
            style: Theme.of(context).textTheme.titleMedium,
          ),
          const SizedBox(height: 8),

          Expanded(
            child: ListView.builder(
              itemCount: _trackedFaces.length,
              itemBuilder: (context, index) {
                final face = _trackedFaces[index];
                return Card(
                  child: ListTile(
                    leading: Icon(
                      face.isTracked
                          ? Icons.tag_faces
                          : Icons.face_retouching_off,
                      color: face.isTracked ? Colors.green : Colors.orange,
                    ),
                    title: Text('Face ID: ${face.id}'),
                    subtitle: Text(
                      'State: ${face.trackingState.name}\n'
                      'Confidence: ${(face.confidence * 100).toStringAsFixed(1)}%\n'
                      'Landmarks: ${face.landmarks.length}',
                    ),
                    trailing: Icon(
                      face.isReliable ? Icons.verified : Icons.warning,
                      color: face.isReliable ? Colors.green : Colors.orange,
                    ),
                  ),
                );
              },
            ),
          ),
        ],
      ),
    );
  }

  Widget _buildCloudAnchorView() {
    return Padding(
      padding: const EdgeInsets.all(16),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Text(
            'Cloud Anchors',
            style: Theme.of(context).textTheme.headlineSmall,
          ),
          const SizedBox(height: 16),

          // Cloud anchor support status
          Card(
            child: ListTile(
              leading: Icon(
                _cloudAnchorsSupported ? Icons.cloud_done : Icons.cloud_off,
                color: _cloudAnchorsSupported ? Colors.green : Colors.red,
              ),
              title: const Text('Cloud Anchor Support'),
              subtitle: Text(
                _cloudAnchorsSupported ? 'Supported' : 'Not Supported',
              ),
              trailing: ElevatedButton(
                onPressed: _checkCloudAnchorSupport,
                child: const Text('Check Support'),
              ),
            ),
          ),

          const SizedBox(height: 16),

          // Cloud anchor actions
          Card(
            child: Padding(
              padding: const EdgeInsets.all(16),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Text(
                    'Cloud Anchor Actions',
                    style: Theme.of(context).textTheme.titleMedium,
                  ),
                  const SizedBox(height: 16),
                  Row(
                    children: [
                      Expanded(
                        child: ElevatedButton.icon(
                          onPressed: _cloudAnchorsSupported
                              ? _createCloudAnchor
                              : null,
                          icon: const Icon(Icons.cloud_upload),
                          label: const Text('Create Cloud Anchor'),
                        ),
                      ),
                      const SizedBox(width: 8),
                      Expanded(
                        child: ElevatedButton.icon(
                          onPressed: _cloudAnchors.isNotEmpty
                              ? _shareCloudAnchor
                              : null,
                          icon: const Icon(Icons.share),
                          label: const Text('Share Session'),
                        ),
                      ),
                    ],
                  ),
                  const SizedBox(height: 8),
                  Row(
                    children: [
                      Expanded(
                        child: ElevatedButton.icon(
                          onPressed: _joinCloudAnchorSession,
                          icon: const Icon(Icons.group_add),
                          label: const Text('Join Session'),
                        ),
                      ),
                      const SizedBox(width: 8),
                      Expanded(
                        child: ElevatedButton.icon(
                          onPressed: _currentSessionId != null
                              ? () async {
                                  try {
                                    await _controller
                                        ?.leaveCloudAnchorSession();
                                    if (!mounted) return;
                                    setState(() {
                                      _currentSessionId = null;
                                    });
                                    ScaffoldMessenger.of(context).showSnackBar(
                                      const SnackBar(
                                        content: Text('Left session'),
                                      ),
                                    );
                                  } catch (e) {
                                    if (!mounted) return;
                                    ScaffoldMessenger.of(context).showSnackBar(
                                      SnackBar(
                                        content: Text(
                                          'Failed to leave session: $e',
                                        ),
                                      ),
                                    );
                                  }
                                }
                              : null,
                          icon: const Icon(Icons.exit_to_app),
                          label: const Text('Leave Session'),
                        ),
                      ),
                    ],
                  ),
                ],
              ),
            ),
          ),

          const SizedBox(height: 16),

          // Cloud anchors list
          Text(
            'Cloud Anchors (${_cloudAnchors.length})',
            style: Theme.of(context).textTheme.titleMedium,
          ),
          const SizedBox(height: 8),
          Expanded(
            child: _cloudAnchors.isEmpty
                ? const Center(child: Text('No cloud anchors yet'))
                : ListView.builder(
                    itemCount: _cloudAnchors.length,
                    itemBuilder: (context, index) {
                      final anchor = _cloudAnchors[index];
                      return Card(
                        child: ListTile(
                          leading: Icon(
                            anchor.isActive
                                ? Icons.cloud_done
                                : Icons.cloud_off,
                            color: anchor.isActive
                                ? Colors.green
                                : Colors.orange,
                          ),
                          title: Text('Cloud Anchor ${anchor.id}'),
                          subtitle: Column(
                            crossAxisAlignment: CrossAxisAlignment.start,
                            children: [
                              Text('State: ${anchor.state.name}'),
                              Text(
                                'Confidence: ${(anchor.confidence * 100).toInt()}%',
                              ),
                              Text('Position: ${anchor.position}'),
                            ],
                          ),
                          trailing: PopupMenuButton(
                            itemBuilder: (context) => [
                              PopupMenuItem(
                                value: 'delete',
                                child: const Text('Delete'),
                              ),
                            ],
                            onSelected: (value) async {
                              if (value == 'delete') {
                                try {
                                  await _controller?.deleteCloudAnchor(
                                    anchor.id,
                                  );
                                  if (!mounted) return;
                                  ScaffoldMessenger.of(context).showSnackBar(
                                    const SnackBar(
                                      content: Text('Cloud anchor deleted'),
                                    ),
                                  );
                                } catch (e) {
                                  if (!mounted) return;
                                  ScaffoldMessenger.of(context).showSnackBar(
                                    SnackBar(
                                      content: Text('Failed to delete: $e'),
                                    ),
                                  );
                                }
                              }
                            },
                          ),
                        ),
                      );
                    },
                  ),
          ),

          // Session info
          if (_currentSessionId != null) ...[
            const SizedBox(height: 16),
            Card(
              child: Padding(
                padding: const EdgeInsets.all(16),
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    Text(
                      'Current Session',
                      style: Theme.of(context).textTheme.titleMedium,
                    ),
                    const SizedBox(height: 8),
                    Text('Session ID: $_currentSessionId'),
                  ],
                ),
              ),
            ),
          ],
        ],
      ),
    );
  }

  Widget _buildAnimationView() {
    return Padding(
      padding: const EdgeInsets.all(16),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Text(
            'Animation Controls',
            style: Theme.of(context).textTheme.headlineSmall,
          ),
          const SizedBox(height: 16),

          // Current animation
          if (_currentAnimation != null)
            Card(
              child: ListTile(
                leading: const Icon(Icons.play_arrow),
                title: const Text('Current Animation'),
                subtitle: Text(_currentAnimation!),
              ),
            ),

          const SizedBox(height: 16),

          // Animation buttons
          Wrap(
            spacing: 8,
            runSpacing: 8,
            children: [
              ElevatedButton.icon(
                onPressed: () => _playAnimation('idle'),
                icon: const Icon(Icons.pause),
                label: const Text('Idle'),
              ),
              ElevatedButton.icon(
                onPressed: () => _playAnimation('walk'),
                icon: const Icon(Icons.directions_walk),
                label: const Text('Walk'),
              ),
              ElevatedButton.icon(
                onPressed: () => _playAnimation('jump'),
                icon: const Icon(Icons.vertical_align_top),
                label: const Text('Jump'),
              ),
              ElevatedButton.icon(
                onPressed: () => _playAnimation('run'),
                icon: const Icon(Icons.directions_run),
                label: const Text('Run'),
              ),
            ],
          ),

          const SizedBox(height: 24),

          // Animation blending
          Text(
            'Animation Blending',
            style: Theme.of(context).textTheme.titleMedium,
          ),
          const SizedBox(height: 8),

          Card(
            child: Padding(
              padding: const EdgeInsets.all(16),
              child: Column(
                children: [
                  Text('Blend Weight: ${_blendWeight.toStringAsFixed(2)}'),
                  Slider(
                    value: _blendWeight,
                    min: 0.0,
                    max: 1.0,
                    divisions: 20,
                    onChanged: (value) {
                      setState(() {
                        _blendWeight = value;
                      });
                    },
                  ),
                  ElevatedButton.icon(
                    onPressed: _blendAnimations,
                    icon: const Icon(Icons.merge),
                    label: const Text('Apply Blend'),
                  ),
                ],
              ),
            ),
          ),

          const SizedBox(height: 24),

          // Crossfade animation
          Text(
            'Animation Crossfade',
            style: Theme.of(context).textTheme.titleMedium,
          ),
          const SizedBox(height: 8),

          ElevatedButton.icon(
            onPressed: _crossfadeAnimation,
            icon: const Icon(Icons.swap_horiz),
            label: const Text('Crossfade to Walk'),
          ),
        ],
      ),
    );
  }

  Widget _buildDemoView() {
    return Padding(
      padding: const EdgeInsets.all(16),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Text(
            'Feature Demonstrations',
            style: Theme.of(context).textTheme.headlineSmall,
          ),
          const SizedBox(height: 16),

          // Quick setup section
          Card(
            child: Padding(
              padding: const EdgeInsets.all(16),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Text(
                    'Quick Setup',
                    style: Theme.of(context).textTheme.titleMedium,
                  ),
                  const SizedBox(height: 8),
                  Row(
                    children: [
                      Expanded(
                        child: ElevatedButton.icon(
                          onPressed: _addSampleImageTargets,
                          icon: const Icon(Icons.image_search),
                          label: const Text('Add Image Targets'),
                        ),
                      ),
                      const SizedBox(width: 8),
                      Expanded(
                        child: ElevatedButton.icon(
                          onPressed: _toggleImageTracking,
                          icon: Icon(
                            _imageTrackingEnabled
                                ? Icons.visibility
                                : Icons.visibility_off,
                          ),
                          label: Text(
                            _imageTrackingEnabled
                                ? 'Disable Image Tracking'
                                : 'Enable Image Tracking',
                          ),
                        ),
                      ),
                    ],
                  ),
                  const SizedBox(height: 8),
                  Row(
                    children: [
                      Expanded(
                        child: ElevatedButton.icon(
                          onPressed: _toggleFaceTracking,
                          icon: Icon(
                            _faceTrackingEnabled
                                ? Icons.face
                                : Icons.face_retouching_off,
                          ),
                          label: Text(
                            _faceTrackingEnabled
                                ? 'Disable Face Tracking'
                                : 'Enable Face Tracking',
                          ),
                        ),
                      ),
                      const SizedBox(width: 8),
                      Expanded(
                        child: ElevatedButton.icon(
                          onPressed: _resetSession,
                          icon: const Icon(Icons.refresh),
                          label: const Text('Reset Session'),
                        ),
                      ),
                    ],
                  ),
                ],
              ),
            ),
          ),

          const SizedBox(height: 16),

          // Feature demonstrations
          Text(
            'Feature Demonstrations',
            style: Theme.of(context).textTheme.titleMedium,
          ),
          const SizedBox(height: 8),

          Expanded(
            child: ListView(
              children: [
                _buildDemoCard(
                  'Basic AR Objects',
                  'Place spheres, cubes, and cylinders in the AR scene',
                  Icons.crop_square,
                  () => _addObjectAtScreenCenter(),
                ),
                _buildDemoCard(
                  '3D Models',
                  'Load and display custom 3D models from URLs',
                  Icons.model_training,
                  () => _addModelFromUrl(),
                ),
                _buildDemoCard(
                  'Image Tracking',
                  'Track specific images and anchor content to them',
                  Icons.image_search,
                  () => _toggleImageTracking(),
                ),
                _buildDemoCard(
                  'Face Tracking',
                  'Detect and track human faces with landmarks',
                  Icons.face,
                  () => _toggleFaceTracking(),
                ),
                _buildDemoCard(
                  'Cloud Anchors',
                  'Create persistent AR experiences that can be shared',
                  Icons.cloud,
                  () => _createCloudAnchor(),
                ),
                _buildDemoCard(
                  'Animations',
                  'Play, blend, and transition between animations',
                  Icons.animation,
                  () => _playSampleAnimation(),
                ),
                _buildDemoCard(
                  'Hit Testing',
                  'Detect surfaces and place objects precisely',
                  Icons.touch_app,
                  () => _performHitTest(),
                ),
                _buildDemoCard(
                  'Anchors',
                  'Create persistent AR anchors in the scene',
                  Icons.anchor,
                  () => _addAnchor(),
                ),
              ],
            ),
          ),
        ],
      ),
    );
  }

  Widget _buildDemoCard(
    String title,
    String description,
    IconData icon,
    VoidCallback onTap,
  ) {
    return Card(
      margin: const EdgeInsets.only(bottom: 8),
      child: ListTile(
        leading: Icon(icon),
        title: Text(title),
        subtitle: Text(description),
        trailing: const Icon(Icons.arrow_forward),
        onTap: onTap,
      ),
    );
  }

  Widget _buildStatusView() {
    return Padding(
      padding: const EdgeInsets.all(16),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Text('AR Status', style: Theme.of(context).textTheme.headlineSmall),
          const SizedBox(height: 16),

          // Session status
          Card(
            child: ListTile(
              leading: Icon(
                _isInitialized ? Icons.check_circle : Icons.error,
                color: _isInitialized ? Colors.green : Colors.red,
              ),
              title: const Text('AR Session'),
              subtitle: Text(_isInitialized ? 'Active' : 'Not Initialized'),
            ),
          ),

          const SizedBox(height: 16),

          // Statistics
          Text('Statistics', style: Theme.of(context).textTheme.titleMedium),
          const SizedBox(height: 8),

          Card(
            child: Padding(
              padding: const EdgeInsets.all(16),
              child: Column(
                children: [
                  _buildStatRow(
                    'Detected Planes',
                    _detectedPlanes.length.toString(),
                  ),
                  _buildStatRow(
                    'Image Targets',
                    _imageTargets.length.toString(),
                  ),
                  _buildStatRow(
                    'Tracked Images',
                    _trackedImages.length.toString(),
                  ),
                  _buildStatRow(
                    'Tracked Faces',
                    _trackedFaces.length.toString(),
                  ),
                  _buildStatRow('Objects Placed', _nodeCounter.toString()),
                  _buildStatRow(
                    'Image Tracking',
                    _imageTrackingEnabled ? 'Enabled' : 'Disabled',
                  ),
                  _buildStatRow(
                    'Face Tracking',
                    _faceTrackingEnabled ? 'Enabled' : 'Disabled',
                  ),
                  _buildStatRow(
                    'Cloud Anchors',
                    '${_cloudAnchors.length} anchors, ${_cloudAnchorsSupported ? 'Supported' : 'Not Supported'}',
                  ),
                ],
              ),
            ),
          ),

          const SizedBox(height: 24),

          // Control buttons
          Text(
            'Session Controls',
            style: Theme.of(context).textTheme.titleMedium,
          ),
          const SizedBox(height: 8),

          Wrap(
            spacing: 8,
            runSpacing: 8,
            children: [
              ElevatedButton.icon(
                onPressed: _addObjectAtScreenCenter,
                icon: const Icon(Icons.add),
                label: const Text('Add Object'),
              ),
              ElevatedButton.icon(
                onPressed: _addAnchor,
                icon: const Icon(Icons.place),
                label: const Text('Add Anchor'),
              ),
              ElevatedButton.icon(
                onPressed: _addCustomImageTarget,
                icon: const Icon(Icons.image),
                label: const Text('Add Target'),
              ),
              ElevatedButton.icon(
                onPressed: _resetSession,
                icon: const Icon(Icons.refresh),
                label: const Text('Reset'),
                style: ElevatedButton.styleFrom(
                  backgroundColor: Colors.red,
                  foregroundColor: Colors.white,
                ),
              ),
            ],
          ),
        ],
      ),
    );
  }

  Widget _buildStatRow(String label, String value) {
    return Padding(
      padding: const EdgeInsets.symmetric(vertical: 4),
      child: Row(
        mainAxisAlignment: MainAxisAlignment.spaceBetween,
        children: [
          Text(label),
          Text(value, style: const TextStyle(fontWeight: FontWeight.bold)),
        ],
      ),
    );
  }

  Future<void> _addModelFromUrl() async {
    if (_controller == null) return;

    try {
      final nodeId = 'model_${DateTime.now().millisecondsSinceEpoch}';
      final modelNode = ARNode.fromModel(
        id: nodeId,
        modelPath: 'https://example.com/models/character.glb',
        position: const Vector3(0, 0, -1),
        rotation: const Quaternion(0, 0, 0, 1),
        scale: const Vector3(0.1, 0.1, 0.1),
      );

      await _controller!.addNode(modelNode);

      if (!mounted) return;
      setState(() {
        _nodeCounter++;
      });

      ScaffoldMessenger.of(
        context,
      ).showSnackBar(const SnackBar(content: Text('3D model added to scene')));
    } catch (e) {
      if (!mounted) return;
      ScaffoldMessenger.of(
        context,
      ).showSnackBar(SnackBar(content: Text('Failed to add model: $e')));
    }
  }

  Future<void> _playSampleAnimation() async {
    if (_controller == null) return;

    try {
      // Try to play animation on the most recent model
      if (_nodeCounter > 0) {
        await _controller!.playAnimation(
          nodeId: 'model_${DateTime.now().millisecondsSinceEpoch}',
          animationId: 'idle',
        );

        if (!mounted) return;
        ScaffoldMessenger.of(
          context,
        ).showSnackBar(const SnackBar(content: Text('Animation started')));
      } else {
        ScaffoldMessenger.of(context).showSnackBar(
          const SnackBar(content: Text('Add a model first to play animations')),
        );
      }
    } catch (e) {
      if (!mounted) return;
      ScaffoldMessenger.of(
        context,
      ).showSnackBar(SnackBar(content: Text('Failed to play animation: $e')));
    }
  }

  Future<void> _performHitTest() async {
    if (_controller == null) return;

    try {
      final results = await _controller!.hitTest(0.5, 0.5);

      if (!mounted) return;
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text('Hit test found ${results.length} surfaces')),
      );
    } catch (e) {
      if (!mounted) return;
      ScaffoldMessenger.of(
        context,
      ).showSnackBar(SnackBar(content: Text('Hit test failed: $e')));
    }
  }
}
4
likes
0
points
60
downloads

Publisher

unverified uploader

Weekly Downloads

A Flutter plugin for building AR applications using ARCore on Android and RealityKit on iOS. Enables pure Dart AR development without native code.

Repository (GitHub)
View/report issues

License

unknown (license)

Dependencies

flutter, plugin_platform_interface

More

Packages that depend on augen

Packages that implement augen