/** * AR Anchors and Persistent AR Experiences * * This module enables anchoring AR content to real-world positions that can * persist across app sessions and be shared between devices. */ import { NativeModules, Platform } from 'react-native'; import { ARSessionManager } from './ARSessionManager'; const NativeAR = NativeModules.ARModule; /** * Types of anchors that can be created */ export enum ARAnchorType { /** * Basic anchor attached to a point in 3D space */ POINT = 'point', /** * Anchor attached to a detected plane */ PLANE = 'plane', /** * Anchor attached to a detected image */ IMAGE = 'image', /** * Anchor attached to a geographic location (requires GPS) */ GEO = 'geo', /** * Anchor that can be shared with other devices */ CLOUD = 'cloud', } /** * Persistence level for anchors */ export enum ARPersistenceLevel { /** * Anchor only exists in the current AR session */ SESSION = 'session', /** * Anchor persists across app restarts on this device */ DEVICE = 'device', /** * Anchor can be shared with other devices and persists in the cloud */ CLOUD = 'cloud', } /** * Base interface for all anchor types */ export interface ARAnchorBase { /** * Unique identifier for the anchor */ id: string; /** * The type of anchor */ type: ARAnchorType; /** * Persistence level of the anchor */ persistenceLevel: ARPersistenceLevel; /** * Position of the anchor in world space */ position: { x: number; y: number; z: number }; /** * Rotation of the anchor in radians */ rotation: { x: number; y: number; z: number }; /** * When the anchor was created */ createdAt: number; /** * Last time the anchor was updated */ updatedAt: number; /** * User-defined metadata for this anchor */ metadata?: Record; } /** * Point anchor, attached to a specific point in 3D space */ export interface ARPointAnchor extends ARAnchorBase { type: ARAnchorType.POINT; } /** * Plane anchor, attached to a detected plane */ export interface ARPlaneAnchor extends ARAnchorBase { type: ARAnchorType.PLANE; /** * Dimensions of the detected plane */ dimensions: { width: number; height: number }; /** * Normal vector of the plane */ normal: { x: number; y: number; z: number }; } /** * Image anchor, attached to a detected reference image */ export interface ARImageAnchor extends ARAnchorBase { type: ARAnchorType.IMAGE; /** * Name of the reference image */ imageName: string; /** * Physical size of the image in meters */ physicalSize: { width: number; height: number }; } /** * Geographic anchor, attached to a real-world location */ export interface ARGeoAnchor extends ARAnchorBase { type: ARAnchorType.GEO; /** * Coordinates of the anchor */ coordinate: { latitude: number; longitude: number; altitude?: number; }; /** * Accuracy of the geo location in meters */ accuracy?: number; } /** * Cloud anchor that can be shared with other devices */ export interface ARCloudAnchor extends ARAnchorBase { type: ARAnchorType.CLOUD; /** * Cloud identifier for the anchor */ cloudIdentifier: string; /** * Whether the cloud anchor is currently being hosted */ isHosting: boolean; /** * Anchor hosting/resolving state */ cloudState: ARCloudAnchorState; /** * Time-to-live for the cloud anchor in seconds * Default: 24 hours */ ttlSeconds?: number; } /** * State of a cloud anchor */ export enum ARCloudAnchorState { NONE = 'none', TASK_IN_PROGRESS = 'taskInProgress', HOSTING_SUCCESS = 'hostingSuccess', HOSTING_ERROR = 'hostingError', RESOLVING_SUCCESS = 'resolvingSuccess', RESOLVING_ERROR = 'resolvingError', } /** * Options for creating a new anchor */ export interface ARCreateAnchorOptions { /** * Position for the anchor in world space */ position: { x: number; y: number; z: number }; /** * Rotation for the anchor in radians */ rotation?: { x: number; y: number; z: number }; /** * Persistence level for the anchor * @default ARPersistenceLevel.SESSION */ persistenceLevel?: ARPersistenceLevel; /** * Custom metadata to attach to the anchor */ metadata?: Record; } /** * Options for creating a new plane anchor */ export interface ARCreatePlaneAnchorOptions extends ARCreateAnchorOptions { /** * Dimensions of the plane */ dimensions: { width: number; height: number }; /** * Normal vector of the plane */ normal: { x: number; y: number; z: number }; } /** * Options for creating a new geo anchor */ export interface ARCreateGeoAnchorOptions extends ARCreateAnchorOptions { /** * Coordinates for the anchor */ coordinate: { latitude: number; longitude: number; altitude?: number; }; } /** * Options for creating a cloud anchor */ export interface ARCreateCloudAnchorOptions extends ARCreateAnchorOptions { /** * Time-to-live for the cloud anchor in seconds * @default 86400 (24 hours) */ ttlSeconds?: number; } /** * Options for hosting a cloud anchor */ export interface ARHostCloudAnchorOptions { /** * ID of the anchor to host in the cloud */ anchorId: string; /** * Time-to-live for the cloud anchor in seconds * @default 86400 (24 hours) */ ttlSeconds?: number; /** * Custom metadata to attach to the cloud anchor */ metadata?: Record; } /** * Options for resolving a cloud anchor */ export interface ARResolveCloudAnchorOptions { /** * Cloud identifier of the anchor to resolve */ cloudIdentifier: string; } /** * Event for anchor updates */ export interface ARAnchorUpdateEvent { /** * The anchor that was updated */ anchor: ARAnchorBase; /** * Type of update that occurred */ updateType: 'added' | 'updated' | 'removed'; } /** * Event for cloud anchor state changes */ export interface ARCloudAnchorEvent { /** * Cloud anchor that changed */ anchor: ARCloudAnchor; /** * The new state of the cloud anchor */ state: ARCloudAnchorState; /** * Error message if hosting/resolving failed */ errorMessage?: string; } /** * Options for anchor finding */ export interface ARFindAnchorsOptions { /** * Types of anchors to find */ types?: ARAnchorType[]; /** * Maximum distance in meters from the specified position */ maxDistance?: number; /** * Only return anchors with this persistence level */ persistenceLevel?: ARPersistenceLevel; } /** * Class that handles AR anchors for persistent experiences */ export class ARAnchorManager { /** * Creates a new point anchor at the given position * @param options Options for creating the anchor * @returns Promise that resolves to the created anchor's ID */ static async createAnchor(options: ARCreateAnchorOptions): Promise { try { // Set default rotation if not provided const rotation = options.rotation || { x: 0, y: 0, z: 0 }; // Set default persistence level if not provided const persistenceLevel = options.persistenceLevel || ARPersistenceLevel.SESSION; // Create the anchor through the native module return await NativeAR.createAnchor({ ...options, rotation, persistenceLevel, type: ARAnchorType.POINT }); } catch (error) { console.error('Error creating anchor:', error); throw error; } } /** * Creates a new plane anchor * @param options Options for creating the plane anchor * @returns Promise that resolves to the created anchor's ID */ static async createPlaneAnchor(options: ARCreatePlaneAnchorOptions): Promise { try { // Set default rotation if not provided const rotation = options.rotation || { x: 0, y: 0, z: 0 }; // Set default persistence level if not provided const persistenceLevel = options.persistenceLevel || ARPersistenceLevel.SESSION; // Create the plane anchor through the native module return await NativeAR.createAnchor({ ...options, rotation, persistenceLevel, type: ARAnchorType.PLANE }); } catch (error) { console.error('Error creating plane anchor:', error); throw error; } } /** * Creates a new geo anchor * @param options Options for creating the geo anchor * @returns Promise that resolves to the created anchor's ID */ static async createGeoAnchor(options: ARCreateGeoAnchorOptions): Promise { try { // Check if geo anchors are supported const features = await ARSessionManager.getSupportedFeatures(); if (!features.geoAnchors) { throw new Error('Geo anchors are not supported on this device'); } // Set default rotation if not provided const rotation = options.rotation || { x: 0, y: 0, z: 0 }; // Set default persistence level if not provided const persistenceLevel = options.persistenceLevel || ARPersistenceLevel.SESSION; // Create the geo anchor through the native module return await NativeAR.createAnchor({ ...options, rotation, persistenceLevel, type: ARAnchorType.GEO }); } catch (error) { console.error('Error creating geo anchor:', error); throw error; } } /** * Creates a new cloud anchor * @param options Options for creating the cloud anchor * @returns Promise that resolves to the created anchor's ID */ static async createCloudAnchor(options: ARCreateCloudAnchorOptions): Promise { try { // Check if cloud anchors are supported const features = await ARSessionManager.getSupportedFeatures(); if (!features.cloudAnchors) { throw new Error('Cloud anchors are not supported on this device'); } // Set default rotation if not provided const rotation = options.rotation || { x: 0, y: 0, z: 0 }; // Always use cloud persistence level for cloud anchors const persistenceLevel = ARPersistenceLevel.CLOUD; // Set default TTL if not provided (24 hours) const ttlSeconds = options.ttlSeconds || 86400; // Create the cloud anchor through the native module return await NativeAR.createAnchor({ ...options, rotation, persistenceLevel, ttlSeconds, type: ARAnchorType.CLOUD }); } catch (error) { console.error('Error creating cloud anchor:', error); throw error; } } /** * Hosts an existing anchor in the cloud so it can be shared * @param options Options for hosting the cloud anchor * @returns Promise that resolves to the cloud identifier once hosting is successful */ static async hostCloudAnchor(options: ARHostCloudAnchorOptions): Promise { try { // Check if cloud anchors are supported const features = await ARSessionManager.getSupportedFeatures(); if (!features.cloudAnchors) { throw new Error('Cloud anchors are not supported on this device'); } // Set default TTL if not provided (24 hours) const ttlSeconds = options.ttlSeconds || 86400; // Host the anchor in the cloud return await NativeAR.hostCloudAnchor({ ...options, ttlSeconds }); } catch (error) { console.error('Error hosting cloud anchor:', error); throw error; } } /** * Resolves a cloud anchor from its cloud identifier * @param options Options for resolving the cloud anchor * @returns Promise that resolves to the anchor ID once resolving is successful */ static async resolveCloudAnchor(options: ARResolveCloudAnchorOptions): Promise { try { // Check if cloud anchors are supported const features = await ARSessionManager.getSupportedFeatures(); if (!features.cloudAnchors) { throw new Error('Cloud anchors are not supported on this device'); } // Resolve the cloud anchor return await NativeAR.resolveCloudAnchor(options); } catch (error) { console.error('Error resolving cloud anchor:', error); throw error; } } /** * Gets an anchor by its ID * @param anchorId ID of the anchor to get * @returns Promise that resolves to the anchor */ static async getAnchor(anchorId: string): Promise { try { const anchorData = await NativeAR.getAnchor(anchorId); return this.parseAnchorData(anchorData); } catch (error) { console.error(`Error getting anchor with ID ${anchorId}:`, error); throw error; } } /** * Updates an existing anchor * @param anchorId ID of the anchor to update * @param properties Properties to update * @returns Promise that resolves when the update is complete */ static async updateAnchor( anchorId: string, properties: Partial<{ position: { x: number; y: number; z: number }; rotation: { x: number; y: number; z: number }; metadata: Record; }> ): Promise { try { await NativeAR.updateAnchor(anchorId, properties); } catch (error) { console.error(`Error updating anchor with ID ${anchorId}:`, error); throw error; } } /** * Removes an anchor * @param anchorId ID of the anchor to remove * @returns Promise that resolves when the anchor is removed */ static async removeAnchor(anchorId: string): Promise { try { await NativeAR.removeAnchor(anchorId); } catch (error) { console.error(`Error removing anchor with ID ${anchorId}:`, error); throw error; } } /** * Gets all anchors with optional filtering * @param options Options for filtering anchors * @returns Promise that resolves to an array of anchors */ static async getAllAnchors(options?: { types?: ARAnchorType[]; persistenceLevel?: ARPersistenceLevel; }): Promise { try { const anchorsData = await NativeAR.getAllAnchors(options || {}); return anchorsData.map((data: any) => this.parseAnchorData(data)); } catch (error) { console.error('Error getting all anchors:', error); throw error; } } /** * Finds anchors near a specified position * @param position Position to search around * @param options Options for finding anchors * @returns Promise that resolves to an array of anchors sorted by distance */ static async findAnchorsNearPosition( position: { x: number; y: number; z: number }, options?: ARFindAnchorsOptions ): Promise { try { const anchorsData = await NativeAR.findAnchorsNearPosition(position, options || {}); return anchorsData.map((data: any) => this.parseAnchorData(data)); } catch (error) { console.error('Error finding anchors near position:', error); throw error; } } /** * Attaches an object to an anchor * @param objectId ID of the object to attach * @param anchorId ID of the anchor to attach to * @param offsetPosition Optional position offset relative to anchor * @param offsetRotation Optional rotation offset relative to anchor * @returns Promise that resolves when the object is attached */ static async attachObjectToAnchor( objectId: string, anchorId: string, offsetPosition?: { x: number; y: number; z: number }, offsetRotation?: { x: number; y: number; z: number } ): Promise { try { await NativeAR.attachObjectToAnchor( objectId, anchorId, offsetPosition || { x: 0, y: 0, z: 0 }, offsetRotation || { x: 0, y: 0, z: 0 } ); } catch (error) { console.error(`Error attaching object ${objectId} to anchor ${anchorId}:`, error); throw error; } } /** * Detaches an object from its anchor * @param objectId ID of the object to detach * @returns Promise that resolves when the object is detached */ static async detachObjectFromAnchor(objectId: string): Promise { try { await NativeAR.detachObjectFromAnchor(objectId); } catch (error) { console.error(`Error detaching object ${objectId} from anchor:`, error); throw error; } } /** * Gets the anchor ID attached to an object * @param objectId ID of the object * @returns Promise that resolves to the anchor ID or null if not attached */ static async getObjectAnchorId(objectId: string): Promise { try { return await NativeAR.getObjectAnchorId(objectId); } catch (error) { console.error(`Error getting anchor ID for object ${objectId}:`, error); throw error; } } /** * Gets all objects attached to an anchor * @param anchorId ID of the anchor * @returns Promise that resolves to an array of object IDs */ static async getAnchorAttachedObjects(anchorId: string): Promise { try { return await NativeAR.getAnchorAttachedObjects(anchorId); } catch (error) { console.error(`Error getting objects attached to anchor ${anchorId}:`, error); throw error; } } /** * Shares an AR experience with another device * This creates a shareable data package containing anchors and object placements * @param options Options for sharing the experience * @returns Promise that resolves to a sharing code that can be used by another device */ static async shareExperience(options: { anchorIds?: string[]; includeAllCloudAnchors?: boolean; includeAllLocalAnchors?: boolean; includeAttachedObjects?: boolean; metadata?: Record; ttlSeconds?: number; }): Promise { try { // Check if experience sharing is supported const features = await ARSessionManager.getSupportedFeatures(); if (!features.cloudAnchors) { throw new Error('Experience sharing requires cloud anchor support'); } // Set default TTL if not provided (24 hours) const ttlSeconds = options.ttlSeconds || 86400; // Share the experience return await NativeAR.shareExperience({ ...options, ttlSeconds }); } catch (error) { console.error('Error sharing AR experience:', error); throw error; } } /** * Restores a shared AR experience * @param sharingCode Code obtained from shareExperience * @returns Promise that resolves to an array of restored anchor IDs */ static async restoreSharedExperience(sharingCode: string): Promise<{ anchorIds: string[]; metadata?: Record; }> { try { // Check if experience sharing is supported const features = await ARSessionManager.getSupportedFeatures(); if (!features.cloudAnchors) { throw new Error('Experience sharing requires cloud anchor support'); } // Restore the shared experience return await NativeAR.restoreSharedExperience(sharingCode); } catch (error) { console.error('Error restoring shared AR experience:', error); throw error; } } /** * Imports a serialized AR experience * @param serializedData Serialized data containing anchors and objects * @returns Promise that resolves to an array of imported anchor IDs */ static async importExperience(serializedData: string): Promise<{ anchorIds: string[]; metadata?: Record; }> { try { return await NativeAR.importExperience(serializedData); } catch (error) { console.error('Error importing AR experience:', error); throw error; } } /** * Exports an AR experience to serialized data * @param options Options for exporting the experience * @returns Promise that resolves to serialized data */ static async exportExperience(options: { anchorIds?: string[]; includeAllAnchors?: boolean; includeAttachedObjects?: boolean; metadata?: Record; }): Promise { try { return await NativeAR.exportExperience(options); } catch (error) { console.error('Error exporting AR experience:', error); throw error; } } /** * Saves all persistent anchors to storage * @returns Promise that resolves when anchors are saved */ static async saveAnchors(): Promise { try { await NativeAR.saveAnchors(); } catch (error) { console.error('Error saving anchors:', error); throw error; } } /** * Loads persistent anchors from storage * @returns Promise that resolves to an array of loaded anchor IDs */ static async loadAnchors(): Promise { try { return await NativeAR.loadAnchors(); } catch (error) { console.error('Error loading anchors:', error); throw error; } } /** * Clears all persistent anchors from storage * @returns Promise that resolves when anchors are cleared */ static async clearSavedAnchors(): Promise { try { await NativeAR.clearSavedAnchors(); } catch (error) { console.error('Error clearing saved anchors:', error); throw error; } } /** * Helper method to convert raw anchor data from native module to typed anchor objects * @param data Raw anchor data * @returns Typed anchor object */ private static parseAnchorData(data: any): ARAnchorBase { switch (data.type) { case ARAnchorType.PLANE: return data as ARPlaneAnchor; case ARAnchorType.IMAGE: return data as ARImageAnchor; case ARAnchorType.GEO: return data as ARGeoAnchor; case ARAnchorType.CLOUD: return data as ARCloudAnchor; case ARAnchorType.POINT: default: return data as ARPointAnchor; } } }