448 lines
12 KiB
TypeScript
448 lines
12 KiB
TypeScript
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<typeof instance.zoom>) => instance.zoom(...args)
|
|
|
|
const setView = (...args: Parameters<typeof instance.setView>) => {
|
|
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<typeof filter>
|
|
}
|
|
|
|
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<SpeckleObject> = []
|
|
objectIds.forEach((value: string) => {
|
|
objs.push(
|
|
...(
|
|
(metadata?.worldTree.value?.findId(value) || []) as unknown as TreeNode[]
|
|
).map(
|
|
(node: TreeNode) =>
|
|
(node.model as Record<string, unknown>).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
|
|
}
|
|
}
|