bde148f286
* 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
329 lines
11 KiB
TypeScript
329 lines
11 KiB
TypeScript
import {
|
|
useInjectedViewerState,
|
|
useResetUiState
|
|
} from '~~/lib/viewer/composables/setup'
|
|
import { SpeckleViewer, TimeoutError } from '@speckle/shared'
|
|
import { get } from 'lodash-es'
|
|
import { Vector3 } from 'three'
|
|
import {
|
|
useDiffUtilities,
|
|
useFilterUtilities,
|
|
useSelectionUtilities
|
|
} from '~~/lib/viewer/composables/ui'
|
|
import { CameraController, ViewMode, VisualDiffMode } from '@speckle/viewer'
|
|
import type { NumericPropertyInfo } from '@speckle/viewer'
|
|
import type { PartialDeep } from 'type-fest'
|
|
import type { SectionBoxData } from '@speckle/shared/viewer/state'
|
|
|
|
type SerializedViewerState = SpeckleViewer.ViewerState.SerializedViewerState
|
|
|
|
export function useStateSerialization() {
|
|
const state = useInjectedViewerState()
|
|
const { objects: selectedObjects } = useSelectionUtilities()
|
|
const { serializeDiffCommand } = useDiffUtilities()
|
|
|
|
/**
|
|
* We don't want to save a comment w/ implicit identifiers like ones that only have a model ID or a folder prefix, because
|
|
* those can resolve to completely different versions/objects as time goes on
|
|
*/
|
|
const buildConcreteResourceIdString = () => {
|
|
const resources = state.resources.response.resourceItems
|
|
const builder = SpeckleViewer.ViewerRoute.resourceBuilder()
|
|
|
|
for (const resource of resources.value) {
|
|
if (resource.modelId && resource.versionId) {
|
|
builder.addModel(resource.modelId, resource.versionId)
|
|
} else {
|
|
builder.addObject(resource.objectId)
|
|
}
|
|
}
|
|
|
|
const finalString = builder.toString()
|
|
return finalString || state.resources.request.resourceIdString.value
|
|
}
|
|
|
|
const serialize = (
|
|
options?: Partial<{
|
|
/**
|
|
* Instead of saving the current resourceIdString value, build a more concrete one that specifies exact version & object ids, so that the
|
|
* string doesn't resolve to different objects in the future. Useful when serializing state for posterity (e.g. for new comment threads)
|
|
*/
|
|
concreteResourceIdString: boolean
|
|
}>
|
|
): SerializedViewerState => {
|
|
const { concreteResourceIdString } = options || {}
|
|
|
|
const camControls = state.viewer.instance.getExtension(CameraController).controls
|
|
const box = state.viewer.instance.getCurrentSectionBox()
|
|
|
|
const ret: SerializedViewerState = {
|
|
projectId: state.projectId.value,
|
|
sessionId: state.sessionId.value,
|
|
viewer: {
|
|
metadata: {
|
|
filteringState: state.viewer.metadata.filteringState.value
|
|
? {
|
|
passMin: state.viewer.metadata.filteringState.value.passMin,
|
|
passMax: state.viewer.metadata.filteringState.value.passMax
|
|
}
|
|
: null
|
|
}
|
|
},
|
|
resources: {
|
|
request: {
|
|
resourceIdString: concreteResourceIdString
|
|
? buildConcreteResourceIdString()
|
|
: state.resources.request.resourceIdString.value,
|
|
threadFilters: { ...state.resources.request.threadFilters.value }
|
|
}
|
|
},
|
|
ui: {
|
|
threads: {
|
|
openThread: {
|
|
threadId: state.ui.threads.openThread.thread.value?.id || null,
|
|
isTyping: state.ui.threads.openThread.isTyping.value,
|
|
newThreadEditor: state.ui.threads.openThread.newThreadEditor.value
|
|
}
|
|
},
|
|
diff: {
|
|
command: state.urlHashState.diff.value
|
|
? serializeDiffCommand(state.urlHashState.diff.value)
|
|
: null,
|
|
time: state.ui.diff.time.value,
|
|
mode: state.ui.diff.mode.value
|
|
},
|
|
spotlightUserSessionId: state.ui.spotlightUserSessionId.value,
|
|
filters: {
|
|
isolatedObjectIds: state.ui.filters.isolatedObjectIds.value,
|
|
hiddenObjectIds: state.ui.filters.hiddenObjectIds.value,
|
|
selectedObjectApplicationIds: selectedObjects.value.reduce((ret, obj) => {
|
|
ret[obj.id] = obj.applicationId ?? null
|
|
return ret
|
|
}, {} as Record<string, string | null>),
|
|
propertyFilter: {
|
|
key: state.ui.filters.propertyFilter.filter.value?.key || null,
|
|
isApplied: state.ui.filters.propertyFilter.isApplied.value
|
|
}
|
|
},
|
|
camera: {
|
|
position: state.ui.camera.position.value.toArray(),
|
|
target: state.ui.camera.target.value.toArray(),
|
|
isOrthoProjection: state.ui.camera.isOrthoProjection.value,
|
|
zoom: (get(camControls, '_zoom') as number) || 1 // kinda hacky, _zoom is a protected prop
|
|
},
|
|
viewMode: state.ui.viewMode.value,
|
|
sectionBox: state.ui.sectionBox.value ? box : null,
|
|
lightConfig: { ...state.ui.lightConfig.value },
|
|
explodeFactor: state.ui.explodeFactor.value,
|
|
selection: state.ui.selection.value?.toArray() || null,
|
|
measurement: {
|
|
enabled: state.ui.measurement.enabled.value,
|
|
options: state.ui.measurement.options.value
|
|
}
|
|
}
|
|
}
|
|
return ret
|
|
}
|
|
|
|
return { serialize }
|
|
}
|
|
|
|
export enum StateApplyMode {
|
|
Spotlight,
|
|
ThreadOpen,
|
|
ThreadFullContextOpen,
|
|
Reset,
|
|
FederatedContext
|
|
}
|
|
|
|
export function useApplySerializedState() {
|
|
const {
|
|
projectId,
|
|
ui: {
|
|
camera: { position, target, isOrthoProjection },
|
|
sectionBox,
|
|
highlightedObjectIds,
|
|
explodeFactor,
|
|
lightConfig,
|
|
diff,
|
|
viewMode
|
|
},
|
|
resources: {
|
|
request: { resourceIdString }
|
|
},
|
|
urlHashState
|
|
} = useInjectedViewerState()
|
|
const {
|
|
resetFilters,
|
|
hideObjects,
|
|
isolateObjects,
|
|
removePropertyFilter,
|
|
setPropertyFilter,
|
|
applyPropertyFilter,
|
|
unApplyPropertyFilter,
|
|
waitForAvailableFilter
|
|
} = useFilterUtilities()
|
|
const resetState = useResetUiState()
|
|
const { diffModelVersions, deserializeDiffCommand, endDiff } = useDiffUtilities()
|
|
const { setSelectionFromObjectIds } = useSelectionUtilities()
|
|
const logger = useLogger()
|
|
|
|
return async (state: PartialDeep<SerializedViewerState>, mode: StateApplyMode) => {
|
|
if (mode === StateApplyMode.Reset) {
|
|
resetState()
|
|
return
|
|
}
|
|
|
|
if (state.projectId && state.projectId !== projectId.value) {
|
|
await projectId.update(state.projectId)
|
|
}
|
|
|
|
if (
|
|
[StateApplyMode.Spotlight, StateApplyMode.ThreadFullContextOpen].includes(mode)
|
|
) {
|
|
await resourceIdString.update(state.resources?.request?.resourceIdString || '')
|
|
}
|
|
|
|
position.value = new Vector3(
|
|
state.ui?.camera?.position?.[0],
|
|
state.ui?.camera?.position?.[1],
|
|
state.ui?.camera?.position?.[2]
|
|
)
|
|
target.value = new Vector3(
|
|
state.ui?.camera?.target?.[0],
|
|
state.ui?.camera?.target?.[1],
|
|
state.ui?.camera?.target?.[2]
|
|
)
|
|
|
|
isOrthoProjection.value = !!state.ui?.camera?.isOrthoProjection
|
|
|
|
sectionBox.value = state.ui?.sectionBox
|
|
? // It's complaining otherwise
|
|
(state.ui.sectionBox as SectionBoxData)
|
|
: null
|
|
|
|
const filters = state.ui?.filters || {}
|
|
if (filters.hiddenObjectIds?.length) {
|
|
resetFilters()
|
|
hideObjects(filters.hiddenObjectIds, { replace: true })
|
|
} else if (filters.isolatedObjectIds?.length) {
|
|
resetFilters()
|
|
isolateObjects(filters.isolatedObjectIds, { replace: true })
|
|
} else {
|
|
resetFilters()
|
|
}
|
|
|
|
const propertyFilterApplied = filters.propertyFilter?.isApplied
|
|
if (propertyFilterApplied) {
|
|
applyPropertyFilter()
|
|
} else {
|
|
unApplyPropertyFilter()
|
|
}
|
|
|
|
const propertyInfoKey = filters.propertyFilter?.key
|
|
const passMin = state.viewer?.metadata?.filteringState?.passMin
|
|
const passMax = state.viewer?.metadata?.filteringState?.passMax
|
|
if (propertyInfoKey) {
|
|
removePropertyFilter()
|
|
|
|
// Setting property filter asynchronously, when it's possible to do so
|
|
waitForAvailableFilter(propertyInfoKey)
|
|
.then((filter) => {
|
|
if (passMin || passMax) {
|
|
const numericFilter = { ...filter } as NumericPropertyInfo
|
|
numericFilter.passMin = passMin || numericFilter.min
|
|
numericFilter.passMax = passMax || numericFilter.max
|
|
setPropertyFilter(numericFilter)
|
|
applyPropertyFilter()
|
|
} else {
|
|
setPropertyFilter(filter)
|
|
applyPropertyFilter()
|
|
}
|
|
})
|
|
.catch((e) => {
|
|
if (e instanceof TimeoutError) {
|
|
logger.warn(
|
|
`${e.message} - filter probably comes from a thread context that isn't currently loaded`
|
|
)
|
|
} else {
|
|
logger.error(e)
|
|
}
|
|
})
|
|
}
|
|
|
|
const selectedObjectIds = Object.keys(filters.selectedObjectApplicationIds ?? {})
|
|
if (mode === StateApplyMode.Spotlight) {
|
|
highlightedObjectIds.value = selectedObjectIds
|
|
} else {
|
|
if (selectedObjectIds.length) {
|
|
setSelectionFromObjectIds(selectedObjectIds)
|
|
}
|
|
}
|
|
|
|
// Handle resource string updates
|
|
if (
|
|
[StateApplyMode.Spotlight, StateApplyMode.ThreadFullContextOpen].includes(mode)
|
|
) {
|
|
await resourceIdString.update(state.resources?.request?.resourceIdString || '')
|
|
} else if (mode === StateApplyMode.FederatedContext) {
|
|
// For federated context, append only model IDs (without versions) to show latest
|
|
const { parseUrlParameters, ViewerModelResource, createGetParamFromResources } =
|
|
SpeckleViewer.ViewerRoute
|
|
|
|
const currentResources = parseUrlParameters(resourceIdString.value)
|
|
const newResources = parseUrlParameters(
|
|
state.resources?.request?.resourceIdString ?? ''
|
|
).map((resource) => {
|
|
if (resource instanceof ViewerModelResource) {
|
|
// Only keep model ID, drop version
|
|
return new ViewerModelResource(resource.modelId)
|
|
}
|
|
return resource
|
|
})
|
|
|
|
if (newResources.length) {
|
|
const allResources = [...currentResources, ...newResources]
|
|
const newResourceString = createGetParamFromResources(allResources)
|
|
await resourceIdString.update(newResourceString)
|
|
}
|
|
}
|
|
|
|
if ([StateApplyMode.Spotlight].includes(mode)) {
|
|
await urlHashState.focusedThreadId.update(
|
|
state.ui?.threads?.openThread?.threadId || null
|
|
)
|
|
}
|
|
|
|
const command = state.ui?.diff?.command
|
|
? deserializeDiffCommand(state.ui.diff.command)
|
|
: null
|
|
const activeDiffEnabled = !!diff.enabled.value
|
|
if (command && command.diffs.length && state.ui?.diff) {
|
|
diff.time.value = state.ui.diff.time || 0.5
|
|
diff.mode.value = state.ui?.diff.mode || VisualDiffMode.COLORED
|
|
|
|
const instruction = command.diffs[0]
|
|
await diffModelVersions(
|
|
instruction.versionA.modelId,
|
|
instruction.versionA.versionId,
|
|
instruction.versionB.versionId
|
|
)
|
|
} else if (!activeDiffEnabled || mode === StateApplyMode.Spotlight) {
|
|
await endDiff()
|
|
}
|
|
|
|
// Restore view mode
|
|
if (state.ui?.viewMode) {
|
|
viewMode.value = state.ui.viewMode
|
|
} else {
|
|
viewMode.value = ViewMode.DEFAULT
|
|
}
|
|
|
|
explodeFactor.value = state.ui?.explodeFactor || 0
|
|
lightConfig.value = {
|
|
...lightConfig.value,
|
|
...(state.ui?.lightConfig || {})
|
|
}
|
|
}
|
|
}
|