From ae458137f9cffa7bd29c7a665b891c565ef4e7bc Mon Sep 17 00:00:00 2001 From: andrewwallacespeckle Date: Tue, 26 Aug 2025 15:34:40 +0100 Subject: [PATCH] improvements --- .../lib/viewer/composables/filtering.ts | 179 ++++++++++++------ .../lib/viewer/helpers/filters/types.ts | 20 +- 2 files changed, 125 insertions(+), 74 deletions(-) diff --git a/packages/frontend-2/lib/viewer/composables/filtering.ts b/packages/frontend-2/lib/viewer/composables/filtering.ts index 44cc21753..ef6b07c60 100644 --- a/packages/frontend-2/lib/viewer/composables/filtering.ts +++ b/packages/frontend-2/lib/viewer/composables/filtering.ts @@ -88,7 +88,6 @@ function createFilteringDataStore() { const objectMap: Record = {} const propertyMap: Record = {} - const propertyIndex: Record> = {} await tree.walkAsync((node: TreeNode) => { if ( @@ -104,17 +103,6 @@ function createFilteringDataStore() { const props = extractNestedProperties(node.model.raw) for (const p of props) { propertyMap[p.concatenatedPath] = p - - const propertyKey = p.concatenatedPath - const value = String(p.value) - - if (!propertyIndex[propertyKey]) { - propertyIndex[propertyKey] = {} - } - if (!propertyIndex[propertyKey][value]) { - propertyIndex[propertyKey][value] = [] - } - propertyIndex[propertyKey][value].push(objectId) } } return true @@ -127,19 +115,59 @@ function createFilteringDataStore() { viewerInstance: markRaw(viewer), rootObject: rootObject ? markRaw(rootObject) : null, objectMap: markRaw(objectMap), - propertyMap, - propertyIndex: markRaw(propertyIndex) + propertyMap + // _propertyIndexCache will be built on-demand } } } + /** + * Build property index on-demand for a specific property key + */ + const buildPropertyIndex = ( + dataSource: DataSource, + propertyKey: string + ): Record => { + // Check if already cached + if (!dataSource._propertyIndexCache) { + dataSource._propertyIndexCache = {} + } + + if (dataSource._propertyIndexCache[propertyKey]) { + return dataSource._propertyIndexCache[propertyKey] + } + + // Build index for this property + const propertyIndex: Record = {} + + for (const [objectId, speckleObject] of Object.entries(dataSource.objectMap)) { + const props = extractNestedProperties(speckleObject as SpeckleObject) + + for (const p of props) { + if (p.concatenatedPath === propertyKey) { + const value = String(p.value) + + if (!propertyIndex[value]) { + propertyIndex[value] = [] + } + propertyIndex[value].push(objectId) + } + } + } + + // Cache the result + dataSource._propertyIndexCache[propertyKey] = propertyIndex + return propertyIndex + } + const queryObjects = (criteria: QueryCriteria): string[] => { const matchingIds: string[] = [] for (const dataSource of dataSources.value) { - const propertyIndex = dataSource.propertyIndex[criteria.propertyKey] + // Build index on-demand for this specific property + const propertyIndex = buildPropertyIndex(dataSource, criteria.propertyKey) - if (!propertyIndex) { + if (!propertyIndex || Object.keys(propertyIndex).length === 0) { continue } @@ -201,47 +229,60 @@ function createFilteringDataStore() { return matchingIds } + // Cascades the intersected object ids from the previous slice to the current slice const computeSliceIntersections = () => { if (dataSlices.value.length < 1) return - if (dataSlices.value.length === 1) { - dataSlices.value[0]!.intersectedObjectIds = [...dataSlices.value[0]!.objectIds] - return - } - - let intersection = new Set(dataSlices.value[0]!.objectIds) + // First slice gets all its objects + dataSlices.value[0]!.intersectedObjectIds = [...dataSlices.value[0]!.objectIds] + // Each subsequent slice intersects with the previous slice for (let i = 1; i < dataSlices.value.length; i++) { - const currentSliceIds = new Set(dataSlices.value[i]!.objectIds) - intersection = new Set([...intersection].filter((id) => currentSliceIds.has(id))) - } - - const intersectionArray = Array.from(intersection) - - for (const slice of dataSlices.value) { - slice.intersectedObjectIds = intersectionArray + const prevSlice = dataSlices.value[i - 1]! + const currentSlice = dataSlices.value[i]! + currentSlice.intersectedObjectIds = currentSlice.objectIds.filter((id) => + prevSlice.intersectedObjectIds!.includes(id) + ) } } const pushOrReplaceSlice = (dataSlice: DataSlice) => { - const existingIndex = dataSlices.value.findIndex( - (slice) => slice.id === dataSlice.id + const sliceByWidgetIdIndex = dataSlices.value.findIndex( + (slice) => slice.widgetId === dataSlice.widgetId ) - if (existingIndex === -1) { + if (sliceByWidgetIdIndex === -1) { + // If it's not present, push dataSlices.value.push(dataSlice) } else { - dataSlices.value[existingIndex] = dataSlice + // If it's the same name, toggle off (remove slice) + if (dataSlices.value[sliceByWidgetIdIndex]!.name === dataSlice.name) { + popSlice(dataSlice) + return + } else { + // Replace slice if it's a new slice coming from an existing widget + dataSlices.value[sliceByWidgetIdIndex] = dataSlice + } } computeSliceIntersections() } - const popSlice = (dataSlice: DataSlice) => { - const existingIndex = dataSlices.value.findIndex( - (slice) => slice.id === dataSlice.id + const replaceExistingSlice = (dataSlice: DataSlice) => { + const sliceByWidgetIdIndex = dataSlices.value.findIndex( + (slice) => slice.widgetId === dataSlice.widgetId ) - if (existingIndex !== -1) { - dataSlices.value.splice(existingIndex, 1) + if (sliceByWidgetIdIndex !== -1) { + dataSlices.value[sliceByWidgetIdIndex] = dataSlice + computeSliceIntersections() + } + } + + const popSlice = (dataSlice: DataSlice) => { + const sliceByWidgetIdIndex = dataSlices.value.findIndex( + (slice) => slice.widgetId === dataSlice.widgetId + ) + if (sliceByWidgetIdIndex !== -1) { + dataSlices.value.splice(sliceByWidgetIdIndex, 1) computeSliceIntersections() } } @@ -250,6 +291,7 @@ function createFilteringDataStore() { if (dataSlices.value.length === 0) return [] if (currentFilterLogic.value === FilterLogic.Any) { + // Union: combine all objects from all slices const allObjectIds = new Set() for (const slice of dataSlices.value) { if (slice.objectIds && Array.isArray(slice.objectIds)) { @@ -262,6 +304,7 @@ function createFilteringDataStore() { } return Array.from(allObjectIds) } else { + // Intersection: use cascading intersections from last slice const lastSlice = dataSlices.value[dataSlices.value.length - 1] return lastSlice?.intersectedObjectIds || [] } @@ -272,6 +315,14 @@ function createFilteringDataStore() { dataSlices.value = [] } + const clearPropertyIndexCache = () => { + for (const dataSource of dataSources.value) { + if (dataSource._propertyIndexCache) { + dataSource._propertyIndexCache = {} + } + } + } + const setFilterLogic = (logic: FilterLogic) => { currentFilterLogic.value = logic } @@ -280,9 +331,12 @@ function createFilteringDataStore() { populateDataStore, queryObjects, pushOrReplaceSlice, + replaceExistingSlice, popSlice, + computeSliceIntersections, finalObjectIds, clearDataOnRouteLeave, + clearPropertyIndexCache, setFilterLogic, currentFilterLogic, dataSlices @@ -363,9 +417,12 @@ export function useFilterUtilities( withWatchersDisabled(() => { const filteringExtension = instance.getExtension(FilteringExtension) if (newObjectIds.length > 0) { - filteringExtension.isolateObjects(newObjectIds, 'utilities', true, true) - filters.hiddenObjectIds.value = [] - filters.isolatedObjectIds.value = newObjectIds + filteringExtension.isolateObjects( + newObjectIds, + 'property-filters', + true, + true + ) } else { // Preserve color filter when clearing isolation const currentColorFilterId = filters.activeColorFilterId.value @@ -388,9 +445,6 @@ export function useFilterUtilities( filteringExtension.setColorFilter(activeColorFilter) filters.activeColorFilterId.value = currentColorFilterId } - - filters.isolatedObjectIds.value = [] - filters.hiddenObjectIds.value = [] } }) }, @@ -409,7 +463,7 @@ export function useFilterUtilities( ...objectIds ]) const filteringExtension = viewer.instance.getExtension(FilteringExtension) - filteringExtension.isolateObjects(objectIds, 'utilities', true, true) + filteringExtension.isolateObjects(objectIds, 'manual-isolation', true, true) } const unIsolateObjects = (objectIds: string[]) => { @@ -418,7 +472,7 @@ export function useFilterUtilities( objectIds ) const filteringExtension = viewer.instance.getExtension(FilteringExtension) - filteringExtension.unIsolateObjects(objectIds, 'utilities', true, true) + filteringExtension.unIsolateObjects(objectIds, 'manual-isolation', true, true) } const hideObjects = ( @@ -432,13 +486,13 @@ export function useFilterUtilities( ...objectIds ]) const filteringExtension = viewer.instance.getExtension(FilteringExtension) - filteringExtension.hideObjects(objectIds, 'utilities', false, false) + filteringExtension.hideObjects(objectIds, 'manual-hiding', false, false) } const showObjects = (objectIds: string[]) => { filters.hiddenObjectIds.value = difference(filters.hiddenObjectIds.value, objectIds) const filteringExtension = viewer.instance.getExtension(FilteringExtension) - filteringExtension.showObjects(objectIds, 'utilities', false) + filteringExtension.showObjects(objectIds, 'manual-hiding', false) } /** @@ -597,11 +651,11 @@ export function useFilterUtilities( * Updates data store slices based on current filter state */ const updateDataStoreSlices = () => { - // Clear existing filter slices - const existingSlices = dataStore.dataSlices.value.filter((slice) => - slice.id.startsWith('filter-') + // Clear existing filter slices manually to avoid widgetId lookup issues + dataStore.dataSlices.value = dataStore.dataSlices.value.filter( + (slice) => !slice.id.startsWith('filter-') ) - existingSlices.forEach((slice) => dataStore.popSlice(slice)) + dataStore.computeSliceIntersections() // Create new slices for active filters filters.propertyFilters.value.forEach((filter) => { @@ -620,6 +674,7 @@ export function useFilterUtilities( const slice: DataSlice = { id: `filter-${filter.id}`, + widgetId: filter.id, name: `${getPropertyName(filter.filter.key)} ${getConditionLabel( filter.condition )} (${filter.numericRange.min.toFixed(2)} - ${filter.numericRange.max.toFixed( @@ -640,6 +695,7 @@ export function useFilterUtilities( const slice: DataSlice = { id: `filter-${filter.id}`, + widgetId: filter.id, name: `${getPropertyName(filter.filter.key)} ${ filter.condition === StringFilterCondition.Is ? 'is' : 'is not' } ${filter.selectedValues.join(', ')}`, @@ -683,15 +739,24 @@ export function useFilterUtilities( const resetFilters = () => { // Clear all filter state - filters.hiddenObjectIds.value = [] - filters.isolatedObjectIds.value = [] filters.propertyFilters.value = [] filters.selectedObjects.value = [] filters.activeColorFilterId.value = null - // Clear all viewer filters including colors + // Clear all filter slices - this will make finalObjectIds = [] and trigger watch + const nonFilterSlices = dataStore.dataSlices.value.filter( + (slice) => !slice.id.startsWith('filter-') + ) + dataStore.dataSlices.value = nonFilterSlices + + // Only compute intersections if there are slices remaining + if (nonFilterSlices.length > 0) { + dataStore.computeSliceIntersections() + } + + // The watch should handle filteringExtension.resetFilters() when finalObjectIds becomes [] + // But also clear color filters explicitly since they're handled separately const filteringExtension = viewer.instance.getExtension(FilteringExtension) - filteringExtension.resetFilters() filteringExtension.removeColorFilter() } diff --git a/packages/frontend-2/lib/viewer/helpers/filters/types.ts b/packages/frontend-2/lib/viewer/helpers/filters/types.ts index 687eb6c9f..931cca859 100644 --- a/packages/frontend-2/lib/viewer/helpers/filters/types.ts +++ b/packages/frontend-2/lib/viewer/helpers/filters/types.ts @@ -46,10 +46,6 @@ export const getConditionLabel = (condition: FilterCondition): string => { return CONDITION_CONFIG[condition]?.label || 'is' } -export type FilterConditionLabel = { - [key in FilterCondition]: string -} - export enum FilterLogic { All = 'all', Any = 'any' @@ -118,6 +114,7 @@ export type PropertyInfoBase = { export type DataSlice = { id: string + widgetId: string // Links to filter/widget component name: string objectIds: string[] intersectedObjectIds?: string[] @@ -137,7 +134,8 @@ export type DataSource = { rootObject: unknown | null // Marked as raw in Vue, using unknown instead of any objectMap: Record // Marked as raw in Vue, using unknown instead of any propertyMap: Record - propertyIndex: Record> // Property index structure + // Lazy property index - built on-demand during filtering + _propertyIndexCache?: Record> } export type ResourceInfo = { @@ -151,18 +149,6 @@ export type CreateFilterParams = { availableValues: string[] } -// Helper type for numeric range updates -export type NumericRangeUpdate = { - min: number - max: number -} - -// Helper type for condition selection events -export type ConditionSelectionEvent = { - value: FilterCondition - label: string -} - // Helper type for filter logic selection events export type FilterLogicOption = { value: FilterLogic