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>
This commit is contained in:
Dimitrie Stefanescu
2023-06-08 09:26:19 +01:00
committed by GitHub
parent 2b08bd5452
commit 7b037352df
42 changed files with 1112 additions and 97 deletions
+2 -1
View File
@@ -5,7 +5,8 @@
@tailwind utilities;
/**
* Don't pollute this - it's going to be bundled in all pages!
* Don't pollute this - it's going to be bundled in all pages! If it's a global style change to what can be
considered the "speckle tailwind theme" then make this change in @speckle/tailwind-theme instead
*/
/**
@@ -96,11 +96,15 @@
>
<div v-show="activeControl === 'models'">
<KeepAlive>
<ViewerResourcesList
class="pointer-events-auto"
@loaded-more="scrollControlsToBottom"
@close="activeControl = 'none'"
/>
<div>
<ViewerResourcesList
v-if="!enabled"
class="pointer-events-auto"
@loaded-more="scrollControlsToBottom"
@close="activeControl = 'none'"
/>
<ViewerCompareChangesPanel v-else @close="activeControl = 'none'" />
</div>
</KeepAlive>
</div>
<div v-show="activeControl === 'explorer'">
@@ -134,6 +138,7 @@ import {
ModifierKeys,
getKeyboardShortcutTitle
} from '@speckle/ui-components'
import { useInjectedViewerInterfaceState } from '~~/lib/viewer/composables/setup'
const {
zoomExtentsOrSelection,
@@ -146,6 +151,9 @@ type ActiveControl = 'none' | 'models' | 'explorer' | 'filters' | 'discussions'
const activeControl = ref<ActiveControl>('models')
const scrollableControlsContainer = ref(null as Nullable<HTMLDivElement>)
const {
diff: { enabled }
} = useInjectedViewerInterfaceState()
const modelsShortcut = ref(
`Models (${getKeyboardShortcutTitle([ModifierKeys.AltOrOpt, 'm'])})`
@@ -37,12 +37,12 @@
>
<div
ref="handle"
class="p-1.5 cursor-move rounded-lg group hover:bg-blue-500/50"
class="p-1.5 cursor-move rounded-lg group hover:bg-blue-500/50 transition"
:class="{ 'is-dragging bg-blue-500/50': isDragging }"
>
<div
:class="[
'bg-white/80 dark:bg-neutral-800/90 backdrop-blur-sm shadow-md cursor-auto rounded-lg',
'bg-white dark:bg-neutral-800 backdrop-blur-sm shadow-md cursor-auto rounded-lg',
'group-hover:bg-foundation dark:group-hover:bg-neutral-800 group-[.is-dragging]:bg-foundation dark:group-[.is-dragging]:bg-neutral-800'
]"
>
@@ -0,0 +1,58 @@
<template>
<div>
<div
:class="`flex group justify-between pl-1 py-1 items-center text-foreground cursor-pointer w-full max-w-full overflow-hidden select-none rounded border-l-4 hover:bg-primary-muted hover:shadow-md transition-all ${
isSelected ? 'border-primary bg-primary-muted' : 'border-transparent'
}`"
@click="setSelection()"
@keypress="keyboardClick(setSelection)"
>
<div class="flex space-x-2 items-center truncate">
<span :class="`w-3 h-3 rounded ${colorClasses}`"></span>
<span class="truncate">
{{ name }}
</span>
<span class="text-xs text-foreground-2">({{ objectIds.length }})</span>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { keyboardClick } from '~~/lib/common/helpers/accessibility'
import { useSelectionUtilities } from '~~/lib/viewer/composables/ui'
const {
clearSelection,
setSelectionFromObjectIds,
objects: selectedObjects
} = useSelectionUtilities()
const props = defineProps<{
name: 'unchanged' | 'added' | 'removed' | 'modified'
objectIds: string[]
}>()
const colorClasses = computed(() => {
switch (props.name) {
case 'added':
return 'bg-green-500'
case 'removed':
return 'bg-rose-500'
case 'modified':
return 'bg-yellow-500'
case 'unchanged':
default:
return 'bg-neutral-500'
}
})
const isSelected = computed(() => {
const selObjsIds = selectedObjects.value.map((o) => o.id as string)
return selObjsIds.some((id: string) => props.objectIds.includes(id))
})
const setSelection = () => {
if (isSelected.value) return clearSelection()
setSelectionFromObjectIds(props.objectIds)
}
</script>
@@ -0,0 +1,72 @@
<template>
<div
:class="`bg-foundation-2 shadow h-24 flex items-center justify-center flex-col rounded-md min-w-0 cursor-pointer
border-b-4 hover:bg-primary-muted
${isSelected ? 'border-primary bg-primary-muted' : 'border-transparent'}`"
@click="setSelection()"
@keypress="keyboardClick(setSelection)"
>
<div :class="`h2 font-bold truncate max-w-full ${color}`">
{{ objectCount }}
</div>
<div>{{ name }}</div>
<div class="text-xs text-foreground-2 px-1">{{ description }}</div>
</div>
</template>
<script setup lang="ts">
import { useSelectionUtilities } from '~~/lib/viewer/composables/ui'
import { keyboardClick } from '~~/lib/common/helpers/accessibility'
const {
clearSelection,
setSelectionFromObjectIds,
objects: selectedObjects
} = useSelectionUtilities()
const props = defineProps<{
name: 'unchanged' | 'added' | 'removed' | 'modified'
objectIds: string[]
}>()
const color = computed(() => {
switch (props.name) {
case 'added':
return 'text-green-500'
case 'removed':
return 'text-rose-500'
case 'modified':
return 'text-yellow-500'
case 'unchanged':
default:
return 'text-neutral-500'
}
})
const isSelected = computed(() => {
const selObjsIds = selectedObjects.value.map((o) => o.id as string)
return selObjsIds.some((id: string) => props.objectIds.includes(id))
})
const objectCount = computed(() => {
if (props.name === 'modified') return props.objectIds.length / 2
return props.objectIds.length
})
const description = computed(() => {
switch (props.name) {
case 'added':
return 'in new version'
case 'removed':
return 'from old version'
case 'modified':
return 'across both versions'
default:
return 'across both versions'
}
})
const setSelection = () => {
if (isSelected.value) return clearSelection()
setSelectionFromObjectIds(props.objectIds)
}
</script>
@@ -0,0 +1,142 @@
<template>
<ViewerLayoutPanel @close="$emit('close')">
<template #actions>
<FormButton size="xs" text :icon-left="ChevronLeftIcon" @click="clearDiff">
Back
</FormButton>
</template>
<div class="flex flex-col space-y-2 text-sm p-2">
<div class="text-xs bg-blue-500/20 text-primary p-1 rounded">
This is an experimental feature.
</div>
<div class="flex space-x-2">
<div class="grow w-1/2">
<ViewerCompareChangesVersion
v-if="diffState.newVersion.value"
:version="diffState.newVersion.value"
:is-newest="true"
@click="localDiffTime = 0"
/>
</div>
<div class="grow w-1/2">
<ViewerCompareChangesVersion
v-if="diffState.oldVersion.value"
:version="diffState.oldVersion.value"
:is-newest="false"
@click="localDiffTime = 1"
/>
</div>
</div>
<div class="grow flex items-center space-x-2 py-2">
<label for="diffTime" class="sr-only">Diff Time</label>
<input
id="diffTime"
v-model="localDiffTime"
class="h-2 w-full"
type="range"
name="diffTime"
min="0"
max="1"
step="0.01"
/>
</div>
<div class="flex items-center justify-between w-full px-1">
<span class="text-xs text-left">Color objects by status</span>
<FormButton
size="xs"
:outlined="diffState.mode.value !== VisualDiffMode.COLORED"
@click="swapDiffMode()"
>
{{ diffState.mode.value === VisualDiffMode.COLORED ? 'ON' : 'OFF' }}
</FormButton>
</div>
<!-- <div class="ml-1">Change summary:</div> -->
<div class="grid grid-cols-2 gap-2">
<ViewerCompareChangesObjectGroup name="unchanged" :object-ids="unchangedIds" />
<ViewerCompareChangesObjectGroup name="modified" :object-ids="modifiedIds" />
<ViewerCompareChangesObjectGroup name="added" :object-ids="addedIds" />
<ViewerCompareChangesObjectGroup name="removed" :object-ids="removedIds" />
</div>
</div>
</ViewerLayoutPanel>
</template>
<script setup lang="ts">
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
import { ChevronLeftIcon } from '@heroicons/vue/24/solid'
import { VisualDiffMode } from '@speckle/viewer'
import { useInjectedViewerState } from '~~/lib/viewer/composables/setup'
import { uniqBy } from 'lodash-es'
import { SpeckleObject } from '~~/lib/common/helpers/sceneExplorer'
defineEmits<{
(e: 'close'): void
}>()
const {
ui: { diff: diffState },
urlHashState: { diff }
} = useInjectedViewerState()
const localDiffTime = ref(diffState.time.value)
watch(
localDiffTime,
(newVal) => {
diffState.time.value = newVal
},
{ immediate: false }
)
watch(diffState.result, () => {
localDiffTime.value = 0.5
})
function swapDiffMode() {
if (diffState.mode.value === VisualDiffMode.COLORED)
return (diffState.mode.value = VisualDiffMode.PLAIN)
diffState.mode.value = VisualDiffMode.COLORED
}
// NOTE: deduping will not be needed anymore
const added = computed(() => {
const mapped = diffState.result.value?.added.map(
(node) => node.model.raw as SpeckleObject
)
return uniqBy(mapped, (node) => node.id)
})
const addedIds = computed(() => added.value.map((o) => o.id as string))
const removed = computed(() => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
const mapped = diffState.result.value?.removed.map(
(node) => node.model.raw as SpeckleObject
)
return uniqBy(mapped, (node) => node.id)
})
const removedIds = computed(() => removed.value.map((o) => o.id as string))
const unchanged = computed(() => {
const mapped = diffState.result.value?.unchanged.map(
(node) => node.model.raw as SpeckleObject
)
return uniqBy(mapped, (node) => node.id)
})
const unchangedIds = computed(() => unchanged.value.map((o) => o.id as string))
const modified = computed(() => {
const mapped = diffState.result.value?.modified.map((tuple) => {
return [tuple[0].model.raw as SpeckleObject, tuple[1].model.raw as SpeckleObject]
})
return uniqBy(mapped, (tuple) => tuple[0].id)
})
const modifiedIds = computed(() => {
return [
...modified.value.map((t) => t[0].id as string),
...modified.value.map((t) => t[1].id as string)
]
})
const clearDiff = async () => {
await diff.update(null)
}
</script>
@@ -0,0 +1,30 @@
<template>
<div class="shadow rounded-md p-1 flex flex-col justify-center cursor-pointer">
<div class="h-20 w-full">
<PreviewImage :preview-url="version.previewUrl" />
</div>
<div
v-tippy="createdAt"
class="bg-foundation-focus inline-block rounded-md px-2 text-xs font-bold truncate text-center py-1"
>
{{ timeAgoCreatedAt }}
<br />
{{ isNewest ? 'New' : 'Old' }} Version
</div>
</div>
</template>
<script setup lang="ts">
import dayjs from 'dayjs'
import { ViewerModelVersionCardItemFragment } from '~~/lib/common/generated/gql/graphql'
const props = defineProps<{
version: ViewerModelVersionCardItemFragment
isNewest: boolean
}>()
const timeAgoCreatedAt = computed(() => dayjs(props.version.createdAt).from(dayjs()))
const createdAt = computed(() => {
return dayjs(props.version.createdAt).format('LLL')
})
</script>
@@ -41,18 +41,17 @@
</ViewerLayoutPanel>
</template>
<script setup lang="ts">
import { SpeckleViewer } from '@speckle/shared'
import {
useInjectedViewerLoadedResources,
useInjectedViewerRequestedResources
} from '~~/lib/viewer/composables/setup'
import { PlusIcon, CheckIcon, MinusIcon } from '@heroicons/vue/24/solid'
import { SpeckleViewer } from '@speckle/shared'
defineEmits(['close'])
const showRemove = ref(false)
const { resourceItems, modelsAndVersionIds } = useInjectedViewerLoadedResources()
const { items } = useInjectedViewerRequestedResources()
const open = ref(false)
@@ -75,6 +75,7 @@
:last="index === props.model.versions.totalCount - 1"
:last-loaded="index === props.model.versions.items.length - 1"
@change-version="handleVersionChange"
@view-changes="handleViewChanges"
/>
<div class="mt-4 px-2 py-2">
<FormButton
@@ -92,21 +93,22 @@
</template>
<script setup lang="ts">
import dayjs from 'dayjs'
import localizedFormat from 'dayjs/plugin/localizedFormat'
import { graphql } from '~~/lib/common/generated/gql'
import {
XMarkIcon,
ArrowPathRoundedSquareIcon,
ChevronUpIcon
} from '@heroicons/vue/24/solid'
import { ViewerLoadedResourcesQuery } from '~~/lib/common/generated/gql/graphql'
import {
ViewerLoadedResourcesQuery,
ViewerModelVersionCardItemFragment
} from '~~/lib/common/generated/gql/graphql'
import { Get } from 'type-fest'
import {
useInjectedViewerLoadedResources,
useInjectedViewerRequestedResources
} from '~~/lib/viewer/composables/setup'
dayjs.extend(localizedFormat)
import { useDiffUtilities } from '~~/lib/viewer/composables/ui'
type ModelItem = NonNullable<Get<ViewerLoadedResourcesQuery, 'project.models.items[0]'>>
@@ -122,6 +124,7 @@ const props = defineProps<{
const { switchModelToVersion } = useInjectedViewerRequestedResources()
const { loadMoreVersions } = useInjectedViewerLoadedResources()
const { diffModelVersions } = useDiffUtilities()
const showVersions = ref(false)
@@ -196,4 +199,9 @@ async function handleVersionChange(versionId: string) {
const onLoadMore = async () => {
await loadMoreVersions(props.model.id)
}
async function handleViewChanges(version: ViewerModelVersionCardItemFragment) {
if (!loadedVersion.value?.id) return
await diffModelVersions(modelId.value, loadedVersion.value.id, version.id)
}
</script>
@@ -3,14 +3,18 @@
:class="`bg-foundation group relative block w-full space-y-2 rounded-md pb-2 text-left transition ${
clickable ? 'hover:bg-primary-muted' : 'cursor-default'
}
${!showTimeline ? 'bg-primary-muted' : ''}`"
${!showTimeline ? 'bg-primary-muted' : ''}
${isLoaded ? '' : ''}
`"
@click="handleClick"
>
<!-- Timeline left border -->
<div
v-if="showTimeline"
:class="`absolute top-3 ml-[2px] h-[99%] w-1 border-dashed ${
isLoaded ? 'border-primary border-r-2' : 'border-outline-3 border-r-2'
:class="`absolute top-3 ml-[2px] h-[99%] w-1 ${
isLoaded
? 'border-primary border-r-4 border'
: 'border-dashed border-outline-3 border-r-2'
} group-hover:border-primary left-[7px] z-10 transition-all`"
></div>
<div
@@ -34,6 +38,17 @@
>
<span>{{ isLatest ? 'Latest' : timeAgoCreatedAt }}</span>
</div>
<FormButton
v-if="!isLoaded"
v-tippy="'Shows a summary of added, deleted and changed elements.'"
size="xs"
text
class="opacity-0 group-hover:opacity-100 transition"
@click.stop="emit('viewChanges', props.version)"
>
View Changes
</FormButton>
<FormButton v-else size="xs" text disabled>Currently Viewing</FormButton>
</div>
<!-- Main stuff -->
<div class="flex items-center space-x-1 pl-5">
@@ -82,6 +97,7 @@ const props = withDefaults(
const emit = defineEmits<{
(e: 'changeVersion', version: string): void
(e: 'viewChanges', version: ViewerModelVersionCardItemFragment): void
}>()
const isLoaded = computed(() => props.isLoadedVersion)
@@ -95,6 +111,7 @@ const createdAt = computed(() => {
})
function handleClick() {
console.log('wot')
if (props.clickable) emit('changeVersion', props.version.id)
}
</script>
@@ -1,15 +1,28 @@
<template>
<div>
<div
:class="`${
isModifiedQuery.modified && root
? 'outline outline-2 rounded py-1 px-1 outline-amber-500'
: ''
}`"
>
<div class="mb-1 flex items-center">
<button
class="hover:bg-primary-muted hover:text-primary flex h-full min-w-0 items-center space-x-1 rounded"
@click="unfold = !unfold"
>
<ChevronDownIcon
:class="`h-3 w-3 transition ${!unfold ? '-rotate-90' : 'rotate-0'}`"
:class="`h-3 w-3 transition ${headerClasses} ${
!unfold ? '-rotate-90' : 'rotate-0'
}`"
/>
<div class="truncate text-xs font-bold">
<div :class="`truncate text-xs font-bold ${headerClasses}`">
{{ title || headerAndSubheader.header }}
<span
v-if="(props.root || props.modifiedSibling) && isModifiedQuery.modified"
>
{{ isModifiedQuery.isNew ? '(New)' : '(Old)' }}
</span>
</div>
</button>
</div>
@@ -81,25 +94,100 @@
</div>
</div>
</div>
<div v-if="isModifiedQuery.modified && isModifiedQuery.pair && root" class="mt-2">
<ViewerSelectionObject :object="isModifiedQuery.pair" :modified-sibling="true" />
</div>
</div>
</template>
<script setup lang="ts">
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
import { ChevronDownIcon } from '@heroicons/vue/24/solid'
import { SpeckleObject } from '~~/lib/common/helpers/sceneExplorer'
import { getHeaderAndSubheaderForSpeckleObject } from '~~/lib/object-sidebar/helpers'
import { useInjectedViewerState } from '~~/lib/viewer/composables/setup'
const {
ui: {
diff: { result, enabled: diffEnabled }
}
} = useInjectedViewerState()
const props = withDefaults(
defineProps<{
object: SpeckleObject
root?: boolean
title?: string
unfold: boolean
unfold?: boolean
debug?: boolean
modifiedSibling?: boolean
}>(),
{ debug: false, unfold: true }
{ debug: false, unfold: false, root: false, modifiedSibling: false }
)
const unfold = ref(props.unfold)
const isAdded = computed(() => {
if (!diffEnabled.value) return false
return (
result.value?.added.findIndex(
(o) => (o.model.raw as SpeckleObject).applicationId === props.object.applicationId
) !== -1
)
})
const isRemoved = computed(() => {
if (!diffEnabled.value) return false
return (
result.value?.removed.findIndex(
(o) => (o.model.raw as SpeckleObject).applicationId === props.object.applicationId
) !== -1
)
})
const isUnchanged = computed(() => {
if (!diffEnabled.value) return false
return (
result.value?.unchanged.findIndex(
(o) => (o.model.raw as SpeckleObject).applicationId === props.object.applicationId
) !== -1
)
})
const isModifiedQuery = computed(() => {
// if (props.modifiedSibling) return { modified: false } // prevent recursion?
if (!diffEnabled.value) return { modified: false }
const modifiedObjectPairs = result.value?.modified.map((pair) => {
return [pair[0].model.raw as SpeckleObject, pair[1].model.raw as SpeckleObject]
})
if (!modifiedObjectPairs) return { modified: false }
const obj = props.object
const pairedItems = modifiedObjectPairs.find(
(item) => item[0].id === obj.id || item[1].id === obj.id
)
if (!pairedItems) return { modified: false }
const pair = pairedItems[0].id === obj.id ? pairedItems[1] : pairedItems[0]
if (!pair) return { modified: false }
return {
modified: true,
pair,
isNew: pairedItems[0].id !== obj.id
}
})
const headerClasses = computed(() => {
if (props.modifiedSibling) return 'text-amber-500'
if (!props.root) return ''
if (!diffEnabled.value) return ''
if (!Object.keys(props.object).includes('applicationId')) return ''
if (isAdded.value) return 'text-green-500'
if (isRemoved.value) return 'text-red-500'
if (isUnchanged.value) return 'text-foreground'
return 'text-amber-500'
})
const headerAndSubheader = computed(() => {
return getHeaderAndSubheaderForSpeckleObject(props.object)
})
@@ -31,6 +31,7 @@
:key="(object.id as string)"
:object="object"
:unfold="false"
:root="true"
/>
</div>
<div v-if="itemCount <= objects.length" class="mb-2">
@@ -54,19 +55,29 @@ import { useInjectedViewerState } from '~~/lib/viewer/composables/setup'
import { getTargetObjectIds } from '~~/lib/object-sidebar/helpers'
import { containsAll } from '~~/lib/common/helpers/utils'
import { useFilterUtilities, useSelectionUtilities } from '~~/lib/viewer/composables/ui'
import { uniqWith } from 'lodash-es'
const {
viewer: {
metadata: { filteringState }
}
},
ui: { diff }
} = useInjectedViewerState()
const { objects, clearSelection } = useSelectionUtilities()
const { hideObjects, showObjects, isolateObjects, unIsolateObjects } =
useFilterUtilities()
const itemCount = ref(10)
const itemCount = ref(42)
const objectsUniqueByAppId = computed(() => {
if (!diff.enabled.value) return objects.value
return uniqWith(objects.value, (a, b) => {
return a.applicationId === b.applicationId
})
})
const objectsLimited = computed(() => {
return objects.value.slice(0, itemCount.value)
return objectsUniqueByAppId.value.slice(0, itemCount.value)
})
const hiddenObjects = computed(() => filteringState.value?.hiddenObjects)
@@ -7,6 +7,7 @@ export interface AsyncWritableComputedOptions<T> {
set: (value: T) => MaybeAsync<void>
initialState: T
readOptions?: AsyncComputedOptions
asyncRead?: boolean
}
export type AsyncWritableComputedRef<T> = ComputedRef<T> & {
@@ -15,16 +16,20 @@ export type AsyncWritableComputedRef<T> = ComputedRef<T> & {
/**
* Allows async read/write from/to computed. Use `res.value` to read and `res.update` to write. If you only need
* the computed to be read-only then use vueuse's `computedAsync`.
* the computed to be read-only then use vueuse's `computedAsync`. If you only need async writes you can
* disable async reads through the `asyncRead` param.
* @param params
*/
export function writableAsyncComputed<T>(
params: AsyncWritableComputedOptions<T>
): AsyncWritableComputedRef<T> {
const readValue = computedAsync(params.get, params.initialState, params.readOptions)
const { get, initialState, readOptions, set, asyncRead = true } = params
const readValue = asyncRead
? computedAsync(get, initialState, readOptions)
: computed(get)
const getter = computed(() => readValue.value) as AsyncWritableComputedRef<T>
getter.update = params.set
getter.update = set
return getter
}
@@ -26,7 +26,7 @@ export function useRouteHashState() {
if (hash.length < 2 || !hash.startsWith('#')) return {}
const keyValuePairs = hash.substring(1).split('&')
return reduce(
const result = reduce(
keyValuePairs,
(result, item) => {
const [key, value] = item.split('=')
@@ -37,6 +37,7 @@ export function useRouteHashState() {
},
{} as Record<string, Nullable<string>>
)
return result
},
set: async (newVal) => {
const hashString = serializeHashState(newVal)
@@ -45,7 +46,8 @@ export function useRouteHashState() {
hash: hashString
})
},
initialState: {}
initialState: {},
asyncRead: false
})
return { hashState }
@@ -128,8 +128,9 @@ const documents = {
"\n mutation CreateCommentReply($input: CreateCommentReplyInput!) {\n commentMutations {\n reply(input: $input) {\n ...ViewerCommentsReplyItem\n }\n }\n }\n": types.CreateCommentReplyDocument,
"\n mutation ArchiveComment($commentId: String!, $archived: Boolean) {\n commentMutations {\n archive(commentId: $commentId, archived: $archived)\n }\n }\n": types.ArchiveCommentDocument,
"\n query ProjectViewerResources($projectId: String!, $resourceUrlString: String!) {\n project(id: $projectId) {\n id\n viewerResources(resourceIdString: $resourceUrlString) {\n identifier\n items {\n modelId\n versionId\n objectId\n }\n }\n }\n }\n": types.ProjectViewerResourcesDocument,
"\n query ViewerLoadedResources(\n $projectId: String!\n $modelIds: [String!]!\n $versionIds: [String!]\n ) {\n project(id: $projectId) {\n id\n role\n models(filter: { ids: $modelIds }) {\n totalCount\n items {\n id\n name\n updatedAt\n loadedVersion: versions(filter: { priorityIds: $versionIds }, limit: 1) {\n items {\n ...ViewerModelVersionCardItem\n }\n }\n versions(limit: 5) {\n totalCount\n cursor\n items {\n ...ViewerModelVersionCardItem\n }\n }\n }\n }\n ...ProjectPageLatestItemsModels\n ...ModelPageProject\n }\n }\n": types.ViewerLoadedResourcesDocument,
"\n query ViewerLoadedResources(\n $projectId: String!\n $modelIds: [String!]!\n $versionIds: [String!]\n ) {\n project(id: $projectId) {\n id\n role\n models(filter: { ids: $modelIds }) {\n totalCount\n items {\n id\n name\n updatedAt\n loadedVersion: versions(\n filter: { priorityIds: $versionIds, priorityIdsOnly: true }\n ) {\n items {\n ...ViewerModelVersionCardItem\n }\n }\n versions(limit: 5) {\n totalCount\n cursor\n items {\n ...ViewerModelVersionCardItem\n }\n }\n }\n }\n ...ProjectPageLatestItemsModels\n ...ModelPageProject\n }\n }\n": types.ViewerLoadedResourcesDocument,
"\n query ViewerModelVersions(\n $projectId: String!\n $modelId: String!\n $versionsCursor: String\n ) {\n project(id: $projectId) {\n id\n role\n model(id: $modelId) {\n id\n versions(cursor: $versionsCursor, limit: 5) {\n totalCount\n cursor\n items {\n ...ViewerModelVersionCardItem\n }\n }\n }\n }\n }\n": types.ViewerModelVersionsDocument,
"\n query ViewerDiffVersions(\n $projectId: String!\n $modelId: String!\n $versionAId: String!\n $versionBId: String!\n ) {\n project(id: $projectId) {\n id\n model(id: $modelId) {\n id\n versionA: version(id: $versionAId) {\n ...ViewerModelVersionCardItem\n }\n versionB: version(id: $versionBId) {\n ...ViewerModelVersionCardItem\n }\n }\n }\n }\n": types.ViewerDiffVersionsDocument,
"\n query ViewerLoadedThreads(\n $projectId: String!\n $filter: ProjectCommentsFilter!\n $cursor: String\n $limit: Int = 25\n ) {\n project(id: $projectId) {\n id\n commentThreads(filter: $filter, cursor: $cursor, limit: $limit) {\n totalCount\n totalArchivedCount\n items {\n ...ViewerCommentThread\n ...LinkableComment\n }\n }\n }\n }\n": types.ViewerLoadedThreadsDocument,
"\n subscription OnViewerUserActivityBroadcasted(\n $target: ViewerUpdateTrackingTarget!\n $sessionId: String!\n ) {\n viewerUserActivityBroadcasted(target: $target, sessionId: $sessionId) {\n userName\n userId\n user {\n ...LimitedUserAvatar\n }\n state\n status\n sessionId\n }\n }\n": types.OnViewerUserActivityBroadcastedDocument,
"\n subscription OnViewerCommentsUpdated($target: ViewerUpdateTrackingTarget!) {\n projectCommentsUpdated(target: $target) {\n id\n type\n comment {\n id\n parent {\n id\n }\n ...ViewerCommentThread\n }\n }\n }\n": types.OnViewerCommentsUpdatedDocument,
@@ -616,11 +617,15 @@ export function graphql(source: "\n query ProjectViewerResources($projectId: St
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n query ViewerLoadedResources(\n $projectId: String!\n $modelIds: [String!]!\n $versionIds: [String!]\n ) {\n project(id: $projectId) {\n id\n role\n models(filter: { ids: $modelIds }) {\n totalCount\n items {\n id\n name\n updatedAt\n loadedVersion: versions(filter: { priorityIds: $versionIds }, limit: 1) {\n items {\n ...ViewerModelVersionCardItem\n }\n }\n versions(limit: 5) {\n totalCount\n cursor\n items {\n ...ViewerModelVersionCardItem\n }\n }\n }\n }\n ...ProjectPageLatestItemsModels\n ...ModelPageProject\n }\n }\n"): (typeof documents)["\n query ViewerLoadedResources(\n $projectId: String!\n $modelIds: [String!]!\n $versionIds: [String!]\n ) {\n project(id: $projectId) {\n id\n role\n models(filter: { ids: $modelIds }) {\n totalCount\n items {\n id\n name\n updatedAt\n loadedVersion: versions(filter: { priorityIds: $versionIds }, limit: 1) {\n items {\n ...ViewerModelVersionCardItem\n }\n }\n versions(limit: 5) {\n totalCount\n cursor\n items {\n ...ViewerModelVersionCardItem\n }\n }\n }\n }\n ...ProjectPageLatestItemsModels\n ...ModelPageProject\n }\n }\n"];
export function graphql(source: "\n query ViewerLoadedResources(\n $projectId: String!\n $modelIds: [String!]!\n $versionIds: [String!]\n ) {\n project(id: $projectId) {\n id\n role\n models(filter: { ids: $modelIds }) {\n totalCount\n items {\n id\n name\n updatedAt\n loadedVersion: versions(\n filter: { priorityIds: $versionIds, priorityIdsOnly: true }\n ) {\n items {\n ...ViewerModelVersionCardItem\n }\n }\n versions(limit: 5) {\n totalCount\n cursor\n items {\n ...ViewerModelVersionCardItem\n }\n }\n }\n }\n ...ProjectPageLatestItemsModels\n ...ModelPageProject\n }\n }\n"): (typeof documents)["\n query ViewerLoadedResources(\n $projectId: String!\n $modelIds: [String!]!\n $versionIds: [String!]\n ) {\n project(id: $projectId) {\n id\n role\n models(filter: { ids: $modelIds }) {\n totalCount\n items {\n id\n name\n updatedAt\n loadedVersion: versions(\n filter: { priorityIds: $versionIds, priorityIdsOnly: true }\n ) {\n items {\n ...ViewerModelVersionCardItem\n }\n }\n versions(limit: 5) {\n totalCount\n cursor\n items {\n ...ViewerModelVersionCardItem\n }\n }\n }\n }\n ...ProjectPageLatestItemsModels\n ...ModelPageProject\n }\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n query ViewerModelVersions(\n $projectId: String!\n $modelId: String!\n $versionsCursor: String\n ) {\n project(id: $projectId) {\n id\n role\n model(id: $modelId) {\n id\n versions(cursor: $versionsCursor, limit: 5) {\n totalCount\n cursor\n items {\n ...ViewerModelVersionCardItem\n }\n }\n }\n }\n }\n"): (typeof documents)["\n query ViewerModelVersions(\n $projectId: String!\n $modelId: String!\n $versionsCursor: String\n ) {\n project(id: $projectId) {\n id\n role\n model(id: $modelId) {\n id\n versions(cursor: $versionsCursor, limit: 5) {\n totalCount\n cursor\n items {\n ...ViewerModelVersionCardItem\n }\n }\n }\n }\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n query ViewerDiffVersions(\n $projectId: String!\n $modelId: String!\n $versionAId: String!\n $versionBId: String!\n ) {\n project(id: $projectId) {\n id\n model(id: $modelId) {\n id\n versionA: version(id: $versionAId) {\n ...ViewerModelVersionCardItem\n }\n versionB: version(id: $versionBId) {\n ...ViewerModelVersionCardItem\n }\n }\n }\n }\n"): (typeof documents)["\n query ViewerDiffVersions(\n $projectId: String!\n $modelId: String!\n $versionAId: String!\n $versionBId: String!\n ) {\n project(id: $projectId) {\n id\n model(id: $modelId) {\n id\n versionA: version(id: $versionAId) {\n ...ViewerModelVersionCardItem\n }\n versionB: version(id: $versionBId) {\n ...ViewerModelVersionCardItem\n }\n }\n }\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
@@ -694,6 +694,8 @@ export type ModelMutationsUpdateArgs = {
export type ModelVersionsFilter = {
/** Make sure these specified versions are always loaded first */
priorityIds?: InputMaybe<Array<Scalars['String']>>;
/** Only return versions specified in `priorityIds` */
priorityIdsOnly?: InputMaybe<Scalars['Boolean']>;
};
export type ModelsTreeItem = {
@@ -3180,6 +3182,16 @@ export type ViewerModelVersionsQueryVariables = Exact<{
export type ViewerModelVersionsQuery = { __typename?: 'Query', project: { __typename?: 'Project', id: string, role?: string | null, model: { __typename?: 'Model', id: string, versions: { __typename?: 'VersionCollection', totalCount: number, cursor?: string | null, items: Array<{ __typename?: 'Version', id: string, message?: string | null, referencedObject: string, sourceApplication?: string | null, createdAt: string, previewUrl: string, authorUser?: { __typename?: 'LimitedUser', id: string, name: string, avatar?: string | null } | null }> } } } };
export type ViewerDiffVersionsQueryVariables = Exact<{
projectId: Scalars['String'];
modelId: Scalars['String'];
versionAId: Scalars['String'];
versionBId: Scalars['String'];
}>;
export type ViewerDiffVersionsQuery = { __typename?: 'Query', project: { __typename?: 'Project', id: string, model: { __typename?: 'Model', id: string, versionA: { __typename?: 'Version', id: string, message?: string | null, referencedObject: string, sourceApplication?: string | null, createdAt: string, previewUrl: string, authorUser?: { __typename?: 'LimitedUser', id: string, name: string, avatar?: string | null } | null }, versionB: { __typename?: 'Version', id: string, message?: string | null, referencedObject: string, sourceApplication?: string | null, createdAt: string, previewUrl: string, authorUser?: { __typename?: 'LimitedUser', id: string, name: string, avatar?: string | null } | null } } } };
export type ViewerLoadedThreadsQueryVariables = Exact<{
projectId: Scalars['String'];
filter: ProjectCommentsFilter;
@@ -3333,8 +3345,9 @@ export const CreateCommentThreadDocument = {"kind":"Document","definitions":[{"k
export const CreateCommentReplyDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CreateCommentReply"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"CreateCommentReplyInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"commentMutations"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"reply"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"ViewerCommentsReplyItem"}}]}}]}}]}},...ViewerCommentsReplyItemFragmentDoc.definitions,...LimitedUserAvatarFragmentDoc.definitions,...ThreadCommentAttachmentFragmentDoc.definitions]} as unknown as DocumentNode<CreateCommentReplyMutation, CreateCommentReplyMutationVariables>;
export const ArchiveCommentDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"ArchiveComment"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"commentId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"archived"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Boolean"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"commentMutations"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"archive"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"commentId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"commentId"}}},{"kind":"Argument","name":{"kind":"Name","value":"archived"},"value":{"kind":"Variable","name":{"kind":"Name","value":"archived"}}}]}]}}]}}]} as unknown as DocumentNode<ArchiveCommentMutation, ArchiveCommentMutationVariables>;
export const ProjectViewerResourcesDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"ProjectViewerResources"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"projectId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"resourceUrlString"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"project"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"projectId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"viewerResources"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"resourceIdString"},"value":{"kind":"Variable","name":{"kind":"Name","value":"resourceUrlString"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"identifier"}},{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"modelId"}},{"kind":"Field","name":{"kind":"Name","value":"versionId"}},{"kind":"Field","name":{"kind":"Name","value":"objectId"}}]}}]}}]}}]}}]} as unknown as DocumentNode<ProjectViewerResourcesQuery, ProjectViewerResourcesQueryVariables>;
export const ViewerLoadedResourcesDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"ViewerLoadedResources"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"projectId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"modelIds"}},"type":{"kind":"NonNullType","type":{"kind":"ListType","type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"versionIds"}},"type":{"kind":"ListType","type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"project"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"projectId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"role"}},{"kind":"Field","name":{"kind":"Name","value":"models"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"filter"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"ids"},"value":{"kind":"Variable","name":{"kind":"Name","value":"modelIds"}}}]}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"totalCount"}},{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","alias":{"kind":"Name","value":"loadedVersion"},"name":{"kind":"Name","value":"versions"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"filter"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"priorityIds"},"value":{"kind":"Variable","name":{"kind":"Name","value":"versionIds"}}}]}},{"kind":"Argument","name":{"kind":"Name","value":"limit"},"value":{"kind":"IntValue","value":"1"}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"ViewerModelVersionCardItem"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"versions"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"limit"},"value":{"kind":"IntValue","value":"5"}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"totalCount"}},{"kind":"Field","name":{"kind":"Name","value":"cursor"}},{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"ViewerModelVersionCardItem"}}]}}]}}]}}]}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"ProjectPageLatestItemsModels"}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"ModelPageProject"}}]}}]}},...ViewerModelVersionCardItemFragmentDoc.definitions,...LimitedUserAvatarFragmentDoc.definitions,...ProjectPageLatestItemsModelsFragmentDoc.definitions,...ModelPageProjectFragmentDoc.definitions]} as unknown as DocumentNode<ViewerLoadedResourcesQuery, ViewerLoadedResourcesQueryVariables>;
export const ViewerLoadedResourcesDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"ViewerLoadedResources"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"projectId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"modelIds"}},"type":{"kind":"NonNullType","type":{"kind":"ListType","type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"versionIds"}},"type":{"kind":"ListType","type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"project"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"projectId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"role"}},{"kind":"Field","name":{"kind":"Name","value":"models"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"filter"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"ids"},"value":{"kind":"Variable","name":{"kind":"Name","value":"modelIds"}}}]}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"totalCount"}},{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","alias":{"kind":"Name","value":"loadedVersion"},"name":{"kind":"Name","value":"versions"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"filter"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"priorityIds"},"value":{"kind":"Variable","name":{"kind":"Name","value":"versionIds"}}},{"kind":"ObjectField","name":{"kind":"Name","value":"priorityIdsOnly"},"value":{"kind":"BooleanValue","value":true}}]}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"ViewerModelVersionCardItem"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"versions"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"limit"},"value":{"kind":"IntValue","value":"5"}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"totalCount"}},{"kind":"Field","name":{"kind":"Name","value":"cursor"}},{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"ViewerModelVersionCardItem"}}]}}]}}]}}]}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"ProjectPageLatestItemsModels"}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"ModelPageProject"}}]}}]}},...ViewerModelVersionCardItemFragmentDoc.definitions,...LimitedUserAvatarFragmentDoc.definitions,...ProjectPageLatestItemsModelsFragmentDoc.definitions,...ModelPageProjectFragmentDoc.definitions]} as unknown as DocumentNode<ViewerLoadedResourcesQuery, ViewerLoadedResourcesQueryVariables>;
export const ViewerModelVersionsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"ViewerModelVersions"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"projectId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"modelId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"versionsCursor"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"project"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"projectId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"role"}},{"kind":"Field","name":{"kind":"Name","value":"model"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"modelId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"versions"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"cursor"},"value":{"kind":"Variable","name":{"kind":"Name","value":"versionsCursor"}}},{"kind":"Argument","name":{"kind":"Name","value":"limit"},"value":{"kind":"IntValue","value":"5"}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"totalCount"}},{"kind":"Field","name":{"kind":"Name","value":"cursor"}},{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"ViewerModelVersionCardItem"}}]}}]}}]}}]}}]}},...ViewerModelVersionCardItemFragmentDoc.definitions,...LimitedUserAvatarFragmentDoc.definitions]} as unknown as DocumentNode<ViewerModelVersionsQuery, ViewerModelVersionsQueryVariables>;
export const ViewerDiffVersionsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"ViewerDiffVersions"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"projectId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"modelId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"versionAId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"versionBId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"project"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"projectId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"model"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"modelId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","alias":{"kind":"Name","value":"versionA"},"name":{"kind":"Name","value":"version"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"versionAId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"ViewerModelVersionCardItem"}}]}},{"kind":"Field","alias":{"kind":"Name","value":"versionB"},"name":{"kind":"Name","value":"version"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"versionBId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"ViewerModelVersionCardItem"}}]}}]}}]}}]}},...ViewerModelVersionCardItemFragmentDoc.definitions,...LimitedUserAvatarFragmentDoc.definitions]} as unknown as DocumentNode<ViewerDiffVersionsQuery, ViewerDiffVersionsQueryVariables>;
export const ViewerLoadedThreadsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"ViewerLoadedThreads"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"projectId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"filter"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ProjectCommentsFilter"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"cursor"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"limit"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}},"defaultValue":{"kind":"IntValue","value":"25"}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"project"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"projectId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"commentThreads"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"filter"},"value":{"kind":"Variable","name":{"kind":"Name","value":"filter"}}},{"kind":"Argument","name":{"kind":"Name","value":"cursor"},"value":{"kind":"Variable","name":{"kind":"Name","value":"cursor"}}},{"kind":"Argument","name":{"kind":"Name","value":"limit"},"value":{"kind":"Variable","name":{"kind":"Name","value":"limit"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"totalCount"}},{"kind":"Field","name":{"kind":"Name","value":"totalArchivedCount"}},{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"ViewerCommentThread"}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"LinkableComment"}}]}}]}}]}}]}},...ViewerCommentThreadFragmentDoc.definitions,...ViewerCommentsListItemFragmentDoc.definitions,...LimitedUserAvatarFragmentDoc.definitions,...ViewerCommentsReplyItemFragmentDoc.definitions,...ThreadCommentAttachmentFragmentDoc.definitions,...FormUsersSelectItemFragmentDoc.definitions,...ViewerCommentBubblesDataFragmentDoc.definitions,...LinkableCommentFragmentDoc.definitions]} as unknown as DocumentNode<ViewerLoadedThreadsQuery, ViewerLoadedThreadsQueryVariables>;
export const OnViewerUserActivityBroadcastedDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"subscription","name":{"kind":"Name","value":"OnViewerUserActivityBroadcasted"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"target"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ViewerUpdateTrackingTarget"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"sessionId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"viewerUserActivityBroadcasted"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"target"},"value":{"kind":"Variable","name":{"kind":"Name","value":"target"}}},{"kind":"Argument","name":{"kind":"Name","value":"sessionId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"sessionId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"userName"}},{"kind":"Field","name":{"kind":"Name","value":"userId"}},{"kind":"Field","name":{"kind":"Name","value":"user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"LimitedUserAvatar"}}]}},{"kind":"Field","name":{"kind":"Name","value":"state"}},{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"sessionId"}}]}}]}},...LimitedUserAvatarFragmentDoc.definitions]} as unknown as DocumentNode<OnViewerUserActivityBroadcastedSubscription, OnViewerUserActivityBroadcastedSubscriptionVariables>;
export const OnViewerCommentsUpdatedDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"subscription","name":{"kind":"Name","value":"OnViewerCommentsUpdated"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"target"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ViewerUpdateTrackingTarget"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"projectCommentsUpdated"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"target"},"value":{"kind":"Variable","name":{"kind":"Name","value":"target"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"comment"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"parent"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"ViewerCommentThread"}}]}}]}}]}},...ViewerCommentThreadFragmentDoc.definitions,...ViewerCommentsListItemFragmentDoc.definitions,...LimitedUserAvatarFragmentDoc.definitions,...ViewerCommentsReplyItemFragmentDoc.definitions,...ThreadCommentAttachmentFragmentDoc.definitions,...FormUsersSelectItemFragmentDoc.definitions,...ViewerCommentBubblesDataFragmentDoc.definitions]} as unknown as DocumentNode<OnViewerCommentsUpdatedSubscription, OnViewerCommentsUpdatedSubscriptionVariables>;
@@ -0,0 +1,13 @@
const KEYBOARD_CLICK_CHAR = 'Enter'
/**
* Visible, non-interactive elements with click handlers must have at least one keyboard listener for accessibility.
* You can wrap your click handler with this in @keypress, to run it when enter is pressed on the selected component
* See more: https://github.com/vue-a11y/eslint-plugin-vuejs-accessibility/blob/main/docs/click-events-have-key-events.md
*/
export function keyboardClick(cb: (e: KeyboardEvent) => void) {
return (e: KeyboardEvent) => {
if (e.code !== KEYBOARD_CLICK_CHAR) return
cb(e)
}
}
@@ -12,7 +12,7 @@ export const projectRoute = (id: string) => `/projects/${id}`
export const modelRoute = (
projectId: string,
resourceIdString: string,
hashState?: Record<ViewerHashStateKeys, string>
hashState?: Partial<Record<ViewerHashStateKeys, string>>
) =>
`/projects/${projectId}/models/${encodeURIComponent(resourceIdString)}${
hashState ? serializeHashState(hashState) || '' : ''
@@ -175,7 +175,9 @@ export function useViewerCommentBubbles(
const commentThreads = ref({} as Record<string, CommentBubbleModel>)
const openThread = computed(() =>
Object.values(commentThreads.value).find((t) => t.isExpanded)
Object.values(commentThreads.value).find(
(t) => t.isExpanded && t.id === focusedThreadId.value
)
)
useSelectionEvents(
@@ -6,13 +6,14 @@ import { isNonNullable } from '~~/lib/common/helpers/utils'
import { SpeckleViewer, TimeoutError } from '@speckle/shared'
import { get } from 'lodash-es'
import { Vector3, Box3 } from 'three'
import { useFilterUtilities } from '~~/lib/viewer/composables/ui'
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
@@ -77,6 +78,13 @@ export function useStateSerialization() {
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,
@@ -126,7 +134,8 @@ export function useApplySerializedState() {
sectionBox,
highlightedObjectIds,
explodeFactor,
lightConfig
lightConfig,
diff
},
resources: {
request: { resourceIdString }
@@ -144,6 +153,7 @@ export function useApplySerializedState() {
waitForAvailableFilter
} = useFilterUtilities()
const resetState = useResetUiState()
const { diffModelVersions, deserializeDiffCommand, endDiff } = useDiffUtilities()
return async (state: SerializedViewerState, mode: StateApplyMode) => {
if (mode === StateApplyMode.Reset) {
@@ -241,6 +251,23 @@ export function useApplySerializedState() {
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,
@@ -8,7 +8,9 @@ import {
ViewerEvent,
SunLightConfiguration,
DefaultLightConfiguration,
SpeckleView
SpeckleView,
DiffResult,
VisualDiffMode
} from '@speckle/viewer'
import { MaybeRef } from '@vueuse/shared'
import {
@@ -19,7 +21,8 @@ import {
ComputedRef,
WritableComputedRef,
Raw,
Ref
Ref,
ShallowRef
} from 'vue'
import { useScopedState } from '~~/lib/common/composables/scopedState'
import { Nullable, Optional, SpeckleViewer } from '@speckle/shared'
@@ -37,7 +40,8 @@ import {
ViewerLoadedThreadsQuery,
ViewerResourceItem,
ViewerLoadedThreadsQueryVariables,
ProjectCommentsFilter
ProjectCommentsFilter,
ViewerModelVersionCardItemFragment
} from '~~/lib/common/generated/gql/graphql'
import { SetNonNullable, Get } from 'type-fest'
import {
@@ -55,11 +59,16 @@ import { SpeckleObject } from '~~/lib/common/helpers/sceneExplorer'
import { Box3, Vector3 } from 'three'
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import { wrapRefWithTracking } from '~~/lib/common/helpers/debugging'
import { useFilterUtilities } from '~~/lib/viewer/composables/ui'
import {
AsyncWritableComputedRef,
writableAsyncComputed
} from '~~/lib/common/composables/async'
import {
DiffStateCommand,
setupUiDiffState
} from '~~/lib/viewer/composables/setup/diff'
import { useDiffUtilities, useFilterUtilities } from '~~/lib/viewer/composables/ui'
import { reduce } from 'lodash-es'
export type LoadedModel = NonNullable<
Get<ViewerLoadedResourcesQuery, 'project.models.items[0]'>
@@ -71,12 +80,6 @@ export type LoadedThreadsMetadata = NonNullable<
export type LoadedCommentThread = NonNullable<Get<LoadedThreadsMetadata, 'items[0]'>>
// export type FilterAction = (
// objectIds: string[],
// stateKey: string,
// includeDescendants?: boolean
// ) => Promise<void>
export type InjectableViewerState = Readonly<{
/**
* The project which we're opening in the viewer (all loaded models should belong to it)
@@ -145,6 +148,9 @@ export type InjectableViewerState = Readonly<{
* Helper for switching model to a specific version (or just latest)
*/
switchModelToVersion: (modelId: string, versionId?: string) => Promise<void>
// addModelVersion: (modelId: string, versionId: string) => void
// removeModelVersion: (modelId: string, versionId?: string) => void
// setModelVersions: (newResources: ViewerResource[]) => void
}
/**
* State of resolved, validated & de-duplicated resources that are loaded in the viewer. These
@@ -165,6 +171,12 @@ export type InjectableViewerState = Readonly<{
* Model GQL objects paired with their loaded version IDs
*/
modelsAndVersionIds: ComputedRef<Array<{ model: LoadedModel; versionId: string }>>
/**
* All available (retrieved from GQL) models and their versions
*/
availableModelsAndVersions: ComputedRef<
Array<{ model: LoadedModel; versions: LoadedModel['versions']['items'] }>
>
/**
* Detached objects (not models/versions)
*/
@@ -232,6 +244,14 @@ export type InjectableViewerState = Readonly<{
target: Ref<Vector3>
isOrthoProjection: Ref<boolean>
}
diff: {
newVersion: ComputedRef<ViewerModelVersionCardItemFragment | undefined>
oldVersion: ComputedRef<ViewerModelVersionCardItemFragment | undefined>
time: Ref<number>
mode: Ref<VisualDiffMode>
result: ShallowRef<Optional<DiffResult>> //ComputedRef<Optional<DiffResult>>
enabled: Ref<boolean>
}
sectionBox: Ref<Nullable<Box3>>
highlightedObjectIds: Ref<string[]>
lightConfig: Ref<SunLightConfiguration>
@@ -244,6 +264,7 @@ export type InjectableViewerState = Readonly<{
*/
urlHashState: {
focusedThreadId: AsyncWritableComputedRef<Nullable<string>>
diff: AsyncWritableComputedRef<Nullable<DiffStateCommand>>
}
}>
@@ -394,7 +415,8 @@ function setupResourceRequest(state: InitialSetupState): InitialStateWithRequest
hash: route.hash
})
},
initialState: []
initialState: [],
asyncRead: false
})
// we could use getParam, but `createGetParamFromResources` does sorting and de-duplication AFAIK
@@ -404,7 +426,8 @@ function setupResourceRequest(state: InitialSetupState): InitialStateWithRequest
const newResources = SpeckleViewer.ViewerRoute.parseUrlParameters(newVal)
await resources.update(newResources)
},
initialState: ''
initialState: '',
asyncRead: false
})
const threadFilters = ref({} as Omit<ProjectCommentsFilter, 'resourceIdString'>)
@@ -527,8 +550,7 @@ function setupResponseResourceItems(
...objectItems
]
// Get rid of duplicates - only 1 resource per model & 1 resource per objectId
// TODO: @dim here you can remove the restriction to only have 1 model
// Get rid of duplicates - only 1 resource per objectId
const encounteredModels = new Set<string>()
const encounteredObjects = new Set<string>()
const finalItems: ViewerResourceItem[] = []
@@ -536,7 +558,8 @@ function setupResponseResourceItems(
const modelId = item.modelId
const objectId = item.objectId
if (modelId && encounteredModels.has(modelId)) continue
// In case we want to go back to 1 resource per model:
// if (modelId && encounteredModels.has(modelId)) continue
if (encounteredObjects.has(objectId)) continue
finalItems.push(item)
@@ -615,6 +638,24 @@ function setupResponseResourceData(
.filter((o): o is SetNonNullable<typeof o, 'model'> => !!(o.versionId && o.model))
)
const availableModelsAndVersions = computed(() => {
const modelItems = models.value
return reduce(
modelItems,
(res, entry) => {
res.push({
model: entry,
versions: [...entry.loadedVersion.items, ...entry.versions.items]
})
return res
},
[] as Array<{
model: (typeof modelItems)[0]
versions: (typeof modelItems)[0]['versions']['items']
}>
)
})
onViewerLoadedResourcesError((err) => {
globalError.value = createError({
statusCode: 500,
@@ -698,6 +739,7 @@ function setupResponseResourceData(
commentThreads,
commentThreadsMetadata,
modelsAndVersionIds,
availableModelsAndVersions,
project,
resourceQueryVariables: computed(() => viewerLoadedResourcesVariables.value),
threadsQueryVariables: computed(() => threadsQueryVariables.value),
@@ -768,6 +810,11 @@ function setupInterfaceState(
const newThreadEditor = ref(false)
const hideBubbles = ref(false)
/**
* Diffing
*/
const diffState = setupUiDiffState(state)
const position = ref(new Vector3())
const target = ref(new Vector3())
const isOrthoProjection = ref(false as boolean)
@@ -775,6 +822,9 @@ function setupInterfaceState(
return {
...state,
ui: {
diff: {
...diffState
},
selection,
lightConfig,
explodeFactor,
@@ -881,6 +931,7 @@ export function useResetUiState() {
ui: { threads, camera, sectionBox, highlightedObjectIds, lightConfig }
} = useInjectedViewerState()
const { resetFilters } = useFilterUtilities()
const { endDiff } = useDiffUtilities()
return async () => {
await threads.closeAllThreads()
@@ -889,5 +940,6 @@ export function useResetUiState() {
highlightedObjectIds.value = []
lightConfig.value = { ...DefaultLightConfiguration }
resetFilters()
endDiff()
}
}
@@ -0,0 +1,152 @@
import { Nullable, Optional, SpeckleViewer } from '@speckle/shared'
import { DiffResult, VisualDiffMode } from '@speckle/viewer'
import { ViewerModelVersionCardItemFragment } from '~~/lib/common/generated/gql/graphql'
import {
InitialStateWithUrlHashState,
InjectableViewerState
} from '~~/lib/viewer/composables/setup'
import {
isObjectLike,
has,
get,
isArray,
differenceBy,
sortBy,
isString
} from 'lodash-es'
export function setupUiDiffState(
state: InitialStateWithUrlHashState
): InjectableViewerState['ui']['diff'] {
const {
urlHashState: { diff },
resources: {
response: { availableModelsAndVersions }
}
} = state
const result = shallowRef(undefined as Optional<DiffResult>)
const time = ref(0.5)
const mode = ref<VisualDiffMode>(VisualDiffMode.COLORED)
// TODO: Only single diff for now
const getVersion = (type: keyof DiffInstruction) => {
const instruction = diff.value?.diffs[0]
if (!instruction) return undefined
const model = availableModelsAndVersions.value.find(
(m) => m.model.id === instruction[type].modelId
)
if (!model) return undefined
return model.versions.find((v) => v.id === instruction[type].versionId)
}
const versionA = computed(
(): Optional<ViewerModelVersionCardItemFragment> => getVersion('versionA')
)
const versionB = computed(
(): Optional<ViewerModelVersionCardItemFragment> => getVersion('versionB')
)
const sortedActiveVersions = computed(() => {
if (!versionA.value || !versionB.value) return null
const sorted = sortBy([versionA.value, versionB.value], (v) =>
new Date(v.createdAt).getTime()
)
return {
oldVersion: sorted[0],
newVersion: sorted[1]
}
})
const enabled = computed(() => !!(diff.value && sortedActiveVersions.value))
return {
newVersion: computed(() => sortedActiveVersions.value?.newVersion),
oldVersion: computed(() => sortedActiveVersions.value?.oldVersion),
time,
mode,
enabled,
result
}
}
export type DiffInstruction = {
versionA: SpeckleViewer.ViewerRoute.ViewerVersionResource
versionB: SpeckleViewer.ViewerRoute.ViewerVersionResource
}
export type DiffStateCommand = {
diffs: DiffInstruction[]
}
export function useDiffBuilderUtilities() {
const serializeDiffCommand = (command: DiffStateCommand): string =>
JSON.stringify(command)
const deserializeDiffCommand = (
command: Nullable<string>
): Nullable<DiffStateCommand> => {
if (!command) return null
try {
const deserializedCommand = JSON.parse(command) as unknown
if (!isObjectLike(deserializedCommand)) throw new Error('Invalid structure')
if (!has(deserializedCommand, 'diffs')) throw new Error('Invalid structure')
const diffs = get(deserializedCommand, 'diffs') as unknown
if (!isArray(diffs)) throw new Error('Invalid structure')
const finalDiffs: DiffInstruction[] = []
for (const diff of diffs) {
const getResource = (
val: unknown
): Nullable<SpeckleViewer.ViewerRoute.ViewerVersionResource> => {
const valString = isString(val) ? val : null
if (!valString) return null
const [resource] = SpeckleViewer.ViewerRoute.parseUrlParameters(valString)
if (!resource || !SpeckleViewer.ViewerRoute.isModelResource(resource))
return null
const modelId = resource.modelId
const versionId = resource.versionId
if (!modelId || !versionId) return null
return new SpeckleViewer.ViewerRoute.ViewerVersionResource(modelId, versionId)
}
const versionA = getResource(get(diff, 'versionA'))
const versionB = getResource(get(diff, 'versionB'))
if (!versionA || !versionB) continue
finalDiffs.push({
versionB,
versionA
})
}
if (!finalDiffs.length) throw new Error('No valid resource referneces found')
return { diffs: finalDiffs }
} catch (e) {
console.warn('Diff command deserialization failed', e)
return null
}
}
const areDiffsEqual = (a: DiffStateCommand, b: DiffStateCommand): boolean => {
const differentInstructions = differenceBy(
a.diffs,
b.diffs,
(instruction) =>
`${instruction.versionA.toString()}->${instruction.versionB.toString()}`
)
return differentInstructions.length < 1
}
return {
serializeDiffCommand,
deserializeDiffCommand,
areDiffsEqual
}
}
@@ -4,7 +4,8 @@ import {
PropertyInfo,
StringPropertyInfo,
SunLightConfiguration,
ViewerEvent
ViewerEvent,
VisualDiffMode
} from '@speckle/viewer'
import { useAuthCookie } from '~~/lib/auth/composables/auth'
import {
@@ -44,6 +45,7 @@ import { Vector3 } from 'three'
import { areVectorsLooselyEqual } from '~~/lib/viewer/helpers/three'
import { Nullable } from '@speckle/shared'
import { useCameraUtilities } from '~~/lib/viewer/composables/ui'
import { watchTriggerable } from '@vueuse/core'
function useViewerIsBusyEventHandler() {
const state = useInjectedViewerState()
@@ -102,7 +104,9 @@ function useViewerObjectAutoLoading() {
// Viewer initialized - load in all resources
if (newIsInitialized && !oldIsInitialized) {
const allObjectIds = getUniqueObjectIds(newResources)
await Promise.all(allObjectIds.map((i) => loadObject(i)))
return
}
@@ -301,11 +305,7 @@ function useViewerCameraIntegration() {
// debouncing pos/target updates to avoid jitteriness + spotlight mode unnecessarily disabling
useViewerCameraTracker(
() => {
const cameraManuallyChanged = loadCameraDataFromViewer()
if (cameraManuallyChanged) {
// Stop following TODO: Doesn't work very well
// spotlightUserSessionId.value = null
}
loadCameraDataFromViewer()
}
// { debounceWait: 100 }
)
@@ -369,7 +369,6 @@ function useViewerCameraIntegration() {
if ((!newVal && !oldVal) || (oldVal && areVectorsLooselyEqual(newVal, oldVal))) {
return
}
instance.setView({
position: newVal,
target: target.value
@@ -479,14 +478,14 @@ function useViewerFiltersIntegration() {
{ immediate: true, flush: 'sync' }
)
const syncColorFilterToViewer = (
const syncColorFilterToViewer = async (
filter: Nullable<PropertyInfo>,
isApplied: boolean
) => {
const targetFilter = filter || speckleTypeFilter.value
if (isApplied && targetFilter) instance.setColorFilter(targetFilter)
if (!isApplied) instance.removeColorFilter()
if (isApplied && targetFilter) await instance.setColorFilter(targetFilter)
if (!isApplied) await instance.removeColorFilter()
}
watch(
@@ -495,19 +494,19 @@ function useViewerFiltersIntegration() {
filters.propertyFilter.filter.value,
filters.propertyFilter.isApplied.value
],
(newVal) => {
async (newVal) => {
const [filter, isApplied] = newVal
syncColorFilterToViewer(filter, isApplied)
await syncColorFilterToViewer(filter, isApplied)
},
{ immediate: true, flush: 'sync' }
)
useOnViewerLoadComplete(
() => {
async () => {
const targetFilter =
filters.propertyFilter.filter.value || speckleTypeFilter.value
const isApplied = filters.propertyFilter.isApplied.value
syncColorFilterToViewer(targetFilter, isApplied)
await syncColorFilterToViewer(targetFilter, isApplied)
},
{ initialOnly: true }
)
@@ -594,6 +593,109 @@ function useExplodeFactorIntegration() {
)
}
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()
})
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
function useDebugViewerEvents() {
if (process.server) return
@@ -624,6 +726,7 @@ export function useViewerPostSetup() {
useViewerFiltersIntegration()
useLightConfigIntegration()
useExplodeFactorIntegration()
useDiffingIntegration()
// test
// useDebugViewerEvents()
@@ -1,5 +1,7 @@
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
import { Nullable } from '@speckle/shared'
import { SelectionEvent } from '@speckle/viewer'
import { SpeckleObject } from '~~/lib/common/helpers/sceneExplorer'
import { useInjectedViewerState } from '~~/lib/viewer/composables/setup'
import { useCameraUtilities, useSelectionUtilities } from '~~/lib/viewer/composables/ui'
import { useSelectionEvents } from '~~/lib/viewer/composables/viewer'
@@ -35,6 +37,30 @@ function useSelectOrZoomOnSelection() {
if (!firstVisibleSelectionHit) return clearSelection()
addToSelection(firstVisibleSelectionHit.object)
// Expands default viewer selection behaviour with a special case in diff mode.
// In diff mode, if we select via a mouse click an object, and that object is
// "modified", we want to select its pair as well.
if (
state.ui.diff.enabled.value &&
state.ui.diff.result.value &&
firstVisibleSelectionHit.object.applicationId
) {
const modifiedObjectPairs = state.ui.diff.result.value.modified
const obj = firstVisibleSelectionHit.object
const pairedItems = modifiedObjectPairs.find(
(item) =>
(item[0].model.raw as SpeckleObject).id === obj.id ||
(item[1].model.raw as SpeckleObject).id === obj.id
)
if (!pairedItems) return
const pair =
(pairedItems[0].model.raw as SpeckleObject).id === obj.id
? (pairedItems[1].model.raw as SpeckleObject)
: (pairedItems[0].model.raw as SpeckleObject)
if (!pair) return
addToSelection(pair)
}
},
doubleClickCallback: (args, { firstVisibleSelectionHit }) => {
if (!args) return zoom()
@@ -1,13 +1,16 @@
import { writableAsyncComputed } from '~~/lib/common/composables/async'
import { useRouteHashState } from '~~/lib/common/composables/url'
import type { InjectableViewerState } from '~~/lib/viewer/composables/setup'
import { useDiffBuilderUtilities } from '~~/lib/viewer/composables/setup/diff'
export enum ViewerHashStateKeys {
FocusedThreadId = 'threadId'
FocusedThreadId = 'threadId',
Diff = 'diff'
}
export function setupUrlHashState(): InjectableViewerState['urlHashState'] {
const { hashState } = useRouteHashState()
const { deserializeDiffCommand, serializeDiffCommand } = useDiffBuilderUtilities()
const focusedThreadId = writableAsyncComputed({
get: () => hashState.value[ViewerHashStateKeys.FocusedThreadId] || null,
@@ -17,10 +20,26 @@ export function setupUrlHashState(): InjectableViewerState['urlHashState'] {
[ViewerHashStateKeys.FocusedThreadId]: newVal
})
},
initialState: null
initialState: null,
asyncRead: false
})
const diff = writableAsyncComputed({
get: () => {
const urlValue = hashState.value[ViewerHashStateKeys.Diff]
return deserializeDiffCommand(urlValue)
},
set: async (newVal) =>
await hashState.update({
...hashState.value,
[ViewerHashStateKeys.Diff]: newVal ? serializeDiffCommand(newVal) : null
}),
initialState: null,
asyncRead: false
})
return {
focusedThreadId
focusedThreadId,
diff
}
}
@@ -1,4 +1,4 @@
import { timeoutAt } from '@speckle/shared'
import { SpeckleViewer, timeoutAt } from '@speckle/shared'
import { PropertyInfo } from '@speckle/viewer'
import { until } from '@vueuse/shared'
import { difference, isString, uniq } from 'lodash-es'
@@ -9,6 +9,7 @@ import {
useInjectedViewerInterfaceState,
useInjectedViewerState
} from '~~/lib/viewer/composables/setup'
import { useDiffBuilderUtilities } from '~~/lib/viewer/composables/setup/diff'
export function useSectionBoxUtilities() {
const { instance } = useInjectedViewer()
@@ -271,3 +272,42 @@ export function useSelectionUtilities() {
objects: selectedObjects
}
}
export function useDiffUtilities() {
const state = useInjectedViewerState()
const { serializeDiffCommand, deserializeDiffCommand, areDiffsEqual } =
useDiffBuilderUtilities()
const endDiff = async () => {
await state.urlHashState.diff.update(null)
}
const diffModelVersions = async (
modelId: string,
versionA: string,
versionB: string
) => {
await state.urlHashState.diff.update({
diffs: [
{
versionA: new SpeckleViewer.ViewerRoute.ViewerVersionResource(
modelId,
versionA
),
versionB: new SpeckleViewer.ViewerRoute.ViewerVersionResource(
modelId,
versionB
)
}
]
})
}
return {
serializeDiffCommand,
deserializeDiffCommand,
endDiff,
diffModelVersions,
areDiffsEqual
}
}
@@ -5,8 +5,8 @@ import {
} from '~~/lib/viewer/composables/setup'
import { SelectionEvent, ViewerEvent } from '@speckle/viewer'
import { debounce, isArray, throttle } from 'lodash-es'
import { until } from '@vueuse/core'
import { MaybeAsync, Nullable, TimeoutError, timeoutAt } from '@speckle/shared'
import { until } from '@vueuse/shared'
import { Vector3 } from 'three'
import { areVectorsLooselyEqual } from '~~/lib/viewer/helpers/three'
@@ -35,7 +35,9 @@ export const viewerLoadedResourcesQuery = graphql(`
id
name
updatedAt
loadedVersion: versions(filter: { priorityIds: $versionIds }, limit: 1) {
loadedVersion: versions(
filter: { priorityIds: $versionIds, priorityIdsOnly: true }
) {
items {
...ViewerModelVersionCardItem
}
@@ -82,6 +84,28 @@ export const viewerModelVersionsQuery = graphql(`
}
`)
export const viewerDiffVersionsQuery = graphql(`
query ViewerDiffVersions(
$projectId: String!
$modelId: String!
$versionAId: String!
$versionBId: String!
) {
project(id: $projectId) {
id
model(id: $modelId) {
id
versionA: version(id: $versionAId) {
...ViewerModelVersionCardItem
}
versionB: version(id: $versionBId) {
...ViewerModelVersionCardItem
}
}
}
}
`)
export const viewerLoadedThreadsQuery = graphql(`
query ViewerLoadedThreads(
$projectId: String!
@@ -28,7 +28,7 @@
<TourOnboarding />
</div>
<!-- Viewer host -->
<div class="special-gradient absolute w-screen h-screen z-10">
<div class="special-gradient absolute w-screen h-screen z-10 overflow-hidden">
<ViewerBase />
<Transition
enter-from-class="opacity-0"
+2
View File
@@ -1,6 +1,8 @@
import dayjs from 'dayjs'
import relativeTime from 'dayjs/plugin/relativeTime'
import localizedFormat from 'dayjs/plugin/localizedFormat'
export default defineNuxtPlugin(() => {
dayjs.extend(relativeTime)
dayjs.extend(localizedFormat)
})
@@ -239,6 +239,11 @@ input ModelVersionsFilter {
Make sure these specified versions are always loaded first
"""
priorityIds: [String!]
"""
Only return versions specified in `priorityIds`
"""
priorityIdsOnly: Boolean
}
enum ProjectModelsUpdatedMessageType {
@@ -163,7 +163,12 @@ export async function convertLegacyDataToState(
data.location.y as number,
data.location.z as number
]
: null
: null,
diff: {
command: null,
mode: 1,
time: 0.5
}
}
}
return ret
@@ -706,6 +706,8 @@ export type ModelMutationsUpdateArgs = {
export type ModelVersionsFilter = {
/** Make sure these specified versions are always loaded first */
priorityIds?: InputMaybe<Array<Scalars['String']>>;
/** Only return versions specified in `priorityIds` */
priorityIdsOnly?: InputMaybe<Scalars['Boolean']>;
};
export type ModelsTreeItem = {
@@ -48,7 +48,8 @@ export async function getPaginatedBranchCommits(
// Load priority commits first
let priorityCommitPromise: Optional<ReturnType<typeof getSpecificBranchCommits>> =
undefined
if (params.filter?.priorityIds && !params.cursor) {
const loadPriorityIds = params.filter?.priorityIds && !params.cursor
if (params.filter?.priorityIds && loadPriorityIds) {
priorityCommitPromise = getSpecificBranchCommits(
params.filter.priorityIds.map((i) => ({
branchId: params.branchId,
@@ -57,16 +58,23 @@ export async function getPaginatedBranchCommits(
)
}
const priorityIdsOnly = loadPriorityIds && params.filter?.priorityIdsOnly
const [results, totalCount, priorityCommits] = await Promise.all([
getPaginatedBranchCommitsDb({
...params,
filter: {
...(params.filter || {}),
// If we loaded priority commits first, exclude them from base results
excludeIds: params.filter?.priorityIds || undefined
}
}),
getBranchCommitsTotalCount(params),
!priorityIdsOnly
? getPaginatedBranchCommitsDb({
...params,
filter: {
...(params.filter || {}),
// If we loaded priority commits first, exclude them from base results
excludeIds: params.filter?.priorityIds || undefined
}
})
: { commits: [], cursor: null },
!priorityIdsOnly
? getBranchCommitsTotalCount(params)
: (priorityCommitPromise || Promise.resolve([])).then(
(commits) => commits.length
),
priorityCommitPromise || Promise.resolve([])
])
@@ -697,6 +697,8 @@ export type ModelMutationsUpdateArgs = {
export type ModelVersionsFilter = {
/** Make sure these specified versions are always loaded first */
priorityIds?: InputMaybe<Array<Scalars['String']>>;
/** Only return versions specified in `priorityIds` */
priorityIdsOnly?: InputMaybe<Scalars['Boolean']>;
};
export type ModelsTreeItem = {
@@ -38,6 +38,19 @@ export class ViewerModelResource implements ViewerResource {
}
}
export class ViewerVersionResource extends ViewerModelResource {
public versionId: string
constructor(modelId: string, versionId: string) {
super(modelId, versionId)
this.versionId = versionId
}
toJSON() {
return this.toString()
}
}
export class ViewerObjectResource implements ViewerResource {
public type: ViewerResourceType
public objectId: string
+19 -2
View File
@@ -1,7 +1,7 @@
import { intersection, isObjectLike } from 'lodash'
import { get, intersection, isObjectLike } from 'lodash'
import { MaybeNullOrUndefined, Nullable } from '../../core/helpers/utilityTypes'
export const SERIALIZED_VIEWER_STATE_VERSION = 1.1
export const SERIALIZED_VIEWER_STATE_VERSION = 1.2
export type SerializedViewerState = {
projectId: string
@@ -31,6 +31,11 @@ export type SerializedViewerState = {
newThreadEditor: boolean
}
}
diff: {
command: Nullable<string>
time: number
mode: number
}
spotlightUserSessionId: Nullable<string>
filters: {
isolatedObjectIds: string[]
@@ -99,5 +104,17 @@ export const formatSerializedViewerState = (
state.ui.spotlightUserSessionId = null
}
/**
* v1.1 -> v1.2
* - ui.diff added
*/
if (!state.ui.diff || !get(state.ui.diff, 'command')) {
state.ui.diff = {
command: null,
mode: 1,
time: 0.5
}
}
return state
}
+12
View File
@@ -122,6 +122,18 @@ export default plugin(function ({ addComponents, addBase }) {
"[type='checkbox']:focus, [type='radio']:focus": {
'@apply ring-offset-foundation': {}
},
"input[type='range']": {
'@apply appearance-none bg-transparent': {}
},
"input[type='range']::-webkit-slider-runnable-track": {
'@apply bg-black/25 rounded-full': {}
},
"input[type='range']::-moz-range-track": {
'@apply bg-black/25 rounded-full': {}
},
"input[type='range']::-ms-track": {
'@apply bg-black/25 rounded-full': {}
},
body: {
'@apply font-sans': {}
},
+2 -2
View File
@@ -262,8 +262,8 @@ export class Differ {
}
public setDiffTime(time: number) {
const from = Math.min(Math.max(1 - time, 0.2), 1)
const to = Math.min(Math.max(time, 0.2), 1)
const from = Math.min(Math.max(1 - time, 0), 1)
const to = Math.min(Math.max(time, 0), 1)
this.addedMaterials.forEach((mat) => {
mat.opacity =
+21 -3
View File
@@ -568,6 +568,10 @@ export class Viewer extends EventEmitter implements IViewer {
}
}
// Note: Alex, don't kill me over this one - it's making things in the FE much easier...
// I know this probably screws up showing multiple diffs at the same time, but for the
// time being it's probs a good compromise
private dynamicallyLoadedDiffResources = [] as string[]
public async diff(
urlA: string,
urlB: string,
@@ -575,10 +579,16 @@ export class Viewer extends EventEmitter implements IViewer {
authToken?: string
): Promise<DiffResult> {
const loadPromises = []
if (!this.tree.findId(urlA))
this.dynamicallyLoadedDiffResources = []
if (!this.tree.findId(urlA)) {
loadPromises.push(this.loadObjectAsync(urlA, authToken, undefined, 1))
if (!this.tree.findId(urlB))
this.dynamicallyLoadedDiffResources.push(urlA)
}
if (!this.tree.findId(urlB)) {
loadPromises.push(this.loadObjectAsync(urlB, authToken, undefined, 1))
this.dynamicallyLoadedDiffResources.push(urlB)
}
await Promise.all(loadPromises)
const diffResult = await this.differ.diff(urlA, urlB)
@@ -599,12 +609,20 @@ export class Viewer extends EventEmitter implements IViewer {
return Promise.resolve(diffResult)
}
public undiff() {
public async undiff() {
const pipelineOptions = this.speckleRenderer.pipelineOptions
pipelineOptions.depthSide = DoubleSide
this.speckleRenderer.pipelineOptions = pipelineOptions
this.differ.resetMaterialGroups()
this.filteringManager.removeUserMaterials()
const unloadPromises = []
if (this.dynamicallyLoadedDiffResources.length !== 0) {
for (const id of this.dynamicallyLoadedDiffResources)
unloadPromises.push(this.unloadObject(id))
}
this.dynamicallyLoadedDiffResources = []
await Promise.all(unloadPromises)
}
public setDiffTime(diffResult: DiffResult, time: number) {
+9 -5
View File
@@ -68,9 +68,7 @@ async function doWork() {
(err, stdout, stderr) => {
if (err) {
console.error(err)
return reject()
}
if (stdout) {
console.log(stdout)
}
@@ -79,9 +77,15 @@ async function doWork() {
}
}
)
proc.on('exit', () => {
console.log(`...done [${Math.round(performance.now() - now)}ms]`)
return resolve()
proc.on('exit', (code) => {
console.log(
`...done w/ status ${code} [${Math.round(performance.now() - now)}ms]`
)
if (!code) {
resolve()
} else {
reject(new Error('Failed with non-0 status code'))
}
})
})
})
+11 -1
View File
@@ -94,7 +94,17 @@
"vue-syntactic-server.trace.server": "off",
"svg.preview.background": "transparent",
"editor.tabSize": 2,
"typescript.tsdk": "node_modules/typescript/lib"
"typescript.tsdk": "node_modules/typescript/lib",
"search.exclude": {
"**/node_modules": true,
"**/bower_components": true,
"**/*.code-search": true,
"**/.nuxt": true,
"**/.output": true
},
"[vue]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
}
},
"extensions": {
// See https://go.microsoft.com/fwlink/?LinkId=827846 to learn about workspace recommendations.