9427686d42
* fix(fe2): unfollow on camera move * WIP new state hydration function * WIP sync state * minor cleanup * fix coloring not being tracked * fix for post thread close camera pos restore * supporting duplicate users * preventing guest commenting + state reset fixes * fixed guests not receiving viewer comment updates * post-thread creation opens new thread * removing gap between 'X is typing' and bubble appearing * reset filters will also reset colors now * fixed thread full context * camera reset fix * thread reset fix * fixed router concurrency issues * followed user avatar fix * TONS OF DEBUGGING FOR ROUTER QUEUING * removing queued routing debugging stuff + disabling spotlight cancelation * WIP async URL updates * missing authLogger fixed * fix for broken projection * fix for bubbles positions not updating correctly * queued routing cleanup * fixed spotlight mode disabling unnecessarily * added back stoplight stop on ctrl * undid spotlight debugging
318 lines
8.4 KiB
TypeScript
318 lines
8.4 KiB
TypeScript
import {
|
|
InitialStateWithRequestAndResponse,
|
|
InjectableViewerState,
|
|
useInjectedViewerState
|
|
} from '~~/lib/viewer/composables/setup'
|
|
import { SelectionEvent, ViewerEvent } from '@speckle/viewer'
|
|
import { debounce, isArray, throttle } from 'lodash-es'
|
|
import { MaybeAsync, Nullable, TimeoutError, timeoutAt } from '@speckle/shared'
|
|
import { until } from '@vueuse/shared'
|
|
import { Vector3 } from 'three'
|
|
import { areVectorsLooselyEqual } from '~~/lib/viewer/helpers/three'
|
|
|
|
function getFirstVisibleSelectionHit(
|
|
{ hits }: SelectionEvent,
|
|
state: Pick<InjectableViewerState, 'viewer'>
|
|
) {
|
|
const {
|
|
viewer: {
|
|
metadata: { filteringState }
|
|
}
|
|
} = state
|
|
|
|
const hasHiddenObjects = (filteringState.value?.hiddenObjects || []).length !== 0
|
|
const hasIsolatedObjects =
|
|
!!filteringState.value?.isolatedObjects &&
|
|
filteringState.value?.isolatedObjects.length !== 0
|
|
|
|
for (const hit of hits) {
|
|
if (hasHiddenObjects) {
|
|
if (!filteringState.value?.hiddenObjects?.includes(hit.object.id as string)) {
|
|
return hit
|
|
}
|
|
} else if (hasIsolatedObjects) {
|
|
if (filteringState.value.isolatedObjects?.includes(hit.object.id as string))
|
|
return hit
|
|
} else {
|
|
return hit
|
|
}
|
|
}
|
|
return null
|
|
}
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
export function useViewerEventListener<A = any>(
|
|
name: ViewerEvent | ViewerEvent[],
|
|
listener: (...args: A[]) => MaybeAsync<void>,
|
|
options?: Partial<{
|
|
state: InitialStateWithRequestAndResponse
|
|
}>
|
|
) {
|
|
const {
|
|
viewer: { instance }
|
|
} = options?.state || useInjectedViewerState()
|
|
const names = isArray(name) ? name : [name]
|
|
|
|
const unmount = () => {
|
|
for (const n of names) {
|
|
instance.removeListener(n, listener)
|
|
}
|
|
}
|
|
|
|
onMounted(() => {
|
|
for (const n of names) {
|
|
instance.on(n, listener)
|
|
}
|
|
})
|
|
|
|
onBeforeUnmount(() => {
|
|
unmount
|
|
})
|
|
|
|
return unmount
|
|
}
|
|
|
|
export function useViewerCameraTracker(
|
|
callback: () => void,
|
|
options?: Partial<{
|
|
throttleWait: number
|
|
debounceWait: number
|
|
onlyInvokeOnMeaningfulChanges: boolean
|
|
}>
|
|
): void {
|
|
const {
|
|
viewer: { instance }
|
|
} = useInjectedViewerState()
|
|
const {
|
|
throttleWait = 50,
|
|
debounceWait,
|
|
onlyInvokeOnMeaningfulChanges
|
|
} = options || {}
|
|
|
|
const lastPos = ref(null as Nullable<Vector3>)
|
|
const lastTarget = ref(null as Nullable<Vector3>)
|
|
|
|
const callbackChangeTrackerWrapper = () => {
|
|
if (!onlyInvokeOnMeaningfulChanges) {
|
|
return callback()
|
|
}
|
|
|
|
// Only invoke callback if position/target changed in a meaningful way
|
|
const activeCam = instance.cameraHandler.activeCam
|
|
const controls = activeCam.controls
|
|
const viewerPos = new Vector3()
|
|
const viewerTarget = new Vector3()
|
|
|
|
controls.getPosition(viewerPos)
|
|
controls.getTarget(viewerTarget)
|
|
|
|
let meaningfulChangeFound = false
|
|
if (!lastPos.value || !areVectorsLooselyEqual(lastPos.value, viewerPos)) {
|
|
meaningfulChangeFound = true
|
|
}
|
|
if (!lastTarget.value || !areVectorsLooselyEqual(lastTarget.value, viewerTarget)) {
|
|
meaningfulChangeFound = true
|
|
}
|
|
|
|
if (meaningfulChangeFound) {
|
|
lastPos.value = viewerPos.clone()
|
|
lastTarget.value = viewerTarget.clone()
|
|
callback()
|
|
}
|
|
}
|
|
|
|
const finalCallback = debounceWait
|
|
? debounce(callbackChangeTrackerWrapper, debounceWait)
|
|
: throttleWait
|
|
? throttle(callbackChangeTrackerWrapper, throttleWait)
|
|
: callbackChangeTrackerWrapper
|
|
|
|
onMounted(() => {
|
|
instance.cameraHandler.controls.addEventListener('update', finalCallback)
|
|
})
|
|
|
|
onBeforeUnmount(() => {
|
|
instance.cameraHandler.controls.removeEventListener('update', finalCallback)
|
|
})
|
|
}
|
|
|
|
export function useViewerCameraControlStartTracker(callback: () => void) {
|
|
const {
|
|
viewer: { instance }
|
|
} = useInjectedViewerState()
|
|
|
|
const removeListener = () =>
|
|
instance.cameraHandler.controls.removeEventListener('controlstart', callback)
|
|
|
|
onMounted(() => {
|
|
instance.cameraHandler.controls.addEventListener('controlstart', callback)
|
|
})
|
|
|
|
onBeforeUnmount(() => {
|
|
removeListener()
|
|
})
|
|
|
|
return removeListener
|
|
}
|
|
|
|
export function useViewerCameraRestTracker(
|
|
callback: () => void,
|
|
options?: Partial<{ debounceWait: number }>
|
|
) {
|
|
const {
|
|
viewer: { instance }
|
|
} = useInjectedViewerState()
|
|
|
|
const { debounceWait = 200 } = options || {}
|
|
|
|
const finalCallback = debounceWait ? debounce(callback, debounceWait) : callback
|
|
const removeListener = () =>
|
|
instance.cameraHandler.controls.removeEventListener('rest', finalCallback)
|
|
|
|
onMounted(() => {
|
|
instance.cameraHandler.controls.addEventListener('rest', finalCallback)
|
|
})
|
|
|
|
onBeforeUnmount(() => {
|
|
removeListener()
|
|
})
|
|
|
|
return removeListener
|
|
}
|
|
|
|
export function useViewerCameraControlEndTracker(callback: () => void) {
|
|
const {
|
|
viewer: { instance }
|
|
} = useInjectedViewerState()
|
|
|
|
const removeListener = () =>
|
|
instance.cameraHandler.controls.removeEventListener('rest', callback)
|
|
|
|
onMounted(() => {
|
|
instance.cameraHandler.controls.addEventListener('rest', callback)
|
|
})
|
|
|
|
onBeforeUnmount(() => {
|
|
removeListener()
|
|
})
|
|
|
|
return removeListener
|
|
}
|
|
|
|
export function useSelectionEvents(
|
|
params: {
|
|
singleClickCallback?: (
|
|
event: Nullable<SelectionEvent>,
|
|
extra: { firstVisibleSelectionHit: Nullable<SelectionEvent['hits'][0]> }
|
|
) => void
|
|
doubleClickCallback?: (
|
|
event: Nullable<SelectionEvent>,
|
|
extra: { firstVisibleSelectionHit: Nullable<SelectionEvent['hits'][0]> }
|
|
) => void
|
|
},
|
|
options?: Partial<{
|
|
state: InitialStateWithRequestAndResponse
|
|
debounceWait: number
|
|
}>
|
|
) {
|
|
if (process.server) return
|
|
const { singleClickCallback, doubleClickCallback } = params
|
|
const state = options?.state || useInjectedViewerState()
|
|
const {
|
|
viewer: { instance }
|
|
} = state
|
|
const { debounceWait = 50 } = options || {}
|
|
|
|
const debouncedSingleClickCallback = singleClickCallback
|
|
? debounce((event: Nullable<SelectionEvent>) => {
|
|
const firstVisibleSelectionHit = event
|
|
? getFirstVisibleSelectionHit(event, state)
|
|
: null
|
|
return singleClickCallback(event, { firstVisibleSelectionHit })
|
|
}, debounceWait)
|
|
: undefined
|
|
const debouncedDoubleClickCallback = doubleClickCallback
|
|
? debounce((event: Nullable<SelectionEvent>) => {
|
|
const firstVisibleSelectionHit = event
|
|
? getFirstVisibleSelectionHit(event, state)
|
|
: null
|
|
return doubleClickCallback(event, { firstVisibleSelectionHit })
|
|
}, debounceWait)
|
|
: undefined
|
|
|
|
onMounted(() => {
|
|
if (debouncedDoubleClickCallback) {
|
|
instance.on(ViewerEvent.ObjectDoubleClicked, debouncedDoubleClickCallback)
|
|
}
|
|
if (debouncedSingleClickCallback) {
|
|
instance.on(ViewerEvent.ObjectClicked, debouncedSingleClickCallback)
|
|
}
|
|
})
|
|
|
|
onBeforeUnmount(() => {
|
|
if (debouncedDoubleClickCallback) {
|
|
instance.removeListener(
|
|
ViewerEvent.ObjectDoubleClicked,
|
|
debouncedDoubleClickCallback
|
|
)
|
|
}
|
|
if (debouncedSingleClickCallback) {
|
|
instance.removeListener(ViewerEvent.ObjectClicked, debouncedSingleClickCallback)
|
|
}
|
|
})
|
|
}
|
|
|
|
export function useGetObjectUrl() {
|
|
const config = useRuntimeConfig()
|
|
return (projectId: string, objectId: string) =>
|
|
`${config.public.apiOrigin}/streams/${projectId}/objects/${objectId}`
|
|
}
|
|
|
|
export function useOnViewerLoadComplete(
|
|
listener: (params: { isInitial: boolean }) => MaybeAsync<void>,
|
|
options?: Partial<{
|
|
/**
|
|
* Whether to only invoke the listener once on the very first LoadComplete event. Default: false
|
|
*/
|
|
initialOnly: boolean
|
|
/**
|
|
* If true, will trigger the listener after the next isBusy=false event that comes after LoadComplete. Default: true
|
|
*/
|
|
waitForBusyOver: boolean
|
|
}>
|
|
) {
|
|
const {
|
|
ui: { viewerBusy }
|
|
} = useInjectedViewerState()
|
|
const { initialOnly, waitForBusyOver = true } = options || {}
|
|
|
|
const hasRun = ref(false)
|
|
|
|
const cancel = useViewerEventListener(ViewerEvent.LoadComplete, async () => {
|
|
if (initialOnly && hasRun.value) {
|
|
cancel()
|
|
return
|
|
}
|
|
|
|
try {
|
|
await (waitForBusyOver
|
|
? Promise.race([
|
|
until(viewerBusy).toBe(false),
|
|
timeoutAt(
|
|
1000,
|
|
'Waiting for viewer business to be over post-LoadComplete timed out'
|
|
)
|
|
])
|
|
: Promise.resolve())
|
|
} catch (e) {
|
|
if (!(e instanceof TimeoutError)) throw e
|
|
console.warn(e.message)
|
|
}
|
|
|
|
listener({ isInitial: !hasRun.value })
|
|
hasRun.value = true
|
|
|
|
if (initialOnly) cancel()
|
|
})
|
|
}
|