pr comments. refactor
This commit is contained in:
@@ -86,11 +86,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
useInjectedViewerInterfaceState,
|
||||
useInjectedViewer,
|
||||
useInjectedViewerState
|
||||
} from '~~/lib/viewer/composables/setup'
|
||||
import { useInjectedViewerInterfaceState } from '~~/lib/viewer/composables/setup'
|
||||
import type { PropertySelectOption } from '~/lib/viewer/helpers/filters/types'
|
||||
import { FilterType } from '~/lib/viewer/helpers/filters/types'
|
||||
import { useMixpanel } from '~~/lib/core/composables/mp'
|
||||
@@ -111,9 +107,6 @@ const {
|
||||
setFilterLogic
|
||||
} = useFilterUtilities()
|
||||
|
||||
const dataStore = useFilteringDataStore()
|
||||
const { instance } = useInjectedViewer()
|
||||
const { resourceItems } = useInjectedViewerState().resources.response
|
||||
const { currentFilterLogic } = useFilteringDataStore()
|
||||
const { filteredObjectsCount } = useFilteredObjectsCount()
|
||||
const mp = useMixpanel()
|
||||
@@ -128,6 +121,10 @@ const filtersContainerRef = ref<HTMLElement>()
|
||||
const shouldScrollToNewFilter = ref(false)
|
||||
|
||||
const propertySelectOptions = computed((): PropertySelectOption[] => {
|
||||
if (!showPropertySelection.value) {
|
||||
return []
|
||||
}
|
||||
|
||||
const existingFilterKeys = new Set(
|
||||
propertyFilters.value.map((f) => f.filter?.key).filter(Boolean)
|
||||
)
|
||||
@@ -137,28 +134,34 @@ const propertySelectOptions = computed((): PropertySelectOption[] => {
|
||||
const allOptions: PropertySelectOption[] = relevantFilters
|
||||
.filter((filter) => !existingFilterKeys.has(filter.key))
|
||||
.map((filter) => {
|
||||
const pathParts = filter.key.split('.')
|
||||
const propertyName = pathParts[pathParts.length - 1]
|
||||
const parentPath = pathParts.slice(0, -1).join('.')
|
||||
const lastDotIndex = filter.key.lastIndexOf('.')
|
||||
const propertyName =
|
||||
lastDotIndex === -1 ? filter.key : filter.key.slice(lastDotIndex + 1)
|
||||
const parentPath = lastDotIndex === -1 ? '' : filter.key.slice(0, lastDotIndex)
|
||||
|
||||
return {
|
||||
value: filter.key,
|
||||
label: propertyName,
|
||||
parentPath,
|
||||
type: filter.type === 'number' ? FilterType.Numeric : FilterType.String,
|
||||
hasParent: parentPath.length > 0
|
||||
hasParent: lastDotIndex !== -1
|
||||
}
|
||||
})
|
||||
|
||||
// Use a more efficient sorting approach
|
||||
const sortedOptions = allOptions.sort((a, b) => {
|
||||
if (!a.hasParent && b.hasParent) return -1
|
||||
if (a.hasParent && !b.hasParent) return 1
|
||||
// First sort by whether they have parents (no-parent items first)
|
||||
if (a.hasParent !== b.hasParent) {
|
||||
return a.hasParent ? 1 : -1
|
||||
}
|
||||
|
||||
// If both have parents, sort by parent path first
|
||||
if (a.hasParent && b.hasParent) {
|
||||
const parentComparison = a.parentPath.localeCompare(b.parentPath)
|
||||
if (parentComparison !== 0) return parentComparison
|
||||
}
|
||||
|
||||
// Finally sort by label
|
||||
return a.label.localeCompare(b.label)
|
||||
})
|
||||
|
||||
@@ -244,26 +247,6 @@ onKeyStroke('Escape', () => {
|
||||
}
|
||||
})
|
||||
|
||||
// Populate data store on mount to avoid delay when clicking plus button
|
||||
onMounted(async () => {
|
||||
if (dataStore.dataSources.value.length === 0 && resourceItems.value.length > 0) {
|
||||
const tree = instance.getWorldTree()
|
||||
if (tree) {
|
||||
const availableResources = resourceItems.value.filter((item) => {
|
||||
const nodes = tree.findId(item.objectId)
|
||||
return nodes && nodes.length > 0
|
||||
})
|
||||
|
||||
if (availableResources.length > 0) {
|
||||
const resources = availableResources.map((item) => ({
|
||||
resourceUrl: item.objectId
|
||||
}))
|
||||
await dataStore.populateDataStore(instance, resources)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Watch for new filters being added and scroll when needed
|
||||
watch(
|
||||
() => propertyFilters.value.length,
|
||||
|
||||
@@ -79,19 +79,35 @@ const updateDebouncedSearch = useDebounceFn((query: string) => {
|
||||
debouncedSearchQuery.value = query
|
||||
}, 200)
|
||||
|
||||
// Pre-compute lowercase versions for efficient searching
|
||||
const optionsWithLowercase = computed(() => {
|
||||
return props.options.map((option) => ({
|
||||
...option,
|
||||
_searchLabel: option.label.toLowerCase(),
|
||||
_searchValue: option.value.toLowerCase(),
|
||||
_searchParentPath: option.parentPath.toLowerCase(),
|
||||
_searchType: option.type.toLowerCase()
|
||||
}))
|
||||
})
|
||||
|
||||
const filteredOptions = computed(() => {
|
||||
if (!debouncedSearchQuery.value.trim()) {
|
||||
return props.options
|
||||
}
|
||||
|
||||
const searchTerm = debouncedSearchQuery.value.toLowerCase().trim()
|
||||
return props.options.filter(
|
||||
(option) =>
|
||||
option.label.toLowerCase().includes(searchTerm) ||
|
||||
option.value.toLowerCase().includes(searchTerm) ||
|
||||
option.parentPath.toLowerCase().includes(searchTerm) ||
|
||||
option.type.toLowerCase().includes(searchTerm)
|
||||
)
|
||||
return optionsWithLowercase.value
|
||||
.filter(
|
||||
(option) =>
|
||||
option._searchLabel.includes(searchTerm) ||
|
||||
option._searchValue.includes(searchTerm) ||
|
||||
option._searchParentPath.includes(searchTerm) ||
|
||||
option._searchType.includes(searchTerm)
|
||||
)
|
||||
.map(
|
||||
({ _searchLabel, _searchValue, _searchParentPath, _searchType, ...option }) =>
|
||||
option
|
||||
)
|
||||
})
|
||||
|
||||
const listItems = computed((): PropertySelectionListItem[] => {
|
||||
@@ -105,8 +121,10 @@ const listItems = computed((): PropertySelectionListItem[] => {
|
||||
return searchResults
|
||||
}
|
||||
|
||||
// Create a map for O(1) lookup instead of O(n) find operations
|
||||
const optionsMap = new Map(props.options.map((opt) => [opt.value, opt]))
|
||||
const availablePopular = FILTERS_POPULAR_PROPERTIES.map((filterKey) =>
|
||||
props.options.find((opt) => opt.value === filterKey)
|
||||
optionsMap.get(filterKey)
|
||||
)
|
||||
.filter(Boolean)
|
||||
.slice(0, 6) // Show max 6 popular filters
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { SpeckleObject, TreeNode, Viewer } from '@speckle/viewer'
|
||||
import { uniq, flatten, isEmpty, compact } from 'lodash-es'
|
||||
import { uniq, flatten, compact } from 'lodash-es'
|
||||
import {
|
||||
FilterLogic,
|
||||
NumericFilterCondition,
|
||||
@@ -21,39 +21,6 @@ export function createViewerFilteringDataStore() {
|
||||
const currentFilterLogic = ref<FilterLogic>(FilterLogic.All)
|
||||
const dataSlices: Ref<DataSlice[]> = ref([])
|
||||
|
||||
const propertyExtractionCache = new Map<Record<string, unknown>, PropertyInfoBase[]>()
|
||||
|
||||
const extractNestedProperties = (
|
||||
obj: Record<string, unknown>
|
||||
): PropertyInfoBase[] => {
|
||||
if (propertyExtractionCache.has(obj)) {
|
||||
return propertyExtractionCache.get(obj)!
|
||||
}
|
||||
|
||||
const properties: PropertyInfoBase[] = []
|
||||
|
||||
function traverse(current: Record<string, unknown>, path: string[] = []) {
|
||||
for (const [key, value] of Object.entries(current)) {
|
||||
const currentPath = [...path, key]
|
||||
const concatenatedPath = currentPath.join('.')
|
||||
|
||||
if (value !== null && typeof value === 'object' && !Array.isArray(value)) {
|
||||
traverse(value as Record<string, unknown>, currentPath)
|
||||
} else {
|
||||
properties.push({
|
||||
concatenatedPath,
|
||||
value,
|
||||
type: typeof value
|
||||
} as PropertyInfoBase)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
traverse(obj)
|
||||
propertyExtractionCache.set(obj, properties)
|
||||
return properties
|
||||
}
|
||||
|
||||
const populateDataStore = async (viewer: Viewer, resources: ResourceInfo[]) => {
|
||||
const tree = viewer.getWorldTree()
|
||||
if (!tree) return
|
||||
@@ -67,36 +34,86 @@ export function createViewerFilteringDataStore() {
|
||||
|
||||
const objectMap: Record<string, SpeckleObject> = {}
|
||||
const propertyMap: Record<string, PropertyInfoBase> = {}
|
||||
const propertyIndexCache: Record<string, Record<string, string[]>> = {}
|
||||
// Map from objectId to its property values for efficient filtering
|
||||
const objectProperties: Record<string, Record<string, unknown>> = {}
|
||||
|
||||
await tree.walkAsync((node: TreeNode) => {
|
||||
if (
|
||||
node.model.atomic &&
|
||||
node.model.raw.id &&
|
||||
node.model.raw.id.length === 32 &&
|
||||
node.model.id &&
|
||||
node.model.id.length === 32 &&
|
||||
!node.model.raw.speckle_type?.includes('Proxy') &&
|
||||
node.model.raw.properties?.builtInCategory !== 'OST_Levels'
|
||||
) {
|
||||
const objectId = node.model.raw.id
|
||||
const objectId = node.model.id
|
||||
objectMap[objectId] = node.model.raw as SpeckleObject
|
||||
|
||||
const props = extractNestedProperties(node.model.raw)
|
||||
for (const p of props) {
|
||||
propertyMap[p.concatenatedPath] = p
|
||||
// Extract only commonly used properties - avoid expensive full traversal
|
||||
const objProps: Record<string, unknown> = {}
|
||||
|
||||
const propertyKey = p.concatenatedPath
|
||||
const value = String(p.value)
|
||||
|
||||
if (!propertyIndexCache[propertyKey]) {
|
||||
propertyIndexCache[propertyKey] = {}
|
||||
// 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,
|
||||
value,
|
||||
type: typeof value
|
||||
} as PropertyInfoBase
|
||||
}
|
||||
}
|
||||
|
||||
if (!propertyIndexCache[propertyKey][value]) {
|
||||
propertyIndexCache[propertyKey][value] = []
|
||||
}
|
||||
|
||||
propertyIndexCache[propertyKey][value].push(objectId)
|
||||
}
|
||||
|
||||
// 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<string, unknown>
|
||||
)) {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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<string, unknown>
|
||||
)) {
|
||||
if (
|
||||
paramObj &&
|
||||
typeof paramObj === 'object' &&
|
||||
!Array.isArray(paramObj)
|
||||
) {
|
||||
const param = paramObj as Record<string, unknown>
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
objectProperties[objectId] = objProps
|
||||
}
|
||||
return true
|
||||
}, subnode)
|
||||
@@ -109,54 +126,58 @@ export function createViewerFilteringDataStore() {
|
||||
rootObject: rootObject ? markRaw(rootObject) : null,
|
||||
objectMap: markRaw(objectMap),
|
||||
propertyMap,
|
||||
_propertyIndexCache: propertyIndexCache
|
||||
objectProperties
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const buildPropertyIndex = (
|
||||
dataSource: DataSource,
|
||||
propertyKey: string
|
||||
): Record<string, string[]> => {
|
||||
if (dataSource._propertyIndexCache && dataSource._propertyIndexCache[propertyKey]) {
|
||||
return dataSource._propertyIndexCache[propertyKey]
|
||||
}
|
||||
return {}
|
||||
}
|
||||
|
||||
const queryObjects = (criteria: QueryCriteria): string[] => {
|
||||
const matchingIds: string[] = []
|
||||
|
||||
const PRECISION = 4 // matches 0.0001 step
|
||||
|
||||
for (const dataSource of dataSources.value) {
|
||||
const propertyIndex = buildPropertyIndex(dataSource, criteria.propertyKey)
|
||||
|
||||
if (!propertyIndex || isEmpty(propertyIndex)) {
|
||||
// Check if property exists in propertyMap
|
||||
const propertyInfo = dataSource.propertyMap[criteria.propertyKey]
|
||||
if (!propertyInfo) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (criteria.condition === ExistenceFilterCondition.IsSet) {
|
||||
matchingIds.push(...flatten(Object.values(propertyIndex)))
|
||||
} else if (criteria.condition === ExistenceFilterCondition.IsNotSet) {
|
||||
const objectsWithProperty = new Set<string>(
|
||||
flatten(Object.values(propertyIndex))
|
||||
)
|
||||
|
||||
for (const [objectId] of Object.entries(dataSource.objectMap)) {
|
||||
if (!objectsWithProperty.has(objectId)) {
|
||||
// Find all objects that have this property - use pre-computed objectProperties
|
||||
for (const [objectId, objProps] of Object.entries(
|
||||
dataSource.objectProperties
|
||||
)) {
|
||||
const hasProperty = criteria.propertyKey in objProps
|
||||
if (hasProperty) {
|
||||
matchingIds.push(objectId)
|
||||
}
|
||||
}
|
||||
} else if (criteria.minValue !== undefined || criteria.maxValue !== undefined) {
|
||||
const minValue = criteria.minValue ?? -Infinity
|
||||
const maxValue = criteria.maxValue ?? Infinity
|
||||
} else if (criteria.condition === ExistenceFilterCondition.IsNotSet) {
|
||||
// Find all objects that don't have this property
|
||||
for (const [objectId, objProps] of Object.entries(
|
||||
dataSource.objectProperties
|
||||
)) {
|
||||
const hasProperty = criteria.propertyKey in objProps
|
||||
if (!hasProperty) {
|
||||
matchingIds.push(objectId)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// For value-based filtering, check each object
|
||||
for (const [objectId, objProps] of Object.entries(
|
||||
dataSource.objectProperties
|
||||
)) {
|
||||
const value = objProps[criteria.propertyKey]
|
||||
if (value === undefined) continue
|
||||
let shouldInclude = false
|
||||
|
||||
for (const [value, objectIds] of Object.entries(propertyIndex)) {
|
||||
const numericValue = Number(value)
|
||||
if (!isNaN(numericValue)) {
|
||||
// Only round for display purposes, not for filtering logic
|
||||
let shouldInclude = false
|
||||
if (criteria.minValue !== undefined || criteria.maxValue !== undefined) {
|
||||
// Numeric filtering
|
||||
const numericValue = Number(value)
|
||||
if (isNaN(numericValue)) continue
|
||||
|
||||
const minValue = criteria.minValue ?? -Infinity
|
||||
const maxValue = criteria.maxValue ?? Infinity
|
||||
|
||||
switch (criteria.condition) {
|
||||
case NumericFilterCondition.IsBetween:
|
||||
@@ -169,13 +190,11 @@ export function createViewerFilteringDataStore() {
|
||||
shouldInclude = numericValue < maxValue
|
||||
break
|
||||
case NumericFilterCondition.IsEqualTo: {
|
||||
// For equality, use a small tolerance to account for floating-point precision
|
||||
const tolerance = Math.pow(10, -PRECISION)
|
||||
shouldInclude = Math.abs(numericValue - minValue) <= tolerance
|
||||
break
|
||||
}
|
||||
case NumericFilterCondition.IsNotEqualTo: {
|
||||
// For inequality, use a small tolerance to account for floating-point precision
|
||||
const tolerance = Math.pow(10, -PRECISION)
|
||||
shouldInclude = Math.abs(numericValue - minValue) > tolerance
|
||||
break
|
||||
@@ -183,28 +202,16 @@ export function createViewerFilteringDataStore() {
|
||||
default:
|
||||
shouldInclude = numericValue >= minValue && numericValue <= maxValue
|
||||
}
|
||||
} else if (criteria.condition === StringFilterCondition.Is) {
|
||||
// String filtering - exact match
|
||||
shouldInclude = criteria.values.includes(String(value))
|
||||
} else if (criteria.condition === StringFilterCondition.IsNot) {
|
||||
// String filtering - exclude values
|
||||
shouldInclude = !criteria.values.includes(String(value))
|
||||
}
|
||||
|
||||
if (shouldInclude) {
|
||||
matchingIds.push(...objectIds)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (criteria.condition === StringFilterCondition.Is) {
|
||||
for (const value of criteria.values) {
|
||||
const objectIds = propertyIndex[value]
|
||||
if (objectIds) {
|
||||
matchingIds.push(...objectIds)
|
||||
}
|
||||
}
|
||||
} else if (criteria.condition === StringFilterCondition.IsNot) {
|
||||
if (criteria.values.length === 0) {
|
||||
// Return empty array - nothing matches "is not" with no exclusions
|
||||
} else {
|
||||
const excludeValues = new Set(criteria.values)
|
||||
for (const [value, objectIds] of Object.entries(propertyIndex)) {
|
||||
if (!excludeValues.has(value)) {
|
||||
matchingIds.push(...objectIds)
|
||||
}
|
||||
if (shouldInclude) {
|
||||
matchingIds.push(objectId)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -272,17 +279,18 @@ export function createViewerFilteringDataStore() {
|
||||
.map((slice) => slice.objectIds)
|
||||
)
|
||||
)
|
||||
return uniq(validObjectIds)
|
||||
const result = uniq(validObjectIds)
|
||||
return result
|
||||
} else {
|
||||
const lastSlice = dataSlices.value[dataSlices.value.length - 1]
|
||||
return lastSlice?.intersectedObjectIds || []
|
||||
const result = lastSlice?.intersectedObjectIds || []
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
const clearDataOnRouteLeave = () => {
|
||||
dataSourcesMap.value = {}
|
||||
dataSlices.value = []
|
||||
propertyExtractionCache.clear()
|
||||
}
|
||||
|
||||
const setFilterLogic = (logic: FilterLogic) => {
|
||||
@@ -301,8 +309,7 @@ export function createViewerFilteringDataStore() {
|
||||
setFilterLogic,
|
||||
currentFilterLogic,
|
||||
dataSlices,
|
||||
dataSources,
|
||||
buildPropertyIndex
|
||||
dataSources
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -83,18 +83,24 @@ export function useFilterUtilities(
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets ALL values for a property from pre-computed indices (used for filtering logic)
|
||||
* Gets ALL values for a property using pre-computed data (used for filtering logic)
|
||||
*/
|
||||
const getAllPropertyValues = (propertyKey: string): string[] => {
|
||||
const allValues: string[] = []
|
||||
|
||||
for (const dataSource of dataStore.dataSources.value) {
|
||||
if (
|
||||
dataSource._propertyIndexCache &&
|
||||
dataSource._propertyIndexCache[propertyKey]
|
||||
) {
|
||||
const propertyIndex = dataSource._propertyIndexCache[propertyKey]
|
||||
allValues.push(...Object.keys(propertyIndex))
|
||||
// Check if property exists in propertyMap first (quick check)
|
||||
const propertyInfo = dataSource.propertyMap[propertyKey]
|
||||
if (!propertyInfo) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Collect values from pre-computed object properties
|
||||
for (const [, objProps] of Object.entries(dataSource.objectProperties)) {
|
||||
const value = objProps[propertyKey]
|
||||
if (value !== undefined) {
|
||||
allValues.push(String(value))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -130,11 +136,89 @@ export function useFilterUtilities(
|
||||
return []
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes full property data for a given property key (min/max, valueGroups)
|
||||
*/
|
||||
const computeFullPropertyData = (propertyKey: string): PropertyInfo | null => {
|
||||
const valueToObjectIds = new Map<string, string[]>()
|
||||
|
||||
for (const dataSource of dataStore.dataSources.value) {
|
||||
// Check if property exists in this data source
|
||||
if (!dataSource.propertyMap[propertyKey]) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Collect values and their associated object IDs using pre-computed data
|
||||
for (const [objectId, objProps] of Object.entries(dataSource.objectProperties)) {
|
||||
const value = objProps[propertyKey]
|
||||
if (value !== undefined) {
|
||||
const stringValue = String(value)
|
||||
if (!valueToObjectIds.has(stringValue)) {
|
||||
valueToObjectIds.set(stringValue, [])
|
||||
}
|
||||
valueToObjectIds.get(stringValue)!.push(objectId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (valueToObjectIds.size === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
const uniqueValues = Array.from(valueToObjectIds.keys())
|
||||
const firstValue = uniqueValues[0]
|
||||
const isNumeric =
|
||||
typeof firstValue === 'number' ||
|
||||
(!isNaN(Number(firstValue)) && String(firstValue) !== '')
|
||||
|
||||
if (isNumeric) {
|
||||
const numericValues = uniqueValues.map((v) => Number(v)).filter((v) => !isNaN(v))
|
||||
const min = Math.min(...numericValues)
|
||||
const max = Math.max(...numericValues)
|
||||
|
||||
const numericValueGroups: { value: number; id: string }[] = []
|
||||
for (const value of uniqueValues) {
|
||||
const objectIds = valueToObjectIds.get(value) || []
|
||||
for (const objectId of objectIds) {
|
||||
numericValueGroups.push({
|
||||
value: Number(value),
|
||||
id: objectId
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
key: propertyKey,
|
||||
type: 'number',
|
||||
objectCount: valueToObjectIds.size,
|
||||
min,
|
||||
max,
|
||||
valueGroups: numericValueGroups,
|
||||
passMin: null,
|
||||
passMax: null
|
||||
} as NumericPropertyInfo
|
||||
} else {
|
||||
return {
|
||||
key: propertyKey,
|
||||
type: 'string',
|
||||
objectCount: valueToObjectIds.size,
|
||||
valueGroups: uniqueValues.map((value) => ({
|
||||
value: String(value),
|
||||
ids: valueToObjectIds.get(value) || []
|
||||
}))
|
||||
} as StringPropertyInfo
|
||||
}
|
||||
}
|
||||
|
||||
const createFilterData = (params: CreateFilterParams): FilterData => {
|
||||
const { filter, id } = params
|
||||
|
||||
if (isNumericPropertyInfo(filter)) {
|
||||
const numericFilter = filter as NumericPropertyInfo
|
||||
// If the filter doesn't have full data, compute it now
|
||||
const fullFilter =
|
||||
filter.objectCount === 0 ? computeFullPropertyData(filter.key) || filter : filter
|
||||
|
||||
if (isNumericPropertyInfo(fullFilter)) {
|
||||
const numericFilter = fullFilter as NumericPropertyInfo
|
||||
const { min, max } = numericFilter
|
||||
const range = max - min
|
||||
|
||||
@@ -178,7 +262,7 @@ export function useFilterUtilities(
|
||||
selectedValues: [],
|
||||
condition: StringFilterCondition.Is,
|
||||
type: FilterType.String,
|
||||
filter: filter as StringPropertyInfo,
|
||||
filter: fullFilter as StringPropertyInfo,
|
||||
isDefaultAllSelected: true
|
||||
} satisfies StringFilterData
|
||||
}
|
||||
@@ -388,11 +472,11 @@ export function useFilterUtilities(
|
||||
filter.condition === ExistenceFilterCondition.IsSet ||
|
||||
filter.condition === ExistenceFilterCondition.IsNotSet)
|
||||
) {
|
||||
// Handle lazy loading: if isDefaultAllSelected is true and selectedValues is empty,
|
||||
// use all available values from pre-computed indices
|
||||
const values =
|
||||
filter.isDefaultAllSelected && filter.selectedValues.length === 0
|
||||
? getAllPropertyValues(filter.filter.key)
|
||||
? filter.filter.valueGroups
|
||||
?.map((vg) => String(vg.value))
|
||||
.filter((v) => v !== 'null' && v !== 'undefined') || []
|
||||
: filter.selectedValues
|
||||
|
||||
const { condition } = filter
|
||||
@@ -533,43 +617,32 @@ export function useFilterUtilities(
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets property options from the data store
|
||||
* Gets property options from the data store (optimized to use propertyMap)
|
||||
*/
|
||||
const getPropertyOptionsFromDataStore = (): PropertyInfo[] => {
|
||||
const allProperties = new Map<string, PropertyInfo>()
|
||||
|
||||
for (const dataSource of dataStore.dataSources.value) {
|
||||
for (const [propertyKey] of Object.entries(dataSource.propertyMap)) {
|
||||
if (shouldExcludeFromFiltering(propertyKey)) {
|
||||
continue
|
||||
}
|
||||
const propertyKeys = Object.keys(dataSource.propertyMap)
|
||||
|
||||
for (const propertyKey of propertyKeys) {
|
||||
if (allProperties.has(propertyKey)) {
|
||||
continue
|
||||
}
|
||||
|
||||
const propertyIndex = dataSource._propertyIndexCache?.[propertyKey] || {}
|
||||
const values = Object.keys(propertyIndex)
|
||||
|
||||
if (values.length === 0) {
|
||||
continue
|
||||
}
|
||||
|
||||
const firstValue = values[0]
|
||||
const isNumeric = !isNaN(Number(firstValue)) && firstValue !== ''
|
||||
const propertyInfo = dataSource.propertyMap[propertyKey]
|
||||
const value = propertyInfo.value
|
||||
const isNumeric =
|
||||
typeof value === 'number' || (!isNaN(Number(value)) && String(value) !== '')
|
||||
|
||||
if (isNumeric) {
|
||||
const numericValues = values.map((v) => Number(v)).filter((v) => !isNaN(v))
|
||||
const min = Math.min(...numericValues)
|
||||
const max = Math.max(...numericValues)
|
||||
|
||||
allProperties.set(propertyKey, {
|
||||
key: propertyKey,
|
||||
type: 'number',
|
||||
objectCount: values.length,
|
||||
min,
|
||||
max,
|
||||
valueGroups: values.map((value) => ({ value: Number(value), id: '' })), // IDs not needed for selection
|
||||
objectCount: 0,
|
||||
min: 0,
|
||||
max: 0,
|
||||
valueGroups: [],
|
||||
passMin: null,
|
||||
passMax: null
|
||||
} as NumericPropertyInfo)
|
||||
@@ -577,11 +650,8 @@ export function useFilterUtilities(
|
||||
allProperties.set(propertyKey, {
|
||||
key: propertyKey,
|
||||
type: 'string',
|
||||
objectCount: values.length,
|
||||
valueGroups: values.map((value) => ({
|
||||
value,
|
||||
ids: propertyIndex[value] || []
|
||||
}))
|
||||
objectCount: 0, // Not needed for selection
|
||||
valueGroups: []
|
||||
} as StringPropertyInfo)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -153,19 +153,9 @@ export const usePropertyFilteringPostSetup = () => {
|
||||
*/
|
||||
const applyPropertyFilters = () => {
|
||||
const objectIds = dataStore.getFinalObjectIds()
|
||||
const extension = filteringExtension()
|
||||
|
||||
extension.resetFilters()
|
||||
|
||||
const hasAppliedFilters = filters.propertyFilters.value.some(
|
||||
(filter) => filter.isApplied
|
||||
)
|
||||
|
||||
if (objectIds.length > 0) {
|
||||
extension.isolateObjects(objectIds, 'property-filters', false, true)
|
||||
} else if (hasAppliedFilters) {
|
||||
extension.isolateObjects(['no-match-ghost-all'], 'property-filters', false, true)
|
||||
}
|
||||
filters.isolatedObjectIds.value = objectIds
|
||||
filters.filteredObjectsCount.value = objectIds.length
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -181,6 +171,34 @@ export const usePropertyFilteringPostSetup = () => {
|
||||
{ deep: true }
|
||||
)
|
||||
|
||||
/**
|
||||
* Watch for property filter results and apply to viewer extension
|
||||
*/
|
||||
watchTriggerable(
|
||||
() => filters.filteredObjectsCount.value,
|
||||
() => {
|
||||
const extension = filteringExtension()
|
||||
const objectIds = dataStore.getFinalObjectIds()
|
||||
|
||||
extension.resetFilters()
|
||||
|
||||
const hasAppliedFilters = filters.propertyFilters.value.some(
|
||||
(filter) => filter.isApplied
|
||||
)
|
||||
|
||||
if (objectIds.length > 0) {
|
||||
extension.isolateObjects(objectIds, 'property-filters', false, true)
|
||||
} else if (hasAppliedFilters) {
|
||||
extension.isolateObjects(
|
||||
['no-match-ghost-all'],
|
||||
'property-filters',
|
||||
false,
|
||||
true
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* Watch for changes to filter logic (AND/OR)
|
||||
*/
|
||||
|
||||
@@ -61,7 +61,6 @@ import {
|
||||
usePropertyFilteringPostSetup,
|
||||
useManualFilteringPostSetup
|
||||
} from '~/lib/viewer/composables/setup/filters'
|
||||
import { useFilteredObjectsCountPostSetup } from '~/lib/viewer/composables/setup/filteredObjectsCount'
|
||||
import { useFilterUtilities } from '~/lib/viewer/composables/filtering/filtering'
|
||||
import { useFilteringSetup } from '~/lib/viewer/composables/filtering/setup'
|
||||
|
||||
@@ -860,7 +859,7 @@ export function useViewerPostSetup() {
|
||||
useFilterColoringPostSetup()
|
||||
usePropertyFilteringPostSetup()
|
||||
useManualFilteringPostSetup()
|
||||
useFilteredObjectsCountPostSetup()
|
||||
// useFilteredObjectsCountPostSetup() // Disabled - managing count manually in applyPropertyFilters
|
||||
useDisableZoomOnEmbed()
|
||||
useViewerCursorIntegration()
|
||||
useViewerTreeIntegration()
|
||||
|
||||
@@ -35,7 +35,7 @@ export const FILTERS_POPULAR_PROPERTIES = [
|
||||
'phaseCreated',
|
||||
'ifcType',
|
||||
'layer'
|
||||
] as const
|
||||
]
|
||||
|
||||
// UI Constants
|
||||
export const PROPERTY_SELECTION_ITEM_HEIGHT = 36
|
||||
|
||||
@@ -141,7 +141,7 @@ export type DataSource = {
|
||||
rootObject: Nullable<SpeckleObject>
|
||||
objectMap: Record<string, SpeckleObject>
|
||||
propertyMap: Record<string, PropertyInfoBase>
|
||||
_propertyIndexCache?: Record<string, Record<string, string[]>>
|
||||
objectProperties: Record<string, Record<string, unknown>>
|
||||
}
|
||||
|
||||
export type ResourceInfo = {
|
||||
|
||||
Reference in New Issue
Block a user