Files
speckle-server/packages/frontend-2/lib/viewer/composables/setup/postSetup.ts
T
Kristaps Fabians Geikins bde148f286 chore(server): migrating fully to ESM (#5042)
* wip

* some extra fixes

* stuff kinda works?

* need to figure out mocks

* need to figure out mocks

* fix db listener

* gqlgen fix

* minor gqlgen watch adjustment

* lint fixes

* delete old codegen file

* converting migrations to ESM

* getModuleDIrectory

* vitest sort of works

* added back ts-vitest

* resolve gql double load

* fixing test timeout configs

* TSC lint fix

* fix automate tests

* moar debugging

* debugging

* more debugging

* codegen update

* server works

* yargs migrated

* chore(server): getting rid of global mocks for Server ESM (#5046)

* got rid of email mock

* got rid of comment mocks

* got rid of multi region mocks

* got rid of stripe mock

* admin override mock updated

* removed final mock

* fixing import.meta.resolve calls

* another import.meta.resolve fix

* added requested test

* nyc ESM fix

* removed unneeded deps + linting

* yarn lock forgot to commit

* tryna fix flakyness

* email capture util fix

* sendEmail fix

* fix TSX check

* sender transporter fix + CR comments

* merge main fix

* test fixx

* circleci fix

* gqlgen bigint fix

* error formatter fix

* more error formatting improvements

* esmloader added to Dockerfile

* more dockerfile fixes

* bg jobs fix
2025-07-14 10:26:19 +03:00

922 lines
26 KiB
TypeScript

import { difference, flatten, isEqual, uniq } from 'lodash-es'
import { useThrottleFn, onKeyStroke, watchTriggerable } from '@vueuse/core'
import {
ExplodeEvent,
ExplodeExtension,
LoaderEvent,
type PropertyInfo,
type StringPropertyInfo,
type SunLightConfiguration
} from '@speckle/viewer'
import {
ViewerEvent,
VisualDiffMode,
CameraController,
UpdateFlags,
SectionOutlines,
SectionToolEvent,
SectionTool,
SpeckleLoader
} from '@speckle/viewer'
import { useAuthCookie } 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
} from '~~/lib/viewer/composables/setup'
import { useViewerSelectionEventHandler } from '~~/lib/viewer/composables/setup/selection'
import {
useGetObjectUrl,
useOnViewerLoadComplete,
useViewerCameraControlStartTracker,
useViewerCameraTracker,
useViewerEventListener
} from '~~/lib/viewer/composables/viewer'
import { useViewerCommentUpdateTracking } from '~~/lib/viewer/composables/commentManagement'
import { getCacheId } from '~~/lib/common/helpers/graphql'
import {
useViewerOpenedThreadUpdateEmitter,
useViewerThreadTracking
} from '~~/lib/viewer/composables/commentBubbles'
import { useGeneralProjectPageUpdateTracking } from '~~/lib/projects/composables/projectPages'
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 {
useCameraUtilities,
useMeasurementUtilities
} from '~~/lib/viewer/composables/ui'
import { setupDebugMode } from '~~/lib/viewer/composables/setup/dev'
import { useEmbed } from '~/lib/viewer/composables/setup/embed'
import { useMixpanel } from '~~/lib/core/composables/mp'
import type { SectionBoxData } from '@speckle/shared/viewer/state'
function useViewerIsBusyEventHandler() {
const state = useInjectedViewerState()
const callback = (isBusy: boolean) => {
state.ui.viewerBusy.value = isBusy
}
onMounted(() => {
state.viewer.instance.on(ViewerEvent.Busy, callback)
})
onBeforeUnmount(() => {
state.viewer.instance.removeListener(ViewerEvent.Busy, callback)
})
}
/**
* Automatically loads & unloads objects into the viewer depending on the global URL resource identifier state
*/
function useViewerObjectAutoLoading() {
if (import.meta.server) return
const disableViewerCache =
SafeLocalStorage.get('FE2_FORCE_DISABLE_VIEWER_CACHE') === 'true'
const authToken = useAuthCookie()
const getObjectUrl = useGetObjectUrl()
const {
projectId,
viewer: {
instance: viewer,
init: { ref: isInitialized },
hasDoneInitialLoad
},
resources: {
response: { resourceItems }
},
ui: { loadProgress },
urlHashState: { focusedThreadId }
} = useInjectedViewerState()
const loadingProgressMap: { [id: string]: number } = {}
viewer.on(ViewerEvent.LoadComplete, (id) => {
delete loadingProgressMap[id]
consolidateProgressInternal({ id, progress: 1 })
})
const consolidateProgressInternal = (args: { progress: number; id: string }) => {
loadingProgressMap[args.id] = args.progress
let min = 42
const values = Object.values(loadingProgressMap) as number[]
for (const num of values) {
min = Math.min(min, num)
}
loadProgress.value = min
}
const consolidateProgressThorttled = useThrottleFn(consolidateProgressInternal, 250)
const loadObject = async (
objectId: string,
unload?: boolean,
options?: Partial<{ zoomToObject: boolean }>
) => {
const objectUrl = getObjectUrl(projectId.value, objectId)
if (unload) {
return viewer.unloadObject(objectUrl)
} else {
const loader = new SpeckleLoader(
viewer.getWorldTree(),
objectUrl,
authToken.value || undefined,
disableViewerCache ? false : undefined,
undefined
)
loader.on(LoaderEvent.LoadProgress, (args) => consolidateProgressThorttled(args))
loader.on(LoaderEvent.LoadCancelled, (id) => {
delete loadingProgressMap[id]
consolidateProgressInternal({ id, progress: 1 })
})
return viewer.loadObject(loader, options?.zoomToObject)
}
}
const getUniqueObjectIds = (resourceItems: ViewerResourceItem[]) =>
uniq(resourceItems.map((i) => i.objectId))
watch(
() => <const>[resourceItems.value, isInitialized.value, hasDoneInitialLoad.value],
async ([newResources, newIsInitialized, newHasDoneInitialLoad], oldData) => {
// Wait till viewer loaded in
if (!newIsInitialized) return
const [oldResources] = oldData || [[], false]
const zoomToObject = !focusedThreadId.value // we want to zoom to the thread instead
// Viewer initialized - load in all resources
if (!newHasDoneInitialLoad) {
const allObjectIds = getUniqueObjectIds(newResources)
/** Load sequentially */
const res = []
for (const i of allObjectIds) {
res.push(await loadObject(i, false, { zoomToObject }))
}
/** Load in parallel */
// const res = await Promise.all(
// allObjectIds.map((i) => loadObject(i, false, { zoomToObject }))
// )
if (res.length) {
hasDoneInitialLoad.value = true
}
return
}
// Resources changed?
const newObjectIds = getUniqueObjectIds(newResources)
const oldObjectIds = getUniqueObjectIds(oldResources)
const removableObjectIds = difference(oldObjectIds, newObjectIds)
const addableObjectIds = difference(newObjectIds, oldObjectIds)
await Promise.all(removableObjectIds.map((i) => loadObject(i, true)))
await Promise.all(
addableObjectIds.map((i) => loadObject(i, false, { zoomToObject: false }))
)
},
{ deep: true, immediate: true }
)
onBeforeUnmount(async () => {
await viewer.unloadAll()
})
}
/**
* Here we make the viewer pretend it's a connector and send out receive events. Note, this is important for us to track to be able to get a picture of how much data is consumed
* in our viewer.
*/
function useViewerReceiveTracking() {
//
const {
resources: {
response: { modelsAndVersionIds }
}
} = useInjectedViewerState()
const mixpanel = useMixpanel()
const { userId } = useActiveUser()
const receivedVersions = new Set<string>()
watch(modelsAndVersionIds, (newVal) => {
for (const { model, versionId } of newVal) {
if (receivedVersions.has(versionId)) {
continue
}
receivedVersions.add(versionId)
mixpanel.track('Receive', {
hostApp: 'viewer',
sourceHostApp: model.loadedVersion.items[0].sourceApplication,
isMultiplayer: model.loadedVersion.items[0].authorUser?.id !== userId.value
})
}
})
}
/**
* Listening to model/version updates through subscriptions and making various
* cache updates so that we don't need to always refetch queries
*/
function useViewerSubscriptionEventTracker() {
if (import.meta.server) return
const {
projectId,
resources: {
request: { resourceIdString, threadFilters }
}
} = useInjectedViewerState()
// Track all project/model/version updates
useGeneralProjectPageUpdateTracking({
projectId
})
// Also track updates to comments
useViewerCommentUpdateTracking(
{
projectId,
resourceIdString,
loadedVersionsOnly: computed(() => threadFilters.value.loadedVersionsOnly)
},
(event, cache) => {
const isArchived = event.type === ProjectCommentsUpdatedMessageType.Archived
const isNew = event.type === ProjectCommentsUpdatedMessageType.Created
const comment = event.comment
if (isArchived) {
// Mark as archived
cache.modify({
id: getCacheId('Comment', event.id),
fields: {
archived: () => true
}
})
// Remove from project.commentThreads
modifyObjectField(
cache,
getCacheId('Project', projectId.value),
'commentThreads',
({ variables, helpers: { createUpdatedValue, readField } }) => {
if (variables.filter?.includeArchived) return // we want it in that list
return createUpdatedValue(({ update }) => {
update('totalCount', (totalCount) => totalCount - 1)
update('items', (items) =>
items.filter((i) => readField(i, 'id') !== event.id)
)
})
}
)
} else if (isNew && comment) {
const parentId = comment.parent?.id
// Add reply to parent
if (parentId) {
modifyObjectField(
cache,
getCacheId('Comment', parentId),
'replies',
({ helpers: { createUpdatedValue, ref } }) =>
createUpdatedValue(({ update }) => {
update('totalCount', (totalCount) => totalCount + 1)
update('items', (items) => [ref('Comment', comment.id), ...items])
})
)
} else {
// Add comment thread
modifyObjectField(
cache,
getCacheId('Project', projectId.value),
'commentThreads',
({ helpers: { ref, createUpdatedValue, readField }, value }) => {
// In case this is actually an unarchived comment, we only want to add it if it doesnt
// exist in the includesArchived list already
const includesItem = value.items?.find(
(i) => readField(i, 'id') === comment.id
)
if (includesItem) return
return createUpdatedValue(({ update }) => {
update('totalCount', (totalCount) => totalCount + 1)
update('items', (items) => [ref('Comment', comment.id), ...items])
})
}
)
}
}
}
)
}
function sectionBoxDataEquals(a: SectionBoxData, b: SectionBoxData): boolean {
const isEqual = (a: number[], b: number[]) =>
a.length === b.length && a.every((v, i) => Math.abs(v - b[i]) < 1e-6)
return (
isEqual(a.min, b.min) &&
isEqual(a.max, b.max) &&
(a.rotation && b.rotation ? isEqual(a.rotation, b.rotation) : true)
)
}
function useViewerSectionBoxIntegration() {
const {
ui: {
sectionBox,
sectionBoxContext: { visible, edited }
},
viewer: { instance }
} = useInjectedViewerState()
// Change edited=true when user starts changing the section box by dragging it
const sectionTool = instance.getExtension(SectionTool)
const onDragStart = () => {
edited.value = true
}
sectionTool.on(SectionToolEvent.DragStart, onDragStart)
// No two-way sync for section boxes, because once you set a Box3 into the viewer
// the viewer transforms it into something else causing the updates going into an infinite loop
// state -> viewer
watch(
sectionBox,
(newVal, oldVal) => {
if (newVal && oldVal && sectionBoxDataEquals(newVal, oldVal)) return
if (!newVal && !oldVal) return
if (oldVal && !newVal) {
visible.value = false
edited.value = false
instance.sectionBoxOff()
instance.requestRender(UpdateFlags.RENDER_RESET)
return
}
if (newVal && (!oldVal || !sectionBoxDataEquals(newVal, oldVal))) {
visible.value = true
edited.value = false
instance.setSectionBox(newVal)
instance.sectionBoxOn()
const outlines = instance.getExtension(SectionOutlines)
if (outlines) outlines.requestUpdate()
instance.requestRender(UpdateFlags.RENDER_RESET)
}
},
{ immediate: true, deep: true, flush: 'sync' }
)
watch(
visible,
(newVal, oldVal) => {
if (newVal && oldVal) return
if (!newVal && !oldVal) return
if (newVal) {
sectionTool.visible = true
} else {
sectionTool.visible = false
}
instance.requestRender()
},
{ immediate: true, deep: true, flush: 'sync' }
)
onBeforeUnmount(() => {
instance.sectionBoxOff()
sectionTool.removeListener(SectionToolEvent.DragStart, onDragStart)
})
}
function useViewerCameraIntegration() {
const {
viewer: { instance },
ui: {
camera: { isOrthoProjection, position, target },
spotlightUserSessionId
}
} = useInjectedViewerState()
const { forceViewToViewerSync } = useCameraUtilities()
const hasInitialLoadFired = ref(false)
const loadCameraDataFromViewer = () => {
const extension: CameraController = instance.getExtension(CameraController)
let cameraManuallyChanged = false
const viewerPos = new Vector3().copy(extension.getPosition())
const viewerTarget = new Vector3().copy(extension.getTarget())
if (!areVectorsLooselyEqual(position.value, viewerPos)) {
if (hasInitialLoadFired.value) position.value = viewerPos.clone()
cameraManuallyChanged = true
}
if (!areVectorsLooselyEqual(target.value, viewerTarget)) {
if (hasInitialLoadFired.value) target.value = viewerTarget.clone()
cameraManuallyChanged = true
}
return cameraManuallyChanged
}
// viewer -> state
// debouncing pos/target updates to avoid jitteriness + spotlight mode unnecessarily disabling
useViewerCameraTracker(
() => {
loadCameraDataFromViewer()
}
// { debounceWait: 100 }
)
useOnViewerLoadComplete(({ isInitial }) => {
if (isInitial) {
hasInitialLoadFired.value = true
// Load camera position so we can return to it correctly
// ONLY if we don't already have specific coordinates (e.g. from opened thread)
// otherwise - load current pos/target into viewer
const hasInitCoords =
position.value.equals(new Vector3()) && target.value.equals(new Vector3())
if (hasInitCoords) {
loadCameraDataFromViewer()
} else {
forceViewToViewerSync()
}
// Only now set projection, we can't do it too early
orthoProjectionUpdate(isOrthoProjection.value)
} else {
loadCameraDataFromViewer()
}
})
useViewerCameraControlStartTracker(() => {
if (spotlightUserSessionId.value) {
spotlightUserSessionId.value = null // cancel
}
})
const orthoProjectionUpdate = (newVal: boolean) => {
if (!hasInitialLoadFired.value) {
throw new Error('Attempting to set projection too early')
}
if (newVal) {
instance.setOrthoCameraOn()
} else {
instance.setPerspectiveCameraOn()
}
// reset camera pos, cause we've switched cameras now and it might not have the new ones
forceViewToViewerSync()
}
// state -> viewer
watch(
isOrthoProjection,
(newVal, oldVal) => {
if (newVal === oldVal || !hasInitialLoadFired.value) return
orthoProjectionUpdate(newVal)
},
{ immediate: true }
)
watch(
position,
(newVal, oldVal) => {
if ((!newVal && !oldVal) || (oldVal && areVectorsLooselyEqual(newVal, oldVal))) {
return
}
instance.setView({
position: newVal,
target: target.value
})
}
// { immediate: true }
)
watch(
target,
(newVal, oldVal) => {
if ((!newVal && !oldVal) || (oldVal && areVectorsLooselyEqual(newVal, oldVal))) {
return
}
instance.setView({
position: position.value,
target: newVal
})
}
// { immediate: true }
)
}
function useViewerFiltersIntegration() {
const {
viewer: { instance },
ui: { filters, highlightedObjectIds }
} = useInjectedViewerState()
const {
metadata: { availableFilters: allFilters }
} = useInjectedViewer()
const stateKey = 'default'
let preventFilterWatchers = false
const withWatchersDisabled = (fn: () => void) => {
const isAlreadyInPreventScope = !!preventFilterWatchers
preventFilterWatchers = true
fn()
if (!isAlreadyInPreventScope) preventFilterWatchers = false
}
const speckleTypeFilter = computed(
() => allFilters.value?.find((f) => f.key === 'speckle_type') as StringPropertyInfo
)
// state -> viewer
watch(
highlightedObjectIds,
(newVal, oldVal) => {
if (arraysEqual(newVal, oldVal || [])) return
instance.highlightObjects(newVal)
},
{ immediate: true, flush: 'sync' }
)
watch(
filters.isolatedObjectIds,
(newVal, oldVal) => {
if (preventFilterWatchers) return
if (arraysEqual(newVal, oldVal || [])) return
const isolatable = difference(newVal, oldVal || [])
const unisolatable = difference(oldVal || [], newVal)
if (isolatable.length) {
withWatchersDisabled(() => {
instance.isolateObjects(isolatable, stateKey, true)
filters.hiddenObjectIds.value = []
})
}
if (unisolatable.length) {
withWatchersDisabled(() => {
instance.unIsolateObjects(unisolatable, stateKey, true)
filters.hiddenObjectIds.value = []
})
}
},
{ immediate: true, flush: 'sync' }
)
watch(
filters.hiddenObjectIds,
(newVal, oldVal) => {
if (preventFilterWatchers) return
if (arraysEqual(newVal, oldVal || [])) return
const hidable = difference(newVal, oldVal || [])
const showable = difference(oldVal || [], newVal)
if (hidable.length) {
withWatchersDisabled(() => {
instance.hideObjects(hidable, stateKey, true)
filters.isolatedObjectIds.value = []
})
}
if (showable.length) {
withWatchersDisabled(() => {
instance.showObjects(showable, stateKey, true)
filters.isolatedObjectIds.value = []
})
}
},
{ immediate: true, flush: 'sync' }
)
const syncColorFilterToViewer = async (
filter: Nullable<PropertyInfo>,
isApplied: boolean
) => {
const targetFilter = filter || speckleTypeFilter.value
if (isApplied && targetFilter) await instance.setColorFilter(targetFilter)
if (!isApplied) await instance.removeColorFilter()
}
watch(
() =>
<const>[
filters.propertyFilter.filter.value,
filters.propertyFilter.isApplied.value
],
async (newVal) => {
const [filter, isApplied] = newVal
await syncColorFilterToViewer(filter, isApplied)
},
{ immediate: true, flush: 'sync' }
)
useOnViewerLoadComplete(
async () => {
const targetFilter =
filters.propertyFilter.filter.value || speckleTypeFilter.value
const isApplied = filters.propertyFilter.isApplied.value
await syncColorFilterToViewer(targetFilter, isApplied)
},
{ initialOnly: true }
)
watch(
filters.selectedObjects,
(newVal, oldVal) => {
const newIds = flatten(newVal.map((v) => getTargetObjectIds({ ...v }))).filter(
isNonNullable
)
const oldIds = flatten(
(oldVal || []).map((v) => getTargetObjectIds({ ...v }))
).filter(isNonNullable)
if (arraysEqual(newIds, oldIds)) return
if (!newVal.length) {
instance.resetSelection()
return
}
instance.selectObjects(newIds)
},
{ immediate: true, flush: 'sync' }
)
}
function useLightConfigIntegration() {
const {
ui: { lightConfig },
viewer: { instance }
} = useInjectedViewerState()
// viewer -> state
useViewerEventListener(
ViewerEvent.LightConfigUpdated,
(config: SunLightConfiguration) => {
if (isEqual(lightConfig.value, config)) return
lightConfig.value = config
}
)
// state -> viewer
watch(
lightConfig,
(newVal, oldVal) => {
if (newVal && oldVal && isEqual(newVal, oldVal)) return
instance.setLightConfiguration(newVal)
},
{
immediate: true,
deep: true,
flush: 'sync'
}
)
useOnViewerLoadComplete(
() => {
instance.setLightConfiguration(lightConfig.value)
},
{ initialOnly: true }
)
}
function useExplodeFactorIntegration() {
const {
ui: { explodeFactor },
viewer: { instance }
} = useInjectedViewerState()
const updateOutlines = () => {
const sectionOutlines = instance.getExtension(SectionOutlines)
if (sectionOutlines && sectionOutlines.enabled) sectionOutlines.requestUpdate(true)
}
onMounted(() => {
instance.getExtension(ExplodeExtension).on(ExplodeEvent.Finshed, updateOutlines)
})
onBeforeUnmount(() => {
instance
.getExtension(ExplodeExtension)
.removeListener(ExplodeEvent.Finshed, updateOutlines)
})
// state -> viewer only. we don't need the reverse.
watch(
explodeFactor,
(newVal) => {
/** newVal turns out to be a string. It needs to be a */
instance.explode(newVal)
},
{ immediate: true }
)
useOnViewerLoadComplete(
() => {
instance.explode(explodeFactor.value)
},
{ initialOnly: true }
)
}
function useDiffingIntegration() {
const state = useInjectedViewerState()
const authCookie = useAuthCookie()
const getObjectUrl = useGetObjectUrl()
const hasInitialLoadFired = ref(false)
const { trigger: triggerDiffCommandWatch } = watchTriggerable(
() => <const>[state.ui.diff.oldVersion.value, state.ui.diff.newVersion.value],
async (newVal, oldVal) => {
if (!hasInitialLoadFired.value) return
const [oldVersion, newVersion] = newVal
const [oldOldVersion, oldNewVersion] = oldVal || [null, null]
const versionId = (version: typeof oldOldVersion) => version?.id || null
const commandId = (
oldVersion: typeof oldOldVersion,
newVersion: typeof oldOldVersion
) => {
const oldId = versionId(oldVersion)
const newId = versionId(newVersion)
return oldId && newId ? `${oldId}->${newId}` : null
}
const newCommand = commandId(oldVersion, newVersion)
const oldCommand = commandId(oldOldVersion, oldNewVersion)
if ((newCommand && oldCommand === newCommand) || !!newCommand === !!oldCommand)
return
if (!newCommand || oldVal) {
await state.viewer.instance.undiff()
if (!newCommand) return
}
// values shouldn't be undefined cause commandId() generation succeeded
const oldObjUrl = getObjectUrl(
state.projectId.value,
oldVersion?.referencedObject as string
)
const newObjUrl = getObjectUrl(
state.projectId.value,
newVersion?.referencedObject as string
)
state.ui.diff.result.value = await state.viewer.instance.diff(
oldObjUrl,
newObjUrl,
state.ui.diff.mode.value,
authCookie.value
)
},
{ immediate: true }
)
// const preventWatchers = 0
watch(state.ui.diff.result, (val) => {
if (!val) return
// reset visual diff time and mode on new diff result
// sometimes the watcher won't fire even when the values are updated, because they're updated to
// the same values that they were already. because of that we're manually & forcefully running
// the relevant watchers when diffResult changes
ignoreDiffModeUpdates(() => {
ignoreDiffTimeUpdates(() => {
state.ui.diff.time.value = 0.5
state.ui.diff.mode.value = VisualDiffMode.COLORED
// this watcher also updates diffTime, so no need to invoke that separately
triggerDiffModeWatch()
})
})
})
const { ignoreUpdates: ignoreDiffTimeUpdates } = watchTriggerable(
state.ui.diff.time,
(val) => {
if (!hasInitialLoadFired.value) return
if (!state.ui.diff.result.value) return
state.viewer.instance.setDiffTime(state.ui.diff.result.value, val)
}
)
const { trigger: triggerDiffModeWatch, ignoreUpdates: ignoreDiffModeUpdates } =
watchTriggerable(state.ui.diff.mode, (val) => {
if (!hasInitialLoadFired.value) return
if (!state.ui.diff.result.value) return
state.viewer.instance.setVisualDiffMode(state.ui.diff.result.value, val)
state.viewer.instance.setDiffTime(
state.ui.diff.result.value,
state.ui.diff.time.value
) // hmm, why do i need to call diff time again? seems like a minor viewer bug
})
useOnViewerLoadComplete(({ isInitial }) => {
if (!isInitial) return
hasInitialLoadFired.value = true
triggerDiffCommandWatch()
})
}
function useViewerMeasurementIntegration() {
const {
ui: { measurement },
viewer: { instance }
} = useInjectedViewerState()
const { clearMeasurements, removeMeasurement } = useMeasurementUtilities()
onBeforeUnmount(() => {
clearMeasurements()
})
watch(
() => measurement.enabled.value,
(newVal, oldVal) => {
if (newVal !== oldVal) {
instance.enableMeasurements(newVal)
}
},
{ immediate: true }
)
watch(
() => ({ ...measurement.options.value }),
(newMeasurementState) => {
if (newMeasurementState) {
instance.setMeasurementOptions(newMeasurementState)
}
},
{ immediate: true, deep: true }
)
onKeyStroke('Delete', () => {
removeMeasurement()
})
onKeyStroke('Backspace', () => {
removeMeasurement()
})
}
function useDisableZoomOnEmbed() {
const { viewer } = useInjectedViewerState()
const embedOptions = useEmbed()
watch(
() => embedOptions.noScroll.value,
(newNoScrollValue) => {
const cameraController: CameraController =
viewer.instance.getExtension(CameraController)
if (newNoScrollValue) {
cameraController.options = { enableZoom: false }
} else {
cameraController.options = { enableZoom: true }
}
},
{ immediate: true }
)
}
export function useViewerPostSetup() {
if (import.meta.server) return
useViewerObjectAutoLoading()
useViewerReceiveTracking()
useViewerSelectionEventHandler()
useViewerIsBusyEventHandler()
useViewerSubscriptionEventTracker()
useViewerThreadTracking()
useViewerOpenedThreadUpdateEmitter()
useViewerSectionBoxIntegration()
useViewerCameraIntegration()
useViewerFiltersIntegration()
useLightConfigIntegration()
useExplodeFactorIntegration()
useDiffingIntegration()
useViewerMeasurementIntegration()
useDisableZoomOnEmbed()
setupDebugMode()
}