From 4e0efba217e4dd70745f85501200c964c0fc3959 Mon Sep 17 00:00:00 2001 From: andrewwallacespeckle Date: Wed, 20 Aug 2025 13:33:08 +0100 Subject: [PATCH] New Viewer API & big tidy up --- .../panel/FunctionRunRowObjectResult.vue | 6 +- .../components/viewer/controls/Left.vue | 10 +- .../viewer/filters/NumericFilter.vue | 7 +- .../components/viewer/filters/Panel.vue | 85 ++------- .../viewer/selection/KeyValuePair.vue | 67 ++++--- .../composables/viewer/useObjectDataStore.ts | 11 +- .../lib/viewer/composables/serialization.ts | 58 +----- .../lib/viewer/composables/setup.ts | 25 +-- .../lib/viewer/composables/setup/postSetup.ts | 96 ++-------- .../frontend-2/lib/viewer/composables/ui.ts | 180 ++++-------------- packages/shared/src/viewer/helpers/state.ts | 14 +- 11 files changed, 150 insertions(+), 409 deletions(-) diff --git a/packages/frontend-2/components/automate/viewer/panel/FunctionRunRowObjectResult.vue b/packages/frontend-2/components/automate/viewer/panel/FunctionRunRowObjectResult.vue index 64f70fe34..3ff97c592 100644 --- a/packages/frontend-2/components/automate/viewer/panel/FunctionRunRowObjectResult.vue +++ b/packages/frontend-2/components/automate/viewer/panel/FunctionRunRowObjectResult.vue @@ -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(() => { diff --git a/packages/frontend-2/components/viewer/controls/Left.vue b/packages/frontend-2/components/viewer/controls/Left.vue index b5954f607..6c3883d21 100644 --- a/packages/frontend-2/components/viewer/controls/Left.vue +++ b/packages/frontend-2/components/viewer/controls/Left.vue @@ -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' } } diff --git a/packages/frontend-2/components/viewer/filters/NumericFilter.vue b/packages/frontend-2/components/viewer/filters/NumericFilter.vue index 44852867a..a951f1ca0 100644 --- a/packages/frontend-2/components/viewer/filters/NumericFilter.vue +++ b/packages/frontend-2/components/viewer/filters/NumericFilter.vue @@ -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) } diff --git a/packages/frontend-2/components/viewer/filters/Panel.vue b/packages/frontend-2/components/viewer/filters/Panel.vue index 234bc75d6..f8cc0f879 100644 --- a/packages/frontend-2/components/viewer/filters/Panel.vue +++ b/packages/frontend-2/components/viewer/filters/Panel.vue @@ -8,7 +8,7 @@ size="sm" color="subtle" tabindex="-1" - @click="removePropertyFilter(), refreshColorsIfSetOrActiveFilterIsNumeric()" + @click="resetFilters()" > Reset @@ -26,7 +26,7 @@ @@ -34,12 +34,12 @@
(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() // 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) { diff --git a/packages/frontend-2/components/viewer/selection/KeyValuePair.vue b/packages/frontend-2/components/viewer/selection/KeyValuePair.vue index 1a38d2028..9a2a0a725 100644 --- a/packages/frontend-2/components/viewer/selection/KeyValuePair.vue +++ b/packages/frontend-2/components/viewer/selection/KeyValuePair.vue @@ -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(() => { { 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 } } diff --git a/packages/frontend-2/composables/viewer/useObjectDataStore.ts b/packages/frontend-2/composables/viewer/useObjectDataStore.ts index 5a62d221b..11d736bca 100644 --- a/packages/frontend-2/composables/viewer/useObjectDataStore.ts +++ b/packages/frontend-2/composables/viewer/useObjectDataStore.ts @@ -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): PropertyInfoBase[] { const properties: PropertyInfoBase[] = [] @@ -66,15 +66,6 @@ const globalDataSourcesMap: Ref> = ref({}) const globalDataSlices: Ref = ref([]) const globalCurrentFilterLogic = ref(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() diff --git a/packages/frontend-2/lib/viewer/composables/serialization.ts b/packages/frontend-2/lib/viewer/composables/serialization.ts index bcfc14111..b3da22344 100644 --- a/packages/frontend-2/lib/viewer/composables/serialization.ts +++ b/packages/frontend-2/lib/viewer/composables/serialization.ts @@ -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), 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 ( @@ -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) diff --git a/packages/frontend-2/lib/viewer/composables/setup.ts b/packages/frontend-2/lib/viewer/composables/setup.ts index 8ac328a07..eccc01ba9 100644 --- a/packages/frontend-2/lib/viewer/composables/setup.ts +++ b/packages/frontend-2/lib/viewer/composables/setup.ts @@ -294,13 +294,9 @@ export type InjectableViewerState = Readonly<{ * For quick object ID lookups */ selectedObjectIds: ComputedRef> - propertyFilter: { - filter: Ref> - isApplied: Ref - selectedValues: Ref // 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[]>([]) - const propertyFilter = ref(null as Nullable) - const isPropertyFilterApplied = ref(false) - const selectedFilterValues = ref([]) - // 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.DEFAULT) @@ -1179,12 +1171,7 @@ function setupInterfaceState( hiddenObjectIds, selectedObjects, selectedObjectIds, - propertyFilter: { - filter: propertyFilter, - isApplied: isPropertyFilterApplied, - selectedValues: selectedFilterValues - }, - activeFilters, + propertyFilters, hasAnyFiltersApplied }, highlightedObjectIds, diff --git a/packages/frontend-2/lib/viewer/composables/setup/postSetup.ts b/packages/frontend-2/lib/viewer/composables/setup/postSetup.ts index 5070b9bfe..f190b639a 100644 --- a/packages/frontend-2/lib/viewer/composables/setup/postSetup.ts +++ b/packages/frontend-2/lib/viewer/composables/setup/postSetup.ts @@ -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, - 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( - () => - [ - 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 } diff --git a/packages/frontend-2/lib/viewer/composables/ui.ts b/packages/frontend-2/lib/viewer/composables/ui.ts index 355004e92..22e5ac39b 100644 --- a/packages/frontend-2/lib/viewer/composables/ui.ts +++ b/packages/frontend-2/lib/viewer/composables/ui.ts @@ -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 = () => { diff --git a/packages/shared/src/viewer/helpers/state.ts b/packages/shared/src/viewer/helpers/state.ts index ef586728a..4bf1da7d0 100644 --- a/packages/shared/src/viewer/helpers/state.ts +++ b/packages/shared/src/viewer/helpers/state.ts @@ -81,6 +81,14 @@ export type SerializedViewerState = { key: Nullable isApplied: boolean } + // New multi-filter system (optional for backward compatibility) + propertyFilters?: Array<{ + key: Nullable + 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 || {}),