ed1029e20d
* fix(fe2): optimized 'select all objects by type' * some more utils
292 lines
9.2 KiB
TypeScript
292 lines
9.2 KiB
TypeScript
import {
|
|
useInjectedViewerState,
|
|
useResetUiState
|
|
} from '~~/lib/viewer/composables/setup'
|
|
import { SpeckleViewer, TimeoutError } from '@speckle/shared'
|
|
import { get } from 'lodash-es'
|
|
import { Vector3, Box3 } from 'three'
|
|
import {
|
|
useDiffUtilities,
|
|
useFilterUtilities,
|
|
useSelectionUtilities
|
|
} from '~~/lib/viewer/composables/ui'
|
|
import { CameraController } from '@speckle/viewer'
|
|
import type { NumericPropertyInfo } from '@speckle/viewer'
|
|
|
|
type SerializedViewerState = SpeckleViewer.ViewerState.SerializedViewerState
|
|
|
|
export function useStateSerialization() {
|
|
const state = useInjectedViewerState()
|
|
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,
|
|
selectedObjectIds: [...state.ui.filters.selectedObjectIds.value.values()],
|
|
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
|
|
},
|
|
sectionBox: state.ui.sectionBox.value
|
|
? {
|
|
min: box.min.toArray(),
|
|
max: box.max.toArray()
|
|
}
|
|
: 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,
|
|
TheadFullContextOpen,
|
|
Reset
|
|
}
|
|
|
|
export function useApplySerializedState() {
|
|
const {
|
|
ui: {
|
|
camera: { position, target, isOrthoProjection },
|
|
sectionBox,
|
|
highlightedObjectIds,
|
|
explodeFactor,
|
|
lightConfig,
|
|
diff
|
|
},
|
|
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: SerializedViewerState, mode: StateApplyMode) => {
|
|
if (mode === StateApplyMode.Reset) {
|
|
resetState()
|
|
return
|
|
}
|
|
|
|
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
|
|
? new Box3(
|
|
new Vector3(
|
|
state.ui.sectionBox.min[0],
|
|
state.ui.sectionBox.min[1],
|
|
state.ui.sectionBox.min[2]
|
|
),
|
|
new Vector3(
|
|
state.ui.sectionBox.max[0],
|
|
state.ui.sectionBox.max[1],
|
|
state.ui.sectionBox.max[2]
|
|
)
|
|
)
|
|
: 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 = state.ui.filters.propertyFilter.isApplied
|
|
if (propertyFilterApplied) {
|
|
applyPropertyFilter()
|
|
} else {
|
|
unApplyPropertyFilter()
|
|
}
|
|
|
|
const propertyInfoKey = state.ui.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)
|
|
}
|
|
})
|
|
}
|
|
|
|
if (mode === StateApplyMode.Spotlight) {
|
|
highlightedObjectIds.value = filters.selectedObjectIds.slice()
|
|
} else {
|
|
if (filters.selectedObjectIds.length) {
|
|
setSelectionFromObjectIds(filters.selectedObjectIds)
|
|
}
|
|
}
|
|
|
|
if (
|
|
[StateApplyMode.Spotlight, StateApplyMode.TheadFullContextOpen].includes(mode)
|
|
) {
|
|
await resourceIdString.update(state.resources.request.resourceIdString)
|
|
}
|
|
|
|
if ([StateApplyMode.Spotlight].includes(mode)) {
|
|
await urlHashState.focusedThreadId.update(state.ui.threads.openThread.threadId)
|
|
}
|
|
|
|
const command = state.ui.diff.command
|
|
? deserializeDiffCommand(state.ui.diff.command)
|
|
: null
|
|
const activeDiffEnabled = !!diff.enabled.value
|
|
if (command && command.diffs.length) {
|
|
diff.time.value = state.ui.diff.time
|
|
diff.mode.value = state.ui.diff.mode
|
|
|
|
const instruction = command.diffs[0]
|
|
await diffModelVersions(
|
|
instruction.versionA.modelId,
|
|
instruction.versionA.versionId,
|
|
instruction.versionB.versionId
|
|
)
|
|
} else if (!activeDiffEnabled || mode === StateApplyMode.Spotlight) {
|
|
await endDiff()
|
|
}
|
|
|
|
explodeFactor.value = state.ui.explodeFactor
|
|
lightConfig.value = {
|
|
...lightConfig.value,
|
|
...state.ui.lightConfig
|
|
}
|
|
}
|
|
}
|