12545c6f16
* feat(viewer-lib): WIP on the new OrientedSectionTool * feat(viewer-lib): Added proper face pulling for the oriented section box * feat(viewer-lib): Several updates on the oriented sectioning tool - Implemented section planes calculation and propagation - Unified obb computation from all gizmos - Implemented proper setBox function - Updated the viewer-core to work with OBB instead of AABB for it's clipping volume - Updated the intersections to work with OBB for their intersting bounds - Added extension methods to Box3 and OBB * feat(viewer-lib): Better way of handling gizmo input events overlapping * fix(viewer-lib): Updated clippingVolume occurences to OBB * feat(viewer-lib): Section outlines now work with oriented section tool! * feat(viewer-lib): Integrated new section tool with the frontend and API - Defined an archtype for SectionTool which all section tools can derive from - The old section tool is renamed to AxisAlignedSectionTool - Replaced the old section tool with the oriented one in the frontend * fix(viewer-lib): Fixed compile errors * feat(viewer-lib): Some updates: - Section tool outline, the visible box, is now rendered as before however it's correctly being RTE'd. And we can also make it thinner/thicker now - Fixed the issue where the scale controls had 'exponential' growth. It's now linear like the translate one * feat(viewer-lib): Implemented highlghting the box face when clicking on it to extend/retract it * fix(viewer-lib): A bunch of fixes for the oriented section tool * feat(viewer-lib): Some updates: - Documented new OrientedSectionTool code - Fixed som issues related to section box reseting - Hid the translation and rotation gizmos that we aren't using - Tidied up a bit * feat(viewer-lib): Set the translate and rotate gizmos in local space so the rotation will affect them as wll * chore(viewer-lib): Purged the old section tool * chore(viewer-lib): Updated section box data type. Updated LegacyViewer section box data handling. Updated frontend to use new data type. Still not working doe * fix(viewer-lib): Fixed an issue where comments with section boxes did not enable section outlines at startup * chore(frontend): Fixed ci compiler error * fix(viewer-lib): Fixes WEB-1593
324 lines
11 KiB
TypeScript
324 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/dist/esm/viewer/helpers/state.js'
|
|
|
|
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
|
|
},
|
|
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)
|
|
}
|
|
})
|
|
}
|
|
|
|
if (mode === StateApplyMode.Spotlight) {
|
|
highlightedObjectIds.value = (filters.selectedObjectIds || []).slice()
|
|
} else {
|
|
if (filters.selectedObjectIds?.length) {
|
|
setSelectionFromObjectIds(filters.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 || {})
|
|
}
|
|
}
|
|
}
|