New Viewer API & big tidy up
This commit is contained in:
@@ -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 }
|
||||
|
||||
@@ -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 = () => {
|
||||
|
||||
@@ -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 || {}),
|
||||
|
||||
Reference in New Issue
Block a user