New Viewer API & big tidy up

This commit is contained in:
andrewwallacespeckle
2025-08-20 13:33:08 +01:00
parent a7586bfd2f
commit 4e0efba217
11 changed files with 150 additions and 409 deletions
@@ -54,7 +54,7 @@ const {
}
} = useInjectedViewerState()
const { isolateObjects, resetFilters, setPropertyFilter, applyPropertyFilter } =
const { isolateObjects, resetFilters, addActiveFilter, toggleFilterApplied } =
useFilterUtilities()
const { setSelectionFromObjectIds, clearSelection } = useSelectionUtilities()
@@ -154,8 +154,8 @@ const setOrUnsetGradient = () => {
if (!computedPropInfo.value) return
metadataGradientIsSet.value = true
setPropertyFilter(computedPropInfo.value)
applyPropertyFilter()
const filterId = addActiveFilter(computedPropInfo.value)
toggleFilterApplied(filterId)
}
const iconAndColor = computed(() => {
@@ -356,12 +356,12 @@ watch(isSmallerOrEqualSm, (newVal) => {
activePanel.value = newVal ? 'none' : 'models'
})
// Auto-open filters panel when a new filter is applied from elsewhere
// Auto-open filters panel when property filters are added
watch(
() => filters.propertyFilter.isApplied.value && filters.propertyFilter.filter.value,
(newFilterApplied, oldFilterApplied) => {
// Only trigger if we're going from no filter to having a filter (not when changing filters or removing)
if (newFilterApplied && !oldFilterApplied) {
() => filters.propertyFilters.value.length,
(newCount, oldCount) => {
// Only trigger if we're adding filters
if (newCount > 0 && (oldCount === 0 || newCount > oldCount)) {
activePanel.value = 'filters'
}
}
@@ -55,7 +55,7 @@
import type { NumericPropertyInfo } from '@speckle/viewer'
import { useFilterUtilities } from '~~/lib/viewer/composables/ui'
const { setPropertyFilter } = useFilterUtilities()
const { addActiveFilter, toggleFilterApplied } = useFilterUtilities()
const props = defineProps<{
filter: NumericPropertyInfo
@@ -77,6 +77,9 @@ const setFilterPass = () => {
const max = Math.max(passMin.value, passMax.value)
propInfo.passMin = min
propInfo.passMax = max
setPropertyFilter(propInfo)
// Add filter using new multi-filter system
const filterId = addActiveFilter(propInfo)
toggleFilterApplied(filterId)
}
</script>
@@ -8,7 +8,7 @@
size="sm"
color="subtle"
tabindex="-1"
@click="removePropertyFilter(), refreshColorsIfSetOrActiveFilterIsNumeric()"
@click="resetFilters()"
>
Reset
</FormButton>
@@ -26,7 +26,7 @@
<!-- Filter Logic Selection -->
<ViewerFiltersFilterLogicSelector
v-if="activeFilters.length > 0"
v-if="propertyFilters.length > 0"
v-model="filterLogic"
@update:model-value="handleFilterLogicChange"
/>
@@ -34,12 +34,12 @@
<div class="h-full flex flex-col">
<!-- Active Filters Section -->
<div
v-if="activeFilters.length > 0"
v-if="propertyFilters.length > 0"
class="flex-1 overflow-y-scroll simple-scrollbar"
>
<div class="space-y-3 p-3">
<ViewerFiltersFilterCard
v-for="filter in activeFilters"
v-for="filter in propertyFilters"
:key="filter.id"
:filter="filter"
:property-options="propertySelectOptions"
@@ -85,16 +85,16 @@ import { FormButton } from '@speckle/ui-components'
import { onClickOutside } from '@vueuse/core'
const {
removePropertyFilter,
applyPropertyFilter,
unApplyPropertyFilter,
filters: { propertyFilter, activeFilters },
filters: { propertyFilters },
getRelevantFilters,
getPropertyName,
addActiveFilter,
removeActiveFilter,
toggleFilterApplied,
toggleActiveFilterValue,
updateFilterCondition
updateFilterCondition,
updateActiveFilterValues,
resetFilters
} = useFilterUtilities()
const {
@@ -158,38 +158,16 @@ const filterLogic = ref<FilterLogic>(FilterLogic.All)
// Initialize data store logic
objectDataStore.setFilterLogic(filterLogic.value)
const speckleTypeFilter = computed(() =>
relevantFilters.value.find((f: PropertyInfo) => f.key === 'speckle_type')
)
const activeFilter = computed(
() => propertyFilter.filter.value || speckleTypeFilter.value
)
const mp = useMixpanel()
watch(activeFilter, (newVal) => {
if (!newVal) return
mp.track('Viewer Action', {
type: 'action',
name: 'filters',
action: 'set-active-filter',
value: newVal.key
})
})
const numericActiveFilter = computed(() =>
isNumericPropertyInfo(activeFilter.value) ? activeFilter.value : undefined
)
const title = computed(() => getPropertyName(activeFilter.value?.key ?? ''))
const colors = computed(() => !!propertyFilter.isApplied.value)
const title = computed(() => 'Filters')
const showPropertySelection = ref(false)
const propertySelectionRef = ref<HTMLElement>()
// Watch for filter changes and update data store slices
watch(
() => activeFilters.value,
() => propertyFilters.value,
(newFilters) => {
// Clear existing slices from this panel
const existingSlices = objectDataStore.dataSlices.value.filter((slice) =>
@@ -249,16 +227,13 @@ const handleAddFilterClick = () => {
}
const selectProperty = (propertyKey: string) => {
// Create the filter with the selected property
const filterId = `filter-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`
// Find the property filter
const property = relevantFilters.value.find((p) => p.key === propertyKey)
activeFilters.value.push({
id: filterId,
filter: relevantFilters.value.find((p) => p.key === propertyKey) || null,
isApplied: false,
selectedValues: [],
condition: FilterCondition.Is
})
if (property) {
// Use the addActiveFilter function to maintain consistency
addActiveFilter(property)
}
// Hide property selection
showPropertySelection.value = false
@@ -272,12 +247,13 @@ const selectProperty = (propertyKey: string) => {
}
const setFilterProperty = (filterId: string, propertyKey: string) => {
const filter = activeFilters.value.find((f) => f.id === filterId)
const filter = propertyFilters.value.find((f) => f.id === filterId)
const property = relevantFilters.value.find((p) => p.key === propertyKey)
if (filter && property) {
filter.filter = property
filter.selectedValues = [] // Reset selected values when property changes
// Reset selected values when property changes using the proper API
updateActiveFilterValues(filterId, [])
mp.track('Viewer Action', {
type: 'action',
@@ -352,27 +328,6 @@ const handleNumericRangeChange = (filterId: string, event: Event) => {
// TODO: Implement proper range handling with min/max values
}
// Handles a rather complicated ux flow: user sets a numeric filter which only makes sense with colors on. we set the force colors flag in that scenario, so we can revert it if user selects a non-numeric filter afterwards.
let forcedColors = false
const refreshColorsIfSetOrActiveFilterIsNumeric = () => {
if (!!numericActiveFilter.value && !colors.value) {
forcedColors = true
applyPropertyFilter()
return
}
if (!colors.value) return
if (forcedColors) {
forcedColors = false
unApplyPropertyFilter()
return
}
// removePropertyFilter()
applyPropertyFilter()
}
// Click outside to close property selection
onClickOutside(propertySelectionRef, () => {
if (showPropertySelection.value) {
@@ -72,35 +72,52 @@ const props = defineProps<{
kvp: KeyValuePair
}>()
const showActionsMenu = ref(false)
const {
isKvpFilterable,
getFilterDisabledReason,
findFilterByKvp,
addActiveFilter,
updateActiveFilterValues,
toggleFilterApplied
} = useFilterUtilities()
const { isKvpFilterable, getFilterDisabledReason, applyKvpFilter } =
useFilterUtilities()
const {
metadata: { availableFilters }
} = useInjectedViewer()
const showActionsMenu = ref(false)
const isUrlString = (v: unknown) => typeof v === 'string' && VALID_HTTP_URL.test(v)
const isCopyable = (kvp: KeyValuePair) => {
return kvp.value !== null && kvp.value !== undefined && typeof kvp.value !== 'object'
}
const isCopyable = computed(() => {
return (
props.kvp.value !== null &&
props.kvp.value !== undefined &&
typeof props.kvp.value !== 'object'
)
})
const isFilterable = (kvp: KeyValuePair) => {
return isKvpFilterable(kvp, availableFilters.value)
}
const isFilterable = computed(() => {
return isKvpFilterable(props.kvp, availableFilters.value)
})
const getDisabledReason = (kvp: KeyValuePair) => {
return getFilterDisabledReason(kvp, availableFilters.value)
}
const getDisabledReason = computed(() => {
return getFilterDisabledReason(props.kvp, availableFilters.value)
})
const handleFilterByProperty = (kvp: KeyValuePair) => {
applyKvpFilter(kvp, availableFilters.value)
const handleAddToFilters = (kvp: KeyValuePair) => {
const filter = findFilterByKvp(kvp, availableFilters.value)
if (filter && kvp.value !== null && kvp.value !== undefined) {
const filterId = addActiveFilter(filter)
const values = [String(kvp.value)]
updateActiveFilterValues(filterId, values)
toggleFilterApplied(filterId)
}
}
const handleCopy = async (kvp: KeyValuePair) => {
const { copy } = useClipboard()
if (isCopyable(kvp)) {
if (isCopyable.value) {
await copy(kvp.value as string, {
successMessage: `${kvp.key} copied to clipboard`,
failureMessage: `Failed to copy ${kvp.key} to clipboard`
@@ -114,20 +131,20 @@ const actionsItems = computed<LayoutMenuItem[][]>(() => {
{
title: 'Copy value',
id: 'copy-value',
disabled: !isCopyable(props.kvp),
disabledTooltip: isCopyable(props.kvp)
disabled: !isCopyable.value,
disabledTooltip: isCopyable.value
? undefined
: 'Cannot copy objects, arrays, or null values'
}
],
[
{
title: 'Filter by property',
id: 'filter-by-property',
disabled: !isFilterable(props.kvp),
disabledTooltip: isFilterable(props.kvp)
? undefined
: getDisabledReason(props.kvp)
title: 'Add to filters',
id: 'add-to-filters',
disabled: !isFilterable.value,
disabledTooltip: isFilterable.value
? 'Add this property to filters'
: getDisabledReason.value
}
]
]
@@ -143,8 +160,8 @@ const onActionChosen = (params: { item: LayoutMenuItem }) => {
case 'copy-value':
handleCopy(props.kvp)
break
case 'filter-by-property':
handleFilterByProperty(props.kvp)
case 'add-to-filters':
handleAddToFilters(props.kvp)
break
}
}
@@ -33,7 +33,7 @@ export type ResourceInfo = {
}
/**
* Extracts nested properties from an object, similar to the viewer's property extraction
* Extracts nested properties from an object for advanced filtering and data slicing.
*/
function extractNestedProperties(obj: Record<string, unknown>): PropertyInfoBase[] {
const properties: PropertyInfoBase[] = []
@@ -66,15 +66,6 @@ const globalDataSourcesMap: Ref<Record<string, DataSource>> = ref({})
const globalDataSlices: Ref<DataSlice[]> = ref([])
const globalCurrentFilterLogic = ref<FilterLogic>(FilterLogic.All)
/**
* Object data store for viewer filtering and data slicing operations.
*
* Based on the dashboard's objectDataStore pattern, this provides:
* - Multi-resource object and property management
* - Slice-based filtering with intersection logic
* - Property extraction from viewer objects
* - Query capabilities for filtering operations
*/
export function useObjectDataStore() {
const logger = useLogger()
@@ -2,7 +2,7 @@ import {
useInjectedViewerState,
useResetUiState
} from '~~/lib/viewer/composables/setup'
import { SpeckleViewer, TimeoutError } from '@speckle/shared'
import { SpeckleViewer } from '@speckle/shared'
import { get } from 'lodash-es'
import { Vector3 } from 'three'
import {
@@ -11,7 +11,7 @@ import {
useSelectionUtilities
} from '~~/lib/viewer/composables/ui'
import { CameraController, ViewMode, VisualDiffMode } from '@speckle/viewer'
import type { NumericPropertyInfo } from '@speckle/viewer'
import type { Merge, PartialDeep } from 'type-fest'
import type { SectionBoxData } from '@speckle/shared/viewer/state'
import { useViewerRealtimeActivityTracker } from '~/lib/viewer/composables/activity'
@@ -107,8 +107,8 @@ export function useStateSerialization() {
return ret
}, {} as Record<string, string | null>),
propertyFilter: {
key: state.ui.filters.propertyFilter.filter.value?.key || null,
isApplied: state.ui.filters.propertyFilter.isApplied.value
key: null, // Legacy field - not used in new multi-filter system
isApplied: false
}
},
camera: {
@@ -167,20 +167,10 @@ export function useApplySerializedState() {
},
urlHashState
} = useInjectedViewerState()
const {
resetFilters,
hideObjects,
isolateObjects,
removePropertyFilter,
setPropertyFilter,
applyPropertyFilter,
unApplyPropertyFilter,
waitForAvailableFilter
} = useFilterUtilities()
const { resetFilters, hideObjects, isolateObjects } = useFilterUtilities()
const resetState = useResetUiState()
const { diffModelVersions, deserializeDiffCommand, endDiff } = useDiffUtilities()
const { setSelectionFromObjectIds } = useSelectionUtilities()
const logger = useLogger()
const { update } = useViewerRealtimeActivityTracker()
return async <Mode extends StateApplyMode>(
@@ -261,44 +251,6 @@ export function useApplySerializedState() {
resetFilters()
}
const propertyFilterApplied = filters.propertyFilter?.isApplied
if (propertyFilterApplied) {
applyPropertyFilter()
} else {
unApplyPropertyFilter()
}
const propertyInfoKey = filters.propertyFilter?.key
const passMin = state.viewer?.metadata?.filteringState?.passMin
const passMax = state.viewer?.metadata?.filteringState?.passMax
if (propertyInfoKey) {
removePropertyFilter()
// Setting property filter asynchronously, when it's possible to do so
waitForAvailableFilter(propertyInfoKey)
.then((filter) => {
if (passMin || passMax) {
const numericFilter = { ...filter } as NumericPropertyInfo
numericFilter.passMin = passMin || numericFilter.min
numericFilter.passMax = passMax || numericFilter.max
setPropertyFilter(numericFilter)
applyPropertyFilter()
} else {
setPropertyFilter(filter)
applyPropertyFilter()
}
})
.catch((e) => {
if (e instanceof TimeoutError) {
logger.warn(
`${e.message} - filter probably comes from a thread context that isn't currently loaded`
)
} else {
logger.error(e)
}
})
}
// Handle resource string updates
if (
[StateApplyMode.Spotlight, StateApplyMode.ThreadFullContextOpen].includes(mode)
@@ -294,13 +294,9 @@ export type InjectableViewerState = Readonly<{
* For quick object ID lookups
*/
selectedObjectIds: ComputedRef<Set<string>>
propertyFilter: {
filter: Ref<Nullable<PropertyInfo>>
isApplied: Ref<boolean>
selectedValues: Ref<string[]> // Array of selected values for checkbox-style filtering
}
// New: Support for multiple active filters
activeFilters: Ref<
// Multi-filter system
propertyFilters: Ref<
Array<{
filter: PropertyInfo | null
isApplied: boolean
@@ -1084,12 +1080,8 @@ function setupInterfaceState(
const isolatedObjectIds = ref([] as string[])
const hiddenObjectIds = ref([] as string[])
const selectedObjects = shallowRef<Raw<SpeckleObject>[]>([])
const propertyFilter = ref(null as Nullable<PropertyInfo>)
const isPropertyFilterApplied = ref(false)
const selectedFilterValues = ref<string[]>([])
// New: Array to track multiple active filters
const activeFilters = ref<
const propertyFilters = ref<
Array<{
filter: PropertyInfo | null
isApplied: boolean
@@ -1101,7 +1093,7 @@ function setupInterfaceState(
const hasAnyFiltersApplied = computed(() => {
if (isolatedObjectIds.value.length) return true
if (hiddenObjectIds.value.length) return true
if (propertyFilter.value || isPropertyFilterApplied.value) return true
if (propertyFilters.value.length > 0) return true
return false
})
const viewMode = ref<ViewMode>(ViewMode.DEFAULT)
@@ -1179,12 +1171,7 @@ function setupInterfaceState(
hiddenObjectIds,
selectedObjects,
selectedObjectIds,
propertyFilter: {
filter: propertyFilter,
isApplied: isPropertyFilterApplied,
selectedValues: selectedFilterValues
},
activeFilters,
propertyFilters,
hasAnyFiltersApplied
},
highlightedObjectIds,
@@ -11,8 +11,8 @@ import {
ExplodeExtension,
LoaderEvent,
type PropertyInfo,
type StringPropertyInfo,
type SunLightConfiguration
type SunLightConfiguration,
FilteringExtension
} from '@speckle/viewer'
import {
ViewerEvent,
@@ -28,7 +28,6 @@ import { useAuthManager } from '~~/lib/auth/composables/auth'
import type { ViewerResourceItem } from '~~/lib/common/generated/gql/graphql'
import { ProjectCommentsUpdatedMessageType } from '~~/lib/common/generated/gql/graphql'
import {
useInjectedViewer,
useInjectedViewerState,
useInjectedViewerInterfaceState
} from '~~/lib/viewer/composables/setup'
@@ -51,7 +50,7 @@ import { arraysEqual, isNonNullable } from '~~/lib/common/helpers/utils'
import { getTargetObjectIds } from '~~/lib/object-sidebar/helpers'
import { Vector3 } from 'three'
import { areVectorsLooselyEqual } from '~~/lib/viewer/helpers/three'
import { SafeLocalStorage, type Nullable } from '@speckle/shared'
import { SafeLocalStorage } from '@speckle/shared'
import {
useCameraUtilities,
useMeasurementUtilities,
@@ -555,10 +554,6 @@ function useViewerFiltersIntegration() {
const filterUtils = useFilterUtilities({ state: useInjectedViewerState() })
const { dataStore: objectDataStore } = filterUtils
const {
metadata: { availableFilters: allFilters }
} = useInjectedViewer()
const logger = useLogger()
const stateKey = 'default'
let preventFilterWatchers = false
@@ -569,7 +564,7 @@ function useViewerFiltersIntegration() {
if (!isAlreadyInPreventScope) preventFilterWatchers = false
}
// Watch data store final object IDs and apply to viewer (follows existing filter patterns)
// Watch data store final object IDs and apply to viewer using FilteringExtension
watch(
objectDataStore.finalObjectIds,
(newObjectIds, oldObjectIds) => {
@@ -577,12 +572,13 @@ function useViewerFiltersIntegration() {
if (arraysEqual(newObjectIds, oldObjectIds || [])) return
withWatchersDisabled(() => {
const filteringExtension = instance.getExtension(FilteringExtension)
if (newObjectIds.length > 0) {
instance.isolateObjects(newObjectIds, stateKey, true)
filteringExtension.isolateObjects(newObjectIds, stateKey, true, true)
filters.hiddenObjectIds.value = []
filters.isolatedObjectIds.value = newObjectIds
} else {
instance.resetFilters()
filteringExtension.resetFilters()
filters.isolatedObjectIds.value = []
filters.hiddenObjectIds.value = []
}
@@ -591,10 +587,6 @@ function useViewerFiltersIntegration() {
{ immediate: true, flush: 'sync' }
)
const speckleTypeFilter = computed(
() => allFilters.value?.find((f) => f.key === 'speckle_type') as StringPropertyInfo
)
// state -> viewer
watch(
highlightedObjectIds,
@@ -663,16 +655,6 @@ function useViewerFiltersIntegration() {
// { immediate: true, flush: 'sync' }
// )
const syncColorFilterToViewer = async (
filter: Nullable<PropertyInfo>,
isApplied: boolean
) => {
const targetFilter = filter || speckleTypeFilter.value
if (isApplied && targetFilter) await instance.setColorFilter(targetFilter)
if (!isApplied) await instance.removeColorFilter()
}
// New function to handle multiple active filters
const applyMultipleFilters = async (
activeFilters: Array<{
@@ -767,7 +749,7 @@ function useViewerFiltersIntegration() {
// Keep the first one, disable the rest
for (let i = 1; i < appliedFilters.length; i++) {
const filterId = appliedFilters[i].id
const filter = filters.activeFilters.value.find((f) => f.id === filterId)
const filter = filters.propertyFilters.value.find((f) => f.id === filterId)
if (filter) {
filter.isApplied = false
}
@@ -875,67 +857,11 @@ function useViewerFiltersIntegration() {
return value
}
// Watch legacy single filter
watch(
() =>
<const>[
filters.propertyFilter.filter.value,
filters.propertyFilter.isApplied.value
],
async (newVal) => {
const [filter, isApplied] = newVal
// Only apply single filter if no active filters are present
if (filters.activeFilters.value.length === 0) {
await syncColorFilterToViewer(filter, isApplied)
}
},
{ immediate: true, flush: 'sync' }
)
// OLD FILTER SYSTEM - DISABLED IN FAVOR OF DATA STORE
// Watch new multi-filter system
// watch(
// () => filters.activeFilters.value,
// async (activeFilters) => {
// await applyMultipleFilters(activeFilters)
// },
// { immediate: true, flush: 'sync', deep: true }
// )
// // Also watch for changes in selected values to trigger isolation immediately
// watch(
// () =>
// filters.activeFilters.value.map((f) => ({
// id: f.id,
// selectedValues: f.selectedValues
// })),
// async () => {
// // Get filters that have selected values (for isolation)
// const filtersWithValues = filters.activeFilters.value.filter(
// (f) => f.filter !== null && f.selectedValues.length > 0
// )
// await applyIsolation(
// filtersWithValues.map((f) => ({
// filter: f.filter!,
// selectedValues: f.selectedValues,
// id: f.id
// }))
// )
// },
// { deep: true, flush: 'sync' }
// )
useOnViewerLoadComplete(
async () => {
// Check if we have active filters first
if (filters.activeFilters.value.length > 0) {
await applyMultipleFilters(filters.activeFilters.value)
} else {
// Fall back to legacy single filter
const targetFilter =
filters.propertyFilter.filter.value || speckleTypeFilter.value
const isApplied = filters.propertyFilter.isApplied.value
await syncColorFilterToViewer(targetFilter, isApplied)
// Apply property filters on load
if (filters.propertyFilters.value.length > 0) {
await applyMultipleFilters(filters.propertyFilters.value)
}
},
{ initialOnly: true }
+39 -141
View File
@@ -5,7 +5,12 @@ import {
type PropertyInfo,
ViewMode
} from '@speckle/viewer'
import { MeasurementsExtension, ViewModes, MeasurementEvent } from '@speckle/viewer'
import {
MeasurementsExtension,
ViewModes,
MeasurementEvent,
FilteringExtension
} from '@speckle/viewer'
import { until } from '@vueuse/shared'
import { useActiveElement } from '@vueuse/core'
import { difference, isString, uniq } from 'lodash-es'
@@ -208,7 +213,8 @@ export function useFilterUtilities(
...(options?.replace ? [] : filters.isolatedObjectIds.value),
...objectIds
])
// instance.isolateObjects(objectIds, 'utilities', true)
const filteringExtension = viewer.instance.getExtension(FilteringExtension)
filteringExtension.isolateObjects(objectIds, 'utilities', true, true)
}
const unIsolateObjects = (objectIds: string[]) => {
@@ -216,7 +222,8 @@ export function useFilterUtilities(
filters.isolatedObjectIds.value,
objectIds
)
// instance.unIsolateObjects(objectIds, 'utilities', true)
const filteringExtension = viewer.instance.getExtension(FilteringExtension)
filteringExtension.unIsolateObjects(objectIds, 'utilities', true, true)
}
const hideObjects = (
@@ -229,41 +236,14 @@ export function useFilterUtilities(
...(options?.replace ? [] : filters.hiddenObjectIds.value),
...objectIds
])
// instance.hideObjects(objectIds, 'utilities', true)
const filteringExtension = viewer.instance.getExtension(FilteringExtension)
filteringExtension.hideObjects(objectIds, 'utilities', false, false)
}
const showObjects = (objectIds: string[]) => {
filters.hiddenObjectIds.value = difference(filters.hiddenObjectIds.value, objectIds)
// instance.showObjects(objectIds, 'utilities', true)
}
/**
* Sets the current filter property. Does not apply it (instruct viewer to color objects).
*/
const setPropertyFilter = (property: PropertyInfo) => {
filters.propertyFilter.filter.value = property
}
/**
* Instructs the viewer to apply the current property filter (color objects).
*/
const applyPropertyFilter = () => {
filters.propertyFilter.isApplied.value = true
}
/**
* Unsets the current property filter.
*/
const removePropertyFilter = () => {
filters.propertyFilter.isApplied.value = false
filters.propertyFilter.filter.value = null
}
/**
* Unapplies the current property filter - removes object colouring
*/
const unApplyPropertyFilter = () => {
filters.propertyFilter.isApplied.value = false
const filteringExtension = viewer.instance.getExtension(FilteringExtension)
filteringExtension.showObjects(objectIds, 'utilities', false)
}
/**
@@ -292,86 +272,20 @@ export function useFilterUtilities(
}
/**
* Sets the selected values for the current property filter
*/
const setSelectedFilterValues = (values: string[]) => {
filters.propertyFilter.selectedValues.value = [...values]
}
/**
* Adds a value to the selected filter values
*/
const addSelectedFilterValue = (value: string) => {
if (!filters.propertyFilter.selectedValues.value.includes(value)) {
filters.propertyFilter.selectedValues.value.push(value)
}
}
/**
* Removes a value from the selected filter values
*/
const removeSelectedFilterValue = (value: string) => {
const index = filters.propertyFilter.selectedValues.value.indexOf(value)
if (index > -1) {
filters.propertyFilter.selectedValues.value.splice(index, 1)
}
}
/**
* Toggles a value in the selected filter values (checkbox-style)
*/
const toggleSelectedFilterValue = (value: string) => {
if (filters.propertyFilter.selectedValues.value.includes(value)) {
removeSelectedFilterValue(value)
} else {
addSelectedFilterValue(value)
}
}
/**
* Checks if a value is currently selected
*/
const isValueSelected = (value: string): boolean => {
return filters.propertyFilter.selectedValues.value.includes(value)
}
/**
* Gets the values to filter by - either selected values or all values (for backward compatibility)
*/
const getFilterValues = (): string[] => {
const selectedValues = filters.propertyFilter.selectedValues.value
const currentFilter = filters.propertyFilter.filter.value
// If we have selected values, use those
if (selectedValues.length > 0) {
return selectedValues
}
// Otherwise, fall back to all available values (backward compatibility)
if (currentFilter) {
return getAvailableFilterValues(currentFilter)
}
return []
}
// === NEW MULTI-FILTER FUNCTIONS ===
/**
* Adds a new active filter or updates existing one
* Adds a new filter or updates existing one
*/
const addActiveFilter = (filter: PropertyInfo): string => {
const existingIndex = filters.activeFilters.value.findIndex(
const existingIndex = filters.propertyFilters.value.findIndex(
(f) => f.filter?.key === filter.key
)
if (existingIndex !== -1) {
// Update existing filter
return filters.activeFilters.value[existingIndex].id
return filters.propertyFilters.value[existingIndex].id
} else {
// Add new filter
const id = `filter-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`
filters.activeFilters.value.push({
filters.propertyFilters.value.push({
filter,
isApplied: false,
selectedValues: [],
@@ -386,9 +300,9 @@ export function useFilterUtilities(
* Removes an active filter by ID
*/
const removeActiveFilter = (filterId: string) => {
const index = filters.activeFilters.value.findIndex((f) => f.id === filterId)
const index = filters.propertyFilters.value.findIndex((f) => f.id === filterId)
if (index !== -1) {
filters.activeFilters.value.splice(index, 1)
filters.propertyFilters.value.splice(index, 1)
}
}
@@ -396,7 +310,7 @@ export function useFilterUtilities(
* Toggles the applied state of a specific filter
*/
const toggleFilterApplied = (filterId: string) => {
const filter = filters.activeFilters.value.find((f) => f.id === filterId)
const filter = filters.propertyFilters.value.find((f) => f.id === filterId)
if (filter) {
filter.isApplied = !filter.isApplied
}
@@ -406,7 +320,7 @@ export function useFilterUtilities(
* Updates selected values for a specific active filter
*/
const updateActiveFilterValues = (filterId: string, values: string[]) => {
const filter = filters.activeFilters.value.find((f) => f.id === filterId)
const filter = filters.propertyFilters.value.find((f) => f.id === filterId)
if (filter) {
filter.selectedValues = [...values]
}
@@ -416,7 +330,7 @@ export function useFilterUtilities(
* Updates condition for a specific active filter
*/
const updateFilterCondition = (filterId: string, condition: FilterCondition) => {
const filter = filters.activeFilters.value.find((f) => f.id === filterId)
const filter = filters.propertyFilters.value.find((f) => f.id === filterId)
if (filter) {
filter.condition = condition
}
@@ -426,7 +340,7 @@ export function useFilterUtilities(
* Toggles a value for a specific active filter
*/
const toggleActiveFilterValue = (filterId: string, value: string) => {
const filter = filters.activeFilters.value.find((f) => f.id === filterId)
const filter = filters.propertyFilters.value.find((f) => f.id === filterId)
if (filter) {
const index = filter.selectedValues.indexOf(value)
if (index > -1) {
@@ -441,7 +355,7 @@ export function useFilterUtilities(
* Checks if a value is selected for a specific active filter
*/
const isActiveFilterValueSelected = (filterId: string, value: string): boolean => {
const filter = filters.activeFilters.value.find((f) => f.id === filterId)
const filter = filters.propertyFilters.value.find((f) => f.id === filterId)
return filter ? filter.selectedValues.includes(value) : false
}
@@ -449,17 +363,16 @@ export function useFilterUtilities(
* Gets all currently applied filters
*/
const getAppliedFilters = () => {
return filters.activeFilters.value.filter((f) => f.isApplied)
return filters.propertyFilters.value.filter((f) => f.isApplied)
}
const resetFilters = () => {
filters.hiddenObjectIds.value = []
filters.isolatedObjectIds.value = []
filters.propertyFilter.filter.value = null
filters.propertyFilter.isApplied.value = false
filters.propertyFilter.selectedValues.value = []
filters.activeFilters.value = [] // Reset active filters
// filters.selectedObjects.value = []
filters.propertyFilters.value = []
filters.selectedObjects.value = []
const filteringExtension = viewer.instance.getExtension(FilteringExtension)
filteringExtension.resetFilters()
}
const resetExplode = () => {
@@ -484,7 +397,7 @@ export function useFilterUtilities(
}
const hasActiveFilters = computed(() => {
return !!filters.propertyFilter.filter.value
return filters.propertyFilters.value.length > 0
})
// Regex patterns for identifying Revit properties
@@ -675,12 +588,12 @@ export function useFilterUtilities(
}
/**
* Applies a filter for a key-value pair (with smart matching)
* Finds a filter for a key-value pair using smart matching logic
*/
const applyKvpFilter = (
const findFilterByKvp = (
kvp: { key: string; backendPath?: string },
availableFilters: PropertyInfo[] | null | undefined
): void => {
): PropertyInfo | undefined => {
// Use backendPath if available, otherwise fall back to display key
const backendKey = kvp.backendPath || kvp.key
@@ -693,10 +606,7 @@ export function useFilterUtilities(
filter = findFilterByDisplayName(displayKey, availableFilters)
}
if (filter) {
setPropertyFilter(filter)
applyPropertyFilter()
}
return filter
}
return {
@@ -705,19 +615,9 @@ export function useFilterUtilities(
hideObjects,
showObjects,
filters,
setPropertyFilter,
applyPropertyFilter,
removePropertyFilter,
unApplyPropertyFilter,
// New multi-value filter functions
// Filter value functions
getAvailableFilterValues,
setSelectedFilterValues,
addSelectedFilterValue,
removeSelectedFilterValue,
toggleSelectedFilterValue,
isValueSelected,
getFilterValues,
// New multi-filter functions
// Multi-filter functions
addActiveFilter,
removeActiveFilter,
toggleFilterApplied,
@@ -738,7 +638,7 @@ export function useFilterUtilities(
findFilterByDisplayName,
isKvpFilterable,
getFilterDisabledReason,
applyKvpFilter,
findFilterByKvp,
// Data store for advanced filtering
dataStore
}
@@ -894,9 +794,7 @@ export function useMeasurementUtilities() {
}
const removeMeasurement = () => {
if (state.viewer.instance?.removeMeasurement) {
state.viewer.instance.removeMeasurement()
}
state.viewer.instance.getExtension(MeasurementsExtension).removeMeasurement()
}
const clearMeasurements = () => {
+13 -1
View File
@@ -81,6 +81,14 @@ export type SerializedViewerState = {
key: Nullable<string>
isApplied: boolean
}
// New multi-filter system (optional for backward compatibility)
propertyFilters?: Array<{
key: Nullable<string>
isApplied: boolean
selectedValues: string[]
id: string
condition: 'AND' | 'OR'
}>
}
camera: {
position: number[]
@@ -227,7 +235,11 @@ const initializeMissingData = (state: UnformattedState): SerializedViewerState =
...(state.ui?.filters?.propertyFilter || {}),
key: state.ui?.filters?.propertyFilter?.key || null,
isApplied: state.ui?.filters?.propertyFilter?.isApplied || false
}
},
// Optional new multi-filter system
...(state.ui?.filters?.propertyFilters && {
propertyFilters: state.ui.filters.propertyFilters
})
},
camera: {
...(state.ui?.camera || {}),