Files
speckle-server/packages/frontend-2/lib/viewer/composables/serialization.ts
T
Dimitrie Stefanescu 7b037352df Dim/fe2/view changes (#1608)
* Fixed an issue with curves doubling up on geometry and also not being selectable after the last filtering changes. Added the options to make lines transparent. Added lines to diffing

* Points now are diff-able and support proper visual diff-ing. Visual diff filters are now chosen internally by the Differ. Fixed an issue with LineBatch and transparency

* Implemented PLAIN visual diff mode, where all objects keep their original materil, but opacity is manipulated via the diff time. Added API member function to switch between the PLAIN and COLORED visual diff modes

* feat(fe2): diffs wip

* Diffing fixes for instances and blocks. Things seem to be working fine, but there are some caveats. Additionally, some older issues were fixed and diffing now works better on all the rest of the streams

* feat(fe2): de-dupes diff results

* feat(fe2): wip diffs

* feat(fe2): diff transparency goes from 0 to 1

* feat(fe2): diff results display work

* feat(fe2): diff results display work

* feat(fe2): diff panel work

* feat(fe2): diff work: various display changes, coloring toggle, selection logic, selection object display wip

* feat(fe2): diff work: cleaned up old/new version, fixed minor bug in viewer diff time when swapping color mode

* feat(fe2): diff work: implements custom selection logic and selection display for modified objects (they come in pairs now)

* feat(fe2): diff minor fix in selected object display

* feat(fe2): wip; trying to fix diff order to be consistent (ordered by date)

* feat(fe2): wip, broken state right now

* feat(fe2): fixes scrollbars in viewer

* feat(fe2): fixes slider sync with diff time

* feat(fe2): WIP syncs of diffs (threads, refreshes, etc.)

* feat(fe2): diffing polish

* speckle shared fix

* speckle shared fix

* more bugfixes

* linter fixess

* more CI fixes

* fix viewerState serialization

* more linting fixess

* template fixes

* moving tailwind classes to theme package

* migrated away from diffString + simplified postSetup

* moved diff new/old version resolution to use state.resources

* cleanup

* updating url threadId & diff command correctly

* minor improvements to diff state

---------

Co-authored-by: AlexandruPopovici <alexandrupopoviciioan@gmail.com>
Co-authored-by: Kristaps Fabians Geikins <fabis94@live.com>
2023-06-08 11:26:19 +03:00

278 lines
8.8 KiB
TypeScript

import {
useInjectedViewerState,
useResetUiState
} from '~~/lib/viewer/composables/setup'
import { isNonNullable } from '~~/lib/common/helpers/utils'
import { SpeckleViewer, TimeoutError } from '@speckle/shared'
import { get } from 'lodash-es'
import { Vector3, Box3 } from 'three'
import { useDiffUtilities, useFilterUtilities } from '~~/lib/viewer/composables/ui'
import { 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.cameraHandler.activeCam.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.selectedObjects.value
.map((o) => o.id)
.filter(isNonNullable),
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
}
}
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()
return async (state: SerializedViewerState, mode: StateApplyMode) => {
if (mode === StateApplyMode.Reset) {
await 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) {
console.warn(
`${e.message} - filter probably comes from a thread context that isn't currently loaded`
)
} else {
console.error(e)
}
})
}
highlightedObjectIds.value =
mode === StateApplyMode.Spotlight ? filters.selectedObjectIds.slice() : []
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
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 {
await endDiff()
}
explodeFactor.value = state.ui.explodeFactor
lightConfig.value = {
...lightConfig.value,
...state.ui.lightConfig
}
}
}