From 6fbd2a2469b2fefb4eb9cf7337a315061738fc7e Mon Sep 17 00:00:00 2001 From: andrewwallacespeckle Date: Tue, 9 Sep 2025 10:45:23 +0100 Subject: [PATCH] Improve property selection list --- .../viewer/composables/filtering/dataStore.ts | 174 ++++++++++++------ .../lib/viewer/helpers/filters/constants.ts | 5 + .../lib/viewer/helpers/filters/utils.ts | 3 +- 3 files changed, 129 insertions(+), 53 deletions(-) diff --git a/packages/frontend-2/lib/viewer/composables/filtering/dataStore.ts b/packages/frontend-2/lib/viewer/composables/filtering/dataStore.ts index 8ba44520a..8a5ffc9c6 100644 --- a/packages/frontend-2/lib/viewer/composables/filtering/dataStore.ts +++ b/packages/frontend-2/lib/viewer/composables/filtering/dataStore.ts @@ -14,6 +14,26 @@ import type { PropertyInfoBase } from '~/lib/viewer/helpers/filters/types' import { useInjectedViewerState } from '~~/lib/viewer/composables/setup' +import { shouldExcludeFromFiltering } from '~/lib/viewer/helpers/filters/utils' +import { DEEP_EXTRACTION_CONFIG } from '~/lib/viewer/helpers/filters/constants' + +/** + * Helper function to batch property map updates for better performance + */ +function processBatchedPropertyUpdates( + updates: Array<{ path: string; value: unknown; type: string }>, + propertyMap: Record +) { + for (const update of updates) { + if (!propertyMap[update.path]) { + propertyMap[update.path] = { + concatenatedPath: update.path, + value: update.value, + type: update.type + } as PropertyInfoBase + } + } +} export function useCreateViewerFilteringDataStore() { const dataSourcesMap: Ref> = ref({}) @@ -48,71 +68,121 @@ export function useCreateViewerFilteringDataStore() { const objectId = node.model.id objectMap[objectId] = node.model.raw as SpeckleObject - // Extract only commonly used properties - avoid expensive full traversal + // Extract all properties with deep traversal const objProps: Record = {} - // Direct properties - for (const [key, value] of Object.entries(node.model.raw)) { - if (typeof value !== 'object' || value === null || Array.isArray(value)) { - const path = key - objProps[path] = value - if (!propertyMap[path]) { - propertyMap[path] = { - concatenatedPath: path, + const pendingPropertyUpdates: Array<{ + path: string + value: unknown + type: string + }> = [] + + const extractionQueue: Array<{ + obj: Record + basePath: string + depth: number + }> = [ + { obj: node.model.raw as Record, basePath: '', depth: 0 } + ] + + while (extractionQueue.length > 0) { + const current = extractionQueue.shift()! + const { obj, basePath, depth } = current + + if ( + depth >= DEEP_EXTRACTION_CONFIG.MAX_DEPTH || + !obj || + typeof obj !== 'object' + ) { + continue + } + + for (const [key, value] of Object.entries(obj)) { + if (value === null || value === undefined) { + continue + } + + const fullPath = basePath ? `${basePath}.${key}` : key + + if (shouldExcludeFromFiltering(fullPath)) { + continue + } + + if (typeof value !== 'object' || Array.isArray(value)) { + objProps[fullPath] = value + + pendingPropertyUpdates.push({ + path: fullPath, value, type: typeof value - } as PropertyInfoBase - } - } - } + }) - // Properties.* (one level) - const props = node.model.raw.properties - if (props && typeof props === 'object' && !Array.isArray(props)) { - for (const [key, value] of Object.entries( - props as Record - )) { - if (typeof value !== 'object' || value === null || Array.isArray(value)) { - const path = `properties.${key}` - objProps[path] = value - if (!propertyMap[path]) { - propertyMap[path] = { - concatenatedPath: path, - value, - type: typeof value - } as PropertyInfoBase + if ( + pendingPropertyUpdates.length >= DEEP_EXTRACTION_CONFIG.BATCH_SIZE + ) { + processBatchedPropertyUpdates(pendingPropertyUpdates, propertyMap) + pendingPropertyUpdates.length = 0 } - } - } - } + } else { + const nestedObj = value as Record - // Parameters.*.value (Revit objects) - const params = node.model.raw.parameters - if (params && typeof params === 'object' && !Array.isArray(params)) { - for (const [key, paramObj] of Object.entries( - params as Record - )) { - if ( - paramObj && - typeof paramObj === 'object' && - !Array.isArray(paramObj) - ) { - const param = paramObj as Record - if ('value' in param) { - const path = `parameters.${key}.value` - objProps[path] = param.value - if (!propertyMap[path]) { - propertyMap[path] = { - concatenatedPath: path, - value: param.value, - type: typeof param.value - } as PropertyInfoBase + // Skip common non-filterable object types early for performance + if ( + key === 'displayMesh' || + key === 'renderMaterial' || + key === 'geometry' || + key === 'mesh' || + key === 'vertices' || + key === 'faces' || + key === 'colors' || + key === 'transform' || + key === 'bbox' || + key.startsWith('__') + ) { + continue + } + + if (key === 'parameters') { + for (const [paramKey, paramObj] of Object.entries(nestedObj)) { + if ( + paramObj && + typeof paramObj === 'object' && + !Array.isArray(paramObj) + ) { + const param = paramObj as Record + if ('value' in param) { + const paramPath = `${fullPath}.${paramKey}.value` + if (!shouldExcludeFromFiltering(paramPath)) { + objProps[paramPath] = param.value + pendingPropertyUpdates.push({ + path: paramPath, + value: param.value, + type: typeof param.value + }) + } + } + extractionQueue.push({ + obj: param, + basePath: `${fullPath}.${paramKey}`, + depth: depth + 2 + }) + } } + } else { + extractionQueue.push({ + obj: nestedObj, + basePath: fullPath, + depth: depth + 1 + }) } } } } + if (pendingPropertyUpdates.length > 0) { + processBatchedPropertyUpdates(pendingPropertyUpdates, propertyMap) + } + objectProperties[objectId] = objProps } return true diff --git a/packages/frontend-2/lib/viewer/helpers/filters/constants.ts b/packages/frontend-2/lib/viewer/helpers/filters/constants.ts index d3e16cea7..02d433606 100644 --- a/packages/frontend-2/lib/viewer/helpers/filters/constants.ts +++ b/packages/frontend-2/lib/viewer/helpers/filters/constants.ts @@ -60,3 +60,8 @@ export const getConditionsForType = (filterType: FilterType): FilterCondition[] export const getConditionLabel = (condition: FilterCondition): string => { return FILTER_CONDITION_CONFIG[condition]?.label || 'is' } + +export const DEEP_EXTRACTION_CONFIG = { + MAX_DEPTH: 10, // Maximum nesting depth + BATCH_SIZE: 100 // Batch size for property map updates +} diff --git a/packages/frontend-2/lib/viewer/helpers/filters/utils.ts b/packages/frontend-2/lib/viewer/helpers/filters/utils.ts index ea7dd3090..76c7a9646 100644 --- a/packages/frontend-2/lib/viewer/helpers/filters/utils.ts +++ b/packages/frontend-2/lib/viewer/helpers/filters/utils.ts @@ -40,7 +40,8 @@ export const shouldExcludeFromFiltering = (key: string): boolean => { key.includes('.materialCategory') || key.includes('displayStyle') || key.includes('displayValue') || - key.includes('displayMesh') + key.includes('displayMesh') || + key.startsWith('__') ) { return true }