91cb011ded
* CodeInput. verify-email page * middleware * Loading toast * Countdown only for registration * Improve middleware * Fix middleware breaking auth flow * Remove old notifications * Remove old onboarding. New segmentation * Remove skip button * Block verify email when verified * useUserEmails composable. Cancel addition * Move user emails queries * Fix fragments etc * redirect updates * HeaderWithEmptyPage * Check env before enforcing * Join workspace * Updates * Fix console warnings on login * Fix register console warnings * Working cache updates * Verify secondary email * Force onboarding off * EMAIL WIP * useIsJustRegistered state * Improve isRequired * Uneeded change * Improved slots * Updates from CR * CR comments * Only show message if forced * Update onboarding middleware * Update loading bar * ref > computed to fix onboarding * Resend tooltip. Better errors * Add other to form. * Email changes * Updates to emails * Remove force email FF * Remove FF's * Hide header on embed * Update graphql.ts * Re-add FF * Update graphql.ts * GQL Fragments * Fix build
584 lines
15 KiB
TypeScript
584 lines
15 KiB
TypeScript
import { SpeckleViewer, timeoutAt } from '@speckle/shared'
|
|
import type {
|
|
TreeNode,
|
|
MeasurementOptions,
|
|
PropertyInfo,
|
|
ViewMode
|
|
} from '@speckle/viewer'
|
|
import { MeasurementsExtension, ViewModes } from '@speckle/viewer'
|
|
import { until } from '@vueuse/shared'
|
|
import { difference, isString, uniq } from 'lodash-es'
|
|
import { useEmbedState, useEmbed } 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 { Vector3, Box3 } from 'three'
|
|
import { getKeyboardShortcutTitle, onKeyboardShortcut } from '@speckle/ui-components'
|
|
import { ViewerShortcuts } from '~/lib/viewer/helpers/shortcuts/shortcuts'
|
|
import type {
|
|
ViewerShortcut,
|
|
ViewerShortcutAction
|
|
} from '~/lib/viewer/helpers/shortcuts/types'
|
|
import { useActiveElement } from '@vueuse/core'
|
|
import { SnowPipeline } from '~/lib/viewer/pipelines/snow/SnowPipeline'
|
|
|
|
export function useSectionBoxUtilities() {
|
|
const { instance } = useInjectedViewer()
|
|
const {
|
|
sectionBox,
|
|
sectionBoxContext: { visible, edited },
|
|
filters: { selectedObjects },
|
|
threads: {
|
|
openThread: { thread }
|
|
}
|
|
} = useInjectedViewerInterfaceState()
|
|
|
|
const isSectionBoxEnabled = computed(() => !!sectionBox.value)
|
|
const isSectionBoxVisible = computed(() => visible.value)
|
|
const isSectionBoxEdited = computed(() => edited.value)
|
|
|
|
const resolveSectionBoxFromSelection = () => {
|
|
const objectIds = selectedObjects.value.map((o) => o.id).filter(isNonNullable)
|
|
const box = instance.getSectionBoxFromObjects(objectIds)
|
|
sectionBox.value = box
|
|
}
|
|
|
|
const toggleSectionBox = () => {
|
|
if (!isSectionBoxEnabled.value) {
|
|
resolveSectionBoxFromSelection()
|
|
return
|
|
}
|
|
|
|
if (isSectionBoxVisible.value) {
|
|
visible.value = false
|
|
} else {
|
|
visible.value = true
|
|
}
|
|
}
|
|
|
|
const resetSectionBox = () => {
|
|
const serializedSectionBox = thread.value?.viewerState?.ui.sectionBox
|
|
sectionBox.value = null
|
|
|
|
if (serializedSectionBox) {
|
|
// Same logic we have in deserialization
|
|
sectionBox.value = new Box3(
|
|
new Vector3(
|
|
serializedSectionBox.min[0],
|
|
serializedSectionBox.min[1],
|
|
serializedSectionBox.min[2]
|
|
),
|
|
new Vector3(
|
|
serializedSectionBox.max[0],
|
|
serializedSectionBox.max[1],
|
|
serializedSectionBox.max[2]
|
|
)
|
|
)
|
|
}
|
|
}
|
|
|
|
return {
|
|
isSectionBoxEnabled,
|
|
isSectionBoxVisible,
|
|
isSectionBoxEdited,
|
|
toggleSectionBox,
|
|
resetSectionBox,
|
|
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 embedMode = useEmbedState()
|
|
|
|
const showControls = computed(() => {
|
|
if (
|
|
embedMode.embedOptions.value?.isEnabled &&
|
|
embedMode.embedOptions.value.hideControls
|
|
) {
|
|
return false
|
|
}
|
|
|
|
return true
|
|
})
|
|
|
|
const showNavbar = computed(() => {
|
|
if (!showControls.value) 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
|
|
}
|
|
}
|
|
|
|
export function useViewModeUtilities() {
|
|
const { instance } = useInjectedViewer()
|
|
const { viewMode } = useInjectedViewerInterfaceState()
|
|
|
|
const currentViewMode = computed(() => viewMode.value)
|
|
|
|
const setViewMode = (mode: ViewMode) => {
|
|
const viewModes = instance.getExtension(ViewModes)
|
|
if (viewModes) {
|
|
viewModes.setViewMode(mode)
|
|
}
|
|
}
|
|
|
|
const letItSnow = () => {
|
|
const snowPipeline = new SnowPipeline(instance.getRenderer())
|
|
instance.getRenderer().pipeline = snowPipeline
|
|
void snowPipeline.start()
|
|
}
|
|
|
|
return {
|
|
currentViewMode,
|
|
setViewMode,
|
|
letItSnow
|
|
}
|
|
}
|
|
|
|
export function useViewerShortcuts() {
|
|
const { ui } = useInjectedViewerState()
|
|
const { isSmallerOrEqualSm } = useIsSmallerOrEqualThanBreakpoint()
|
|
const { isEnabled: isEmbedEnabled } = useEmbed()
|
|
const activeElement = useActiveElement()
|
|
|
|
const isTypingComment = computed(() => {
|
|
if (
|
|
activeElement.value &&
|
|
(activeElement.value.tagName.toLowerCase() === 'input' ||
|
|
activeElement.value.tagName.toLowerCase() === 'textarea' ||
|
|
activeElement.value.getAttribute('contenteditable') === 'true')
|
|
) {
|
|
return true
|
|
}
|
|
|
|
// Check thread editor states
|
|
const isNewThreadEditorOpen = ui.threads.openThread.newThreadEditor.value
|
|
const isExistingThreadEditorOpen = !!ui.threads.openThread.thread.value
|
|
|
|
return isNewThreadEditorOpen || isExistingThreadEditorOpen
|
|
})
|
|
|
|
const formatKey = (key: string) => {
|
|
if (key.startsWith('Digit')) {
|
|
return key.slice(5)
|
|
}
|
|
return key
|
|
}
|
|
|
|
const getShortcutDisplayText = (
|
|
shortcut: ViewerShortcut,
|
|
options?: { hideName?: boolean }
|
|
) => {
|
|
if (isSmallerOrEqualSm.value) return undefined
|
|
if (isEmbedEnabled.value) return undefined
|
|
|
|
const shortcutText = getKeyboardShortcutTitle([
|
|
...shortcut.modifiers,
|
|
formatKey(shortcut.key)
|
|
])
|
|
|
|
if (!options?.hideName) {
|
|
return `${shortcut.name} (${shortcutText})`
|
|
}
|
|
|
|
return shortcutText
|
|
}
|
|
|
|
const disableShortcuts = computed(() => isTypingComment.value || isEmbedEnabled.value)
|
|
|
|
const registerShortcuts = (
|
|
handlers: Partial<Record<ViewerShortcutAction, () => void>>
|
|
) => {
|
|
Object.values(ViewerShortcuts).forEach((shortcut) => {
|
|
const handler = handlers[shortcut.action as ViewerShortcutAction]
|
|
if (handler) {
|
|
onKeyboardShortcut([...shortcut.modifiers], shortcut.key, () => {
|
|
if (!disableShortcuts.value) handler()
|
|
})
|
|
}
|
|
})
|
|
}
|
|
|
|
return {
|
|
shortcuts: ViewerShortcuts,
|
|
registerShortcuts,
|
|
getShortcutDisplayText
|
|
}
|
|
}
|