import { SpeckleViewer, timeoutAt } from '@speckle/shared' import type { TreeNode, MeasurementOptions, PropertyInfo } from '@speckle/viewer' import { MeasurementsExtension } from '@speckle/viewer' import { until } from '@vueuse/shared' import { difference, isString, uniq } from 'lodash-es' import { useEmbedState } from '~/lib/viewer/composables/setup/embed' import type { SpeckleObject } from '~/lib/viewer/helpers/sceneExplorer' import { isNonNullable } from '~~/lib/common/helpers/utils' import { useInjectedViewer, useInjectedViewerInterfaceState, useInjectedViewerState, type InjectableViewerState } from '~~/lib/viewer/composables/setup' import { useDiffBuilderUtilities } from '~~/lib/viewer/composables/setup/diff' import { useTourStageState } from '~~/lib/viewer/composables/tour' export function useSectionBoxUtilities() { const { instance } = useInjectedViewer() const { sectionBox, filters: { selectedObjects } } = useInjectedViewerInterfaceState() const isSectionBoxEnabled = computed(() => !!sectionBox.value) const toggleSectionBox = () => { if (isSectionBoxEnabled.value) { sectionBox.value = null return } const objectIds = selectedObjects.value.map((o) => o.id).filter(isNonNullable) const box = instance.getSectionBoxFromObjects(objectIds) sectionBox.value = box } const sectionBoxOn = () => { if (!isSectionBoxEnabled.value) { toggleSectionBox() } } const sectionBoxOff = () => { sectionBox.value = null } return { isSectionBoxEnabled, toggleSectionBox, sectionBoxOn, sectionBoxOff, sectionBox } } export function useCameraUtilities() { const { instance } = useInjectedViewer() const { filters: { selectedObjects, isolatedObjectIds }, camera } = useInjectedViewerInterfaceState() const zoom = (...args: Parameters) => instance.zoom(...args) const setView = (...args: Parameters) => { instance.setView(...args) } const zoomExtentsOrSelection = () => { const ids = selectedObjects.value.map((o) => o.id).filter(isNonNullable) if (ids.length > 0) { return instance.zoom(ids) } if (isolatedObjectIds.value.length) { return instance.zoom(isolatedObjectIds.value) } instance.zoom() } const toggleProjection = () => { camera.isOrthoProjection.value = !camera.isOrthoProjection.value } const forceViewToViewerSync = () => { setView({ position: camera.position.value, target: camera.target.value }) } return { zoomExtentsOrSelection, toggleProjection, camera, setView, zoom, forceViewToViewerSync } } export function useFilterUtilities( options?: Partial<{ state: InjectableViewerState }> ) { const state = options?.state || useInjectedViewerState() const { viewer, ui: { filters, explodeFactor } } = state const isolateObjects = ( objectIds: string[], options?: Partial<{ replace: boolean }> ) => { filters.isolatedObjectIds.value = uniq([ ...(options?.replace ? [] : filters.isolatedObjectIds.value), ...objectIds ]) // instance.isolateObjects(objectIds, 'utilities', true) } const unIsolateObjects = (objectIds: string[]) => { filters.isolatedObjectIds.value = difference( filters.isolatedObjectIds.value, objectIds ) // instance.unIsolateObjects(objectIds, 'utilities', true) } const hideObjects = ( objectIds: string[], options?: Partial<{ replace: boolean }> ) => { filters.hiddenObjectIds.value = uniq([ ...(options?.replace ? [] : filters.hiddenObjectIds.value), ...objectIds ]) // instance.hideObjects(objectIds, 'utilities', true) } 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 resetFilters = () => { filters.hiddenObjectIds.value = [] filters.isolatedObjectIds.value = [] filters.propertyFilter.filter.value = null filters.propertyFilter.isApplied.value = false explodeFactor.value = 0 // filters.selectedObjects.value = [] } const waitForAvailableFilter = async ( key: string, options?: Partial<{ timeout: number }> ) => { const timeout = options?.timeout || 10000 const res = await Promise.race([ until(viewer.metadata.availableFilters).toMatch( (filters) => !!filters?.find((p) => p.key === key) ), timeoutAt(timeout, 'Waiting for available filter timed out') ]) const filter = res?.find((p) => p.key === key) return filter as NonNullable } return { isolateObjects, unIsolateObjects, hideObjects, showObjects, filters, setPropertyFilter, applyPropertyFilter, removePropertyFilter, unApplyPropertyFilter, resetFilters, waitForAvailableFilter } } export function useSelectionUtilities() { const { filters: { selectedObjects, selectedObjectIds } } = useInjectedViewerInterfaceState() const { metadata } = useInjectedViewer() const setSelectionFromObjectIds = (objectIds: string[]) => { const objs: Array = [] objectIds.forEach((value: string) => { objs.push( ...( (metadata?.worldTree.value?.findId(value) || []) as unknown as TreeNode[] ).map( (node: TreeNode) => (node.model as Record).raw as SpeckleObject ) ) }) selectedObjects.value = objs } const addToSelectionFromObjectIds = (objectIds: string[]) => { const originalObjects = selectedObjects.value.slice() setSelectionFromObjectIds(objectIds) selectedObjects.value = [...originalObjects, ...selectedObjects.value] } const removeFromSelectionObjectIds = (objectIds: string[]) => { const finalObjects = selectedObjects.value.filter( (o) => !objectIds.includes(o.id || '') ) selectedObjects.value = finalObjects } const addToSelection = (object: SpeckleObject) => { const idx = selectedObjects.value.findIndex((o) => o.id === object.id) if (idx !== -1) return selectedObjects.value = [...selectedObjects.value, object] } const removeFromSelection = (objectOrId: SpeckleObject | string) => { const oid = isString(objectOrId) ? objectOrId : objectOrId.id const idx = selectedObjects.value.findIndex((o) => o.id === oid) if (idx === -1) return const newObjects = selectedObjects.value.slice() newObjects.splice(idx, 1) selectedObjects.value = newObjects } const clearSelection = () => { selectedObjects.value = [] } return { addToSelection, removeFromSelection, clearSelection, setSelectionFromObjectIds, addToSelectionFromObjectIds, removeFromSelectionObjectIds, objects: selectedObjects, objectIds: selectedObjectIds } } export function useDiffUtilities() { const state = useInjectedViewerState() const { serializeDiffCommand, deserializeDiffCommand, areDiffsEqual } = useDiffBuilderUtilities() const endDiff = async () => { await state.urlHashState.diff.update(null) } const diffModelVersions = async ( modelId: string, versionA: string, versionB: string ) => { await state.urlHashState.diff.update({ diffs: [ { versionA: new SpeckleViewer.ViewerRoute.ViewerVersionResource( modelId, versionA ), versionB: new SpeckleViewer.ViewerRoute.ViewerVersionResource( modelId, versionB ) } ] }) } return { serializeDiffCommand, deserializeDiffCommand, endDiff, diffModelVersions, areDiffsEqual } } export function useThreadUtilities() { const { urlHashState: { focusedThreadId }, ui: { threads: { openThread: { thread: openThread } } } } = useInjectedViewerState() const isOpenThread = (id: string) => focusedThreadId.value === id const closeAllThreads = async () => { await focusedThreadId.update(null) } const open = async (id: string) => { if (id === focusedThreadId.value) return await focusedThreadId.update(id) await Promise.all([ until(focusedThreadId).toMatch((tid) => tid === id), until(openThread).toMatch((t) => t?.id === id) ]) } return { closeAllThreads, open, isOpenThread } } export function useMeasurementUtilities() { const state = useInjectedViewerState() const enableMeasurements = (enabled: boolean) => { state.ui.measurement.enabled.value = enabled } const setMeasurementOptions = (options: MeasurementOptions) => { state.ui.measurement.options.value = options } const removeMeasurement = () => { if (state.viewer.instance?.removeMeasurement) { state.viewer.instance.removeMeasurement() } } const clearMeasurements = () => { state.viewer.instance.getExtension(MeasurementsExtension).clearMeasurements() } const getActiveMeasurement = () => { const measurementsExtension = state.viewer.instance.getExtension(MeasurementsExtension) const activeMeasurement = measurementsExtension?.activeMeasurement return activeMeasurement && activeMeasurement.state === 2 } return { enableMeasurements, setMeasurementOptions, removeMeasurement, clearMeasurements, getActiveMeasurement } } /** * Some conditional rendering values depend on multiple & overlapping states. This utility reconciles that. */ export function useConditionalViewerRendering() { const tourState = useTourStageState() const embedMode = useEmbedState() const showControls = computed(() => { if (tourState.value.showTour && !tourState.value.showViewerControls) return false if ( embedMode.embedOptions.value?.isEnabled && embedMode.embedOptions.value.hideControls ) { return false } return true }) const showNavbar = computed(() => { if (!showControls.value) return false if (tourState.value.showTour && !tourState.value.showNavbar) return false if (embedMode.embedOptions.value?.isEnabled) return false return true }) return { showNavbar, showControls } } export function useHighlightedObjectsUtilities() { const { ui: { highlightedObjectIds } } = useInjectedViewerState() const highlightObjects = (ids: string[]) => { highlightedObjectIds.value = [...new Set([...highlightedObjectIds.value, ...ids])] } const unhighlightObjects = (ids: string[]) => { highlightedObjectIds.value = highlightedObjectIds.value.filter( (id) => !ids.includes(id) ) } const clearHighlightedObjects = () => { highlightedObjectIds.value = [] } return { highlightObjects, unhighlightObjects, clearHighlightedObjects } }