/** * AR Contextual Recommender * * Analyzes the AR environment and provides contextual object recommendations * based on detected surfaces, lighting conditions, and scene understanding. */ import { NativeModules, Platform } from 'react-native'; import { ARSessionManager } from './ARSessionManager'; import { ARPlaneOrientation } from './types'; /** * Environment context types that can be detected */ export enum AREnvironmentContext { FLOOR = 'floor', TABLE = 'table', WALL = 'wall', CEILING = 'ceiling', OUTDOOR = 'outdoor', INDOOR = 'indoor', BRIGHT = 'bright', DIM = 'dim', SMALL_SPACE = 'smallSpace', LARGE_SPACE = 'largeSpace', OFFICE = 'office', LIVING_ROOM = 'livingRoom', KITCHEN = 'kitchen', BEDROOM = 'bedroom', BATHROOM = 'bathroom', UNKNOWN = 'unknown', } /** * Object recommendation including score and metadata */ export interface ARObjectRecommendation { /** * Unique identifier for the recommended object */ id: string; /** * Display name of the recommended object */ name: string; /** * URI to the 3D model */ modelUri: string; /** * Recommendation score (0.0 to 1.0) with higher being more relevant */ score: number; /** * Category of the object */ category: string; /** * Environment contexts this object is well-suited for */ suitableContexts: AREnvironmentContext[]; /** * Suggested scale for the object in this context */ suggestedScale: number; /** * Suggested position relative to the detected plane/feature * Values: 'center', 'edge', 'corner', 'wall', 'hanging' */ suggestedPlacement?: string; /** * Preview image URI */ previewImageUri?: string; /** * Additional metadata for the object */ metadata?: Record; } /** * Environment analysis result */ export interface AREnvironmentAnalysis { /** * Dominant environment contexts detected */ detectedContexts: AREnvironmentContext[]; /** * Confidence score for each detected context (0.0 to 1.0) */ contextConfidence: Record; /** * Detected horizontal planes (floor, tables) */ horizontalPlanes: number; /** * Detected vertical planes (walls) */ verticalPlanes: number; /** * Estimated room size in cubic meters (if indoor) */ estimatedSpaceVolume?: number; /** * Estimated ambient light intensity (0.0 to 1.0) */ lightIntensity: number; /** * Whether the environment has been successfully analyzed */ isAnalyzed: boolean; /** * Timestamp of the analysis */ timestamp: number; } /** * Configuration options for recommendations */ export interface ARRecommendationOptions { /** * Maximum number of recommendations to return * @default 5 */ maxResults?: number; /** * Minimum score threshold for recommendations (0.0 to 1.0) * @default 0.5 */ minScore?: number; /** * Categories to include in recommendations * If empty, all categories are included */ includeCategories?: string[]; /** * Categories to exclude from recommendations */ excludeCategories?: string[]; /** * Whether to prefer previously used/favorited objects * @default true */ preferUserHistory?: boolean; /** * Whether to use remote recommendation service * If false, only uses local recommendations * @default false */ useRemoteRecommendations?: boolean; /** * URL for remote recommendation service */ remoteServiceUrl?: string; /** * Authentication token for remote service */ remoteServiceToken?: string; } /** * Default recommendation options */ const defaultOptions: ARRecommendationOptions = { maxResults: 5, minScore: 0.5, includeCategories: [], excludeCategories: [], preferUserHistory: true, useRemoteRecommendations: false, }; /** * Data source for object catalog */ export interface ARObjectCatalogSource { /** * Gets all available objects in the catalog */ getAllObjects(): Promise; /** * Gets objects filtered by category */ getObjectsByCategory(category: string): Promise; /** * Gets a specific object by ID */ getObjectById(id: string): Promise; /** * Gets objects that match the given search query */ searchObjects(query: string): Promise; } /** * Simple local catalog implementation */ class LocalObjectCatalog implements ARObjectCatalogSource { private objects: ARObjectRecommendation[] = []; constructor() { // Initialize with some default objects this.initializeDefaultCatalog(); } /** * Initialize with a basic set of objects for different contexts */ private initializeDefaultCatalog() { // Furniture objects this.objects.push( { id: 'chair_001', name: 'Office Chair', modelUri: 'models/office_chair.glb', score: 0.9, category: 'furniture', suitableContexts: [ AREnvironmentContext.FLOOR, AREnvironmentContext.INDOOR, AREnvironmentContext.OFFICE ], suggestedScale: 1.0, suggestedPlacement: 'floor', previewImageUri: 'images/office_chair_preview.jpg' }, { id: 'sofa_001', name: 'Living Room Sofa', modelUri: 'models/sofa.glb', score: 0.9, category: 'furniture', suitableContexts: [ AREnvironmentContext.FLOOR, AREnvironmentContext.INDOOR, AREnvironmentContext.LIVING_ROOM ], suggestedScale: 1.0, suggestedPlacement: 'floor', previewImageUri: 'images/sofa_preview.jpg' }, { id: 'coffee_table_001', name: 'Coffee Table', modelUri: 'models/coffee_table.glb', score: 0.85, category: 'furniture', suitableContexts: [ AREnvironmentContext.FLOOR, AREnvironmentContext.INDOOR, AREnvironmentContext.LIVING_ROOM ], suggestedScale: 1.0, suggestedPlacement: 'floor', previewImageUri: 'images/coffee_table_preview.jpg' } ); // Decor objects this.objects.push( { id: 'plant_001', name: 'Plant', modelUri: 'models/plant.glb', score: 0.8, category: 'decor', suitableContexts: [ AREnvironmentContext.FLOOR, AREnvironmentContext.TABLE, AREnvironmentContext.INDOOR ], suggestedScale: 0.8, suggestedPlacement: 'corner', previewImageUri: 'images/plant_preview.jpg' }, { id: 'lamp_001', name: 'Table Lamp', modelUri: 'models/table_lamp.glb', score: 0.75, category: 'lighting', suitableContexts: [ AREnvironmentContext.TABLE, AREnvironmentContext.INDOOR, AREnvironmentContext.DIM ], suggestedScale: 0.5, suggestedPlacement: 'edge', previewImageUri: 'images/lamp_preview.jpg' }, { id: 'painting_001', name: 'Wall Painting', modelUri: 'models/painting.glb', score: 0.85, category: 'decor', suitableContexts: [ AREnvironmentContext.WALL, AREnvironmentContext.INDOOR, AREnvironmentContext.LIVING_ROOM ], suggestedScale: 0.8, suggestedPlacement: 'wall', previewImageUri: 'images/painting_preview.jpg' } ); // Kitchen objects this.objects.push( { id: 'dining_table_001', name: 'Dining Table', modelUri: 'models/dining_table.glb', score: 0.9, category: 'furniture', suitableContexts: [ AREnvironmentContext.FLOOR, AREnvironmentContext.INDOOR, AREnvironmentContext.KITCHEN ], suggestedScale: 1.0, suggestedPlacement: 'center', previewImageUri: 'images/dining_table_preview.jpg' }, { id: 'kitchen_island_001', name: 'Kitchen Island', modelUri: 'models/kitchen_island.glb', score: 0.85, category: 'furniture', suitableContexts: [ AREnvironmentContext.FLOOR, AREnvironmentContext.INDOOR, AREnvironmentContext.KITCHEN ], suggestedScale: 1.0, suggestedPlacement: 'center', previewImageUri: 'images/kitchen_island_preview.jpg' } ); // Outdoor objects this.objects.push( { id: 'garden_chair_001', name: 'Garden Chair', modelUri: 'models/garden_chair.glb', score: 0.9, category: 'outdoor', suitableContexts: [ AREnvironmentContext.FLOOR, AREnvironmentContext.OUTDOOR ], suggestedScale: 1.0, suggestedPlacement: 'floor', previewImageUri: 'images/garden_chair_preview.jpg' }, { id: 'garden_table_001', name: 'Garden Table', modelUri: 'models/garden_table.glb', score: 0.85, category: 'outdoor', suitableContexts: [ AREnvironmentContext.FLOOR, AREnvironmentContext.OUTDOOR ], suggestedScale: 1.0, suggestedPlacement: 'center', previewImageUri: 'images/garden_table_preview.jpg' } ); // Electronics this.objects.push( { id: 'tv_001', name: 'Flat Screen TV', modelUri: 'models/tv.glb', score: 0.8, category: 'electronics', suitableContexts: [ AREnvironmentContext.WALL, AREnvironmentContext.INDOOR, AREnvironmentContext.LIVING_ROOM ], suggestedScale: 0.9, suggestedPlacement: 'wall', previewImageUri: 'images/tv_preview.jpg' }, { id: 'desk_001', name: 'Computer Desk', modelUri: 'models/desk.glb', score: 0.85, category: 'furniture', suitableContexts: [ AREnvironmentContext.FLOOR, AREnvironmentContext.INDOOR, AREnvironmentContext.OFFICE ], suggestedScale: 1.0, suggestedPlacement: 'wall', previewImageUri: 'images/desk_preview.jpg' } ); } async getAllObjects(): Promise { return this.objects; } async getObjectsByCategory(category: string): Promise { return this.objects.filter(obj => obj.category === category); } async getObjectById(id: string): Promise { const found = this.objects.find(obj => obj.id === id); return found || null; } async searchObjects(query: string): Promise { query = query.toLowerCase(); return this.objects.filter( obj => obj.name.toLowerCase().includes(query) || obj.category.toLowerCase().includes(query) ); } } /** * Remote catalog implementation that fetches from a server */ class RemoteObjectCatalog implements ARObjectCatalogSource { private baseUrl: string; private authToken?: string; private cachedObjects: ARObjectRecommendation[] = []; private lastCacheTime: number = 0; private cacheExpiration: number = 3600000; // 1 hour in milliseconds constructor(baseUrl: string, authToken?: string) { this.baseUrl = baseUrl; this.authToken = authToken; } /** * Makes an authenticated request to the remote server */ private async makeRequest(endpoint: string, params: Record = {}): Promise { const url = new URL(endpoint, this.baseUrl); // Add query parameters Object.keys(params).forEach(key => { url.searchParams.append(key, params[key]); }); const headers: HeadersInit = { 'Content-Type': 'application/json', }; if (this.authToken) { headers['Authorization'] = `Bearer ${this.authToken}`; } try { const response = await fetch(url.toString(), { headers }); if (!response.ok) { throw new Error(`Network response was not ok: ${response.status}`); } return await response.json(); } catch (error) { console.error('Error fetching from remote catalog:', error); throw error; } } /** * Refreshes cache if needed */ private async refreshCacheIfNeeded(): Promise { const now = Date.now(); if (this.cachedObjects.length === 0 || (now - this.lastCacheTime) > this.cacheExpiration) { try { const data = await this.makeRequest('/objects'); this.cachedObjects = data.objects || []; this.lastCacheTime = now; } catch (error) { // On error, keep using cached data if available if (this.cachedObjects.length === 0) { throw error; // No cached data, propagate error } console.warn('Failed to refresh object catalog, using cached data'); } } } async getAllObjects(): Promise { await this.refreshCacheIfNeeded(); return this.cachedObjects; } async getObjectsByCategory(category: string): Promise { try { const data = await this.makeRequest('/objects/category', { category }); return data.objects || []; } catch (error) { // Fall back to filtering cached objects await this.refreshCacheIfNeeded(); return this.cachedObjects.filter(obj => obj.category === category); } } async getObjectById(id: string): Promise { try { const data = await this.makeRequest(`/objects/${id}`); return data.object || null; } catch (error) { // Fall back to finding in cached objects await this.refreshCacheIfNeeded(); return this.cachedObjects.find(obj => obj.id === id) || null; } } async searchObjects(query: string): Promise { try { const data = await this.makeRequest('/objects/search', { query }); return data.objects || []; } catch (error) { // Fall back to searching cached objects await this.refreshCacheIfNeeded(); query = query.toLowerCase(); return this.cachedObjects.filter( obj => obj.name.toLowerCase().includes(query) || obj.category.toLowerCase().includes(query) ); } } } /** * Class for AR contextual object recommendations */ export class ARContextualRecommender { private static environmentAnalysis: AREnvironmentAnalysis | null = null; private static isAnalyzing: boolean = false; private static options: ARRecommendationOptions = { ...defaultOptions }; private static localCatalog: LocalObjectCatalog = new LocalObjectCatalog(); private static remoteCatalog: RemoteObjectCatalog | null = null; private static userHistory: Set = new Set(); // IDs of previously used objects /** * Configure the recommender */ static setOptions(options: Partial): void { this.options = { ...this.options, ...options, }; // Set up remote catalog if options specify it if (options.useRemoteRecommendations && options.remoteServiceUrl) { this.remoteCatalog = new RemoteObjectCatalog( options.remoteServiceUrl, options.remoteServiceToken ); } else { this.remoteCatalog = null; } } /** * Analyzes the current AR environment to detect contexts */ static async analyzeEnvironment(): Promise { if (this.isAnalyzing) { // Return the last analysis if one is in progress return this.environmentAnalysis || this.createEmptyAnalysis(); } this.isAnalyzing = true; try { // Get current AR session information const features = await ARSessionManager.getSupportedFeatures(); // Check if environment analysis is supported // Ensure we have the required features for environment analysis if (!features || !features.planeDetection) { console.warn('Environment analysis not supported on this device - requires plane detection'); return this.createEmptyAnalysis(); } // Get detected planes info const sessionInfo = await ARSessionManager.getSessionProperty('planesInfo'); // Get light estimation const lightEstimation = await ARSessionManager.getLightEstimation(); // Analyze the environment based on available data const analysis: AREnvironmentAnalysis = { detectedContexts: [], contextConfidence: {} as Record, horizontalPlanes: 0, verticalPlanes: 0, lightIntensity: lightEstimation.ambientIntensity || 0.5, isAnalyzed: true, timestamp: Date.now() }; // Process plane information if (sessionInfo && sessionInfo.planes) { // Count horizontal and vertical planes for (const plane of sessionInfo.planes) { if (plane.orientation === ARPlaneOrientation.HORIZONTAL) { analysis.horizontalPlanes++; // Check if this is likely a floor or table if (plane.y < 0.3) { // Plane is low to the ground analysis.detectedContexts.push(AREnvironmentContext.FLOOR); analysis.contextConfidence[AREnvironmentContext.FLOOR] = 0.9; } else if (plane.y >= 0.5 && plane.y <= 1.2) { // Typical table height analysis.detectedContexts.push(AREnvironmentContext.TABLE); analysis.contextConfidence[AREnvironmentContext.TABLE] = 0.8; } } else if (plane.orientation === ARPlaneOrientation.VERTICAL) { analysis.verticalPlanes++; // This is likely a wall analysis.detectedContexts.push(AREnvironmentContext.WALL); analysis.contextConfidence[AREnvironmentContext.WALL] = 0.9; } } // Determine if we're in a room or outdoor space if (analysis.verticalPlanes >= 2) { analysis.detectedContexts.push(AREnvironmentContext.INDOOR); analysis.contextConfidence[AREnvironmentContext.INDOOR] = 0.8; // Try to estimate room type based on detected planes and layout this.estimateRoomType(analysis, sessionInfo.planes); } else { analysis.detectedContexts.push(AREnvironmentContext.OUTDOOR); analysis.contextConfidence[AREnvironmentContext.OUTDOOR] = 0.6; } // Estimate the space size if (analysis.detectedContexts.includes(AREnvironmentContext.INDOOR)) { const volume = this.estimateRoomVolume(sessionInfo.planes); analysis.estimatedSpaceVolume = volume; if (volume < 20) { // Smaller than 20 cubic meters analysis.detectedContexts.push(AREnvironmentContext.SMALL_SPACE); analysis.contextConfidence[AREnvironmentContext.SMALL_SPACE] = 0.7; } else { analysis.detectedContexts.push(AREnvironmentContext.LARGE_SPACE); analysis.contextConfidence[AREnvironmentContext.LARGE_SPACE] = 0.7; } } } // Process lighting information if (lightEstimation) { if (lightEstimation.ambientIntensity > 0.7) { analysis.detectedContexts.push(AREnvironmentContext.BRIGHT); analysis.contextConfidence[AREnvironmentContext.BRIGHT] = lightEstimation.ambientIntensity; } else if (lightEstimation.ambientIntensity < 0.3) { analysis.detectedContexts.push(AREnvironmentContext.DIM); analysis.contextConfidence[AREnvironmentContext.DIM] = 1 - lightEstimation.ambientIntensity; } } // If we couldn't determine any contexts, add UNKNOWN if (analysis.detectedContexts.length === 0) { analysis.detectedContexts.push(AREnvironmentContext.UNKNOWN); analysis.contextConfidence[AREnvironmentContext.UNKNOWN] = 1.0; } // Update the stored analysis this.environmentAnalysis = analysis; return analysis; } catch (error) { console.error('Error analyzing environment:', error); return this.createEmptyAnalysis(); } finally { this.isAnalyzing = false; } } /** * Creates an empty analysis result when proper analysis fails */ private static createEmptyAnalysis(): AREnvironmentAnalysis { const emptyConfidence = {} as Record; // Initialize all environment contexts with zero confidence Object.values(AREnvironmentContext).forEach(context => { emptyConfidence[context] = 0; }); // Set unknown to full confidence emptyConfidence[AREnvironmentContext.UNKNOWN] = 1.0; return { detectedContexts: [AREnvironmentContext.UNKNOWN], contextConfidence: emptyConfidence, horizontalPlanes: 0, verticalPlanes: 0, lightIntensity: 0.5, isAnalyzed: false, timestamp: Date.now() }; } /** * Estimates the room type based on detected planes */ private static estimateRoomType( analysis: AREnvironmentAnalysis, planes: any[] ): void { // This is a simplified logic - real implementation would use ML models // that analyze plane layouts, detected objects, etc. // Simple heuristics for room classification if (planes.length >= 4) { // Look for patterns that might suggest room types // Check for kitchen characteristics (multiple horizontal surfaces at different heights) const horizontalPlaneHeights = planes .filter(p => p.orientation === ARPlaneOrientation.HORIZONTAL) .map(p => p.y); const uniqueHeightGroups = this.groupSimilarHeights(horizontalPlaneHeights); if (uniqueHeightGroups.length >= 2) { // Multiple horizontal surfaces at different heights suggests kitchen or office analysis.detectedContexts.push(AREnvironmentContext.KITCHEN); analysis.contextConfidence[AREnvironmentContext.KITCHEN] = 0.6; } // Simple living room detection (large floor area with few obstacles) const largeHorizontalPlanes = planes.filter( p => p.orientation === ARPlaneOrientation.HORIZONTAL && p.y < 0.3 && // Close to floor level p.extent && p.extent.width * p.extent.height > 4 // At least 2x2 meters ); if (largeHorizontalPlanes.length > 0) { analysis.detectedContexts.push(AREnvironmentContext.LIVING_ROOM); analysis.contextConfidence[AREnvironmentContext.LIVING_ROOM] = 0.7; } // If there are multiple vertical planes but no clear context, guess office if (planes.filter(p => p.orientation === ARPlaneOrientation.VERTICAL).length > 3) { analysis.detectedContexts.push(AREnvironmentContext.OFFICE); analysis.contextConfidence[AREnvironmentContext.OFFICE] = 0.5; } } // If we couldn't determine a specific room type, default to living room // with lower confidence if (!analysis.detectedContexts.some(ctx => ctx === AREnvironmentContext.KITCHEN || ctx === AREnvironmentContext.LIVING_ROOM || ctx === AREnvironmentContext.OFFICE || ctx === AREnvironmentContext.BEDROOM || ctx === AREnvironmentContext.BATHROOM )) { analysis.detectedContexts.push(AREnvironmentContext.LIVING_ROOM); analysis.contextConfidence[AREnvironmentContext.LIVING_ROOM] = 0.4; } } /** * Groups similar heights together (within 10cm) */ private static groupSimilarHeights(heights: number[]): number[][] { const groups: number[][] = []; const THRESHOLD = 0.1; // 10cm for (const height of heights) { let added = false; for (const group of groups) { if (Math.abs(group[0] - height) < THRESHOLD) { group.push(height); added = true; break; } } if (!added) { groups.push([height]); } } return groups; } /** * Estimates room volume based on detected planes */ private static estimateRoomVolume(planes: any[]): number { try { // Get floor and ceiling heights if available const floorPlanes = planes.filter( p => p.orientation === ARPlaneOrientation.HORIZONTAL && p.y < 0.3 ); const ceilingPlanes = planes.filter( p => p.orientation === ARPlaneOrientation.HORIZONTAL && p.y > 2.0 ); // Get wall extents const wallPlanes = planes.filter( p => p.orientation === ARPlaneOrientation.VERTICAL ); if (floorPlanes.length === 0 || wallPlanes.length === 0) { return 30; // Default to 30 cubic meters if can't determine } // Estimate room dimensions based on detected planes let roomWidth = 0; let roomLength = 0; let roomHeight = 2.4; // Default ceiling height // Calculate room height if we have ceiling planes if (ceilingPlanes.length > 0 && floorPlanes.length > 0) { roomHeight = Math.max(...ceilingPlanes.map(p => p.y)) - Math.min(...floorPlanes.map(p => p.y)); } // Calculate room width and length based on wall planes if (wallPlanes.length >= 2) { // Simplified approach: find most distant walls let maxDistance = 0; for (let i = 0; i < wallPlanes.length; i++) { for (let j = i + 1; j < wallPlanes.length; j++) { const distance = this.distanceBetweenPlanes(wallPlanes[i], wallPlanes[j]); maxDistance = Math.max(maxDistance, distance); } } if (maxDistance > 0) { roomWidth = maxDistance; roomLength = maxDistance * 1.5; // Assume rectangular room } else { roomWidth = 4; // Default to 4m if can't determine roomLength = 5; // Default to 5m if can't determine } } else { roomWidth = 4; // Default to 4m if can't determine roomLength = 5; // Default to 5m if can't determine } return roomWidth * roomLength * roomHeight; } catch (error) { console.error('Error estimating room volume:', error); return 30; // Default to 30 cubic meters on error } } /** * Calculate approximate distance between two planes */ private static distanceBetweenPlanes(plane1: any, plane2: any): number { // Simple Euclidean distance between plane centers if (plane1.center && plane2.center) { const dx = plane1.center.x - plane2.center.x; const dy = plane1.center.y - plane2.center.y; const dz = plane1.center.z - plane2.center.z; return Math.sqrt(dx*dx + dy*dy + dz*dz); } return 0; } /** * Gets object catalog source based on current options */ private static getObjectCatalog(): ARObjectCatalogSource { if (this.options.useRemoteRecommendations && this.remoteCatalog) { return this.remoteCatalog; } return this.localCatalog; } /** * Records that a user has selected/used an object */ static recordObjectUsage(objectId: string): void { this.userHistory.add(objectId); } /** * Clears the user history */ static clearUserHistory(): void { this.userHistory.clear(); } /** * Gets recommendations based on the current environment */ static async getRecommendations( options?: Partial ): Promise { // Merge options const mergedOptions = { ...this.options, ...options, }; // Analyze the environment if we don't have recent analysis if (!this.environmentAnalysis || Date.now() - this.environmentAnalysis.timestamp > 10000) { await this.analyzeEnvironment(); } if (!this.environmentAnalysis) { return []; } try { // Get all objects from the catalog const catalog = this.getObjectCatalog(); const allObjects = await catalog.getAllObjects(); // Filter by categories if specified let filteredObjects = allObjects; if (mergedOptions.includeCategories && mergedOptions.includeCategories.length > 0) { filteredObjects = filteredObjects.filter(obj => mergedOptions.includeCategories!.includes(obj.category) ); } if (mergedOptions.excludeCategories && mergedOptions.excludeCategories.length > 0) { filteredObjects = filteredObjects.filter(obj => !mergedOptions.excludeCategories!.includes(obj.category) ); } // Score objects based on environment context const scoredObjects = filteredObjects.map(obj => { let contextScore = 0; // Calculate how well this object fits the detected contexts for (const context of this.environmentAnalysis!.detectedContexts) { if (obj.suitableContexts.includes(context)) { // Weight by confidence of the context detection const contextConfidence = this.environmentAnalysis!.contextConfidence[context] || 0.5; contextScore += contextConfidence; } } // Normalize context score (0-1) const normalizedContextScore = Math.min( 1.0, contextScore / Math.max(1, this.environmentAnalysis!.detectedContexts.length) ); // Adjust score based on user history if preferUserHistory is enabled let userHistoryBonus = 0; if (mergedOptions.preferUserHistory && this.userHistory.has(obj.id)) { userHistoryBonus = 0.1; // Small bonus for previously used objects } // Combine original object score with context score and user history const finalScore = 0.3 * obj.score + 0.6 * normalizedContextScore + userHistoryBonus; return { ...obj, score: finalScore }; }); // Filter by minimum score const minScore = mergedOptions.minScore || 0.5; const recommendedObjects = scoredObjects .filter(obj => obj.score >= minScore) .sort((a, b) => b.score - a.score); // Sort by score descending // Limit to maximum results const maxResults = mergedOptions.maxResults || 5; return recommendedObjects.slice(0, maxResults); } catch (error) { console.error('Error getting recommendations:', error); return []; } } /** * Gets the current environment analysis */ static getCurrentAnalysis(): AREnvironmentAnalysis | null { return this.environmentAnalysis; } /** * Gets recommendations for a specific position */ static async getRecommendationsForPosition( position: { x: number; y: number; z: number }, options?: Partial ): Promise { // First get general recommendations const recommendations = await this.getRecommendations(options); // Then refine based on specific position // This would use hit testing to determine the surface type at the position try { const hitTestResult = await ARSessionManager.performHitTest(position); if (hitTestResult.length > 0) { const hit = hitTestResult[0]; // Adjust recommendations based on surface type if (hit.type === 'horizontal') { // For horizontal surfaces, prefer objects that go on tables or floors return recommendations.filter(obj => obj.suitableContexts.includes(AREnvironmentContext.TABLE) || obj.suitableContexts.includes(AREnvironmentContext.FLOOR) ); } else if (hit.type === 'vertical') { // For vertical surfaces, prefer objects that go on walls return recommendations.filter(obj => obj.suitableContexts.includes(AREnvironmentContext.WALL) ); } } } catch (error) { console.error('Error refining recommendations for position:', error); } // If we couldn't refine, return the general recommendations return recommendations; } /** * Searches for objects by name or category */ static async searchObjects(query: string): Promise { try { const catalog = this.getObjectCatalog(); return await catalog.searchObjects(query); } catch (error) { console.error('Error searching objects:', error); return []; } } }