c8bdf01cdd
* RadioGroup & Initial UI for Measure * Add option to Panel to allow actions to move to bottom * Typo * Add count to precision * Add enable, snap and type api integrations * Update Units WIP * Add precision update * Update v-tippy name * Updates * New design * Better darkmode radio. Keystrokes. * Styling fixes. Fix select mount-menu-on-body * Fix ts bug * Show label in Select for units * Update shortcut to D * Small design changes * Small tidy ups * WIP New Measurements Helper State * Fix build erros * Remove viewer import from shared * Delete WIP * Fix delete * Fix close button on measure mode * Measurement nullable * Updates from PR * Seperate measurements into measurementsEnabled & measurementOptions * Update state.ts * Update ts bugs * Updates to RadioGroup * Use ctx.updateArgs * Replace RadioGroup with Radio - More consistent with existing inputs * Update FE2 to use new Radio * Fix circleci fail * Fix build * Fix wrong initial state for vertexSnap * Adjust type to measurement * Use Lodash isEqual * Fix bug where units don't update * Remove double input * Fix server error in data.ts * Revert change around useEqual
406 lines
12 KiB
Vue
406 lines
12 KiB
Vue
<template>
|
|
<div>
|
|
<div
|
|
class="absolute z-20 flex h-[100dvh] flex-col space-y-2 bg-green-300/0 px-2 pt-[4.2rem]"
|
|
>
|
|
<!-- Models -->
|
|
<ViewerControlsButtonToggle
|
|
v-tippy="modelsShortcut"
|
|
:active="activeControl === 'models'"
|
|
@click="toggleActiveControl('models')"
|
|
>
|
|
<CubeIcon class="h-5 w-5" />
|
|
</ViewerControlsButtonToggle>
|
|
|
|
<!-- Explorer -->
|
|
<ViewerControlsButtonToggle
|
|
v-tippy="explorerShortcut"
|
|
:active="activeControl === 'explorer'"
|
|
@click="toggleActiveControl('explorer')"
|
|
>
|
|
<IconFileExplorer class="h-5 w-5" />
|
|
</ViewerControlsButtonToggle>
|
|
|
|
<!-- Measurements -->
|
|
<ViewerControlsButtonToggle
|
|
v-tippy="measureShortcut"
|
|
:active="activeControl === 'measurements'"
|
|
@click="toggleMeasurements"
|
|
>
|
|
<IconMeasurements class="h-5 w-5" />
|
|
</ViewerControlsButtonToggle>
|
|
|
|
<!-- TODO -->
|
|
<!-- <ViewerControlsButtonToggle
|
|
:active="activeControl === 'filters'"
|
|
@click="toggleActiveControl('filters')"
|
|
>
|
|
<FunnelIcon class="w-5 h-5" />
|
|
</ViewerControlsButtonToggle> -->
|
|
|
|
<!-- Comment threads -->
|
|
<ViewerControlsButtonToggle
|
|
v-tippy="discussionsShortcut"
|
|
:active="activeControl === 'discussions'"
|
|
@click="toggleActiveControl('discussions')"
|
|
>
|
|
<ChatBubbleLeftRightIcon class="h-5 w-5" />
|
|
</ViewerControlsButtonToggle>
|
|
|
|
<!-- Automateeeeeeee FTW -->
|
|
<ViewerControlsButtonToggle
|
|
v-if="allAutomationRuns.length !== 0"
|
|
v-tippy="summary.longSummary"
|
|
:active="activeControl === 'automate'"
|
|
class="p-2"
|
|
@click="toggleActiveControl('automate')"
|
|
>
|
|
<!-- <PlayCircleIcon class="h-5 w-5" /> -->
|
|
<!-- {{allAutomationRuns.length}} -->
|
|
<AutomationDoughnutSummary :summary="summary" />
|
|
</ViewerControlsButtonToggle>
|
|
|
|
<!-- TODO: direct add comment -->
|
|
<!-- <ViewerCommentsDirectAddComment v-show="activeControl === 'comments'" /> -->
|
|
|
|
<!-- Standard viewer controls -->
|
|
<ViewerControlsButtonGroup>
|
|
<!-- Views -->
|
|
<ViewerViewsMenu v-tippy="'Views'" />
|
|
<!-- Zoom extents -->
|
|
<ViewerControlsButtonToggle
|
|
v-tippy="zoomExtentsShortcut"
|
|
flat
|
|
@click="trackAndzoomExtentsOrSelection()"
|
|
>
|
|
<ArrowsPointingOutIcon class="h-5 w-5" />
|
|
</ViewerControlsButtonToggle>
|
|
|
|
<!-- Sun and lights -->
|
|
<ViewerSunMenu v-tippy="'Light Controls'" />
|
|
</ViewerControlsButtonGroup>
|
|
<ViewerControlsButtonGroup>
|
|
<!-- Projection type -->
|
|
<!-- TODO (Question for fabs): How to persist state between page navigation? e.g., swap to iso mode, move out, move back, iso mode is still on in viewer but not in ui -->
|
|
<ViewerControlsButtonToggle
|
|
v-tippy="projectionShortcut"
|
|
flat
|
|
secondary
|
|
:active="isOrthoProjection"
|
|
@click="trackAndtoggleProjection()"
|
|
>
|
|
<IconPerspective v-if="isOrthoProjection" class="h-4 w-4" />
|
|
<IconPerspectiveMore v-else class="h-4 w-4" />
|
|
</ViewerControlsButtonToggle>
|
|
|
|
<!-- Section Box -->
|
|
<ViewerControlsButtonToggle
|
|
v-tippy="sectionBoxShortcut"
|
|
flat
|
|
secondary
|
|
:active="isSectionBoxEnabled"
|
|
@click="toggleSectionBox()"
|
|
>
|
|
<ScissorsIcon class="h-5 w-5" />
|
|
</ViewerControlsButtonToggle>
|
|
|
|
<!-- Explosion -->
|
|
<ViewerExplodeMenu v-tippy="'Explode'" />
|
|
|
|
<!-- Settings -->
|
|
<ViewerSettingsMenu />
|
|
</ViewerControlsButtonGroup>
|
|
</div>
|
|
<div
|
|
ref="scrollableControlsContainer"
|
|
:class="`simple-scrollbar absolute z-10 mx-14 mt-[4rem] mb-4 max-h-[calc(100dvh-5.5rem)] w-64 sm:w-72 overflow-y-auto px-[2px] py-[2px] transition ${
|
|
activeControl !== 'none'
|
|
? 'translate-x-0 opacity-100'
|
|
: '-translate-x-[100%] opacity-0'
|
|
}`"
|
|
>
|
|
<div v-show="activeControl.length !== 0 && activeControl === 'measurements'">
|
|
<KeepAlive>
|
|
<div><ViewerMeasurementsOptions @close="toggleMeasurements" /></div>
|
|
</KeepAlive>
|
|
</div>
|
|
<div v-show="resourceItems.length !== 0 && activeControl === 'models'">
|
|
<KeepAlive>
|
|
<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="resourceItems.length !== 0 && activeControl === 'explorer'">
|
|
<KeepAlive>
|
|
<ViewerExplorer class="pointer-events-auto" @close="activeControl = 'none'" />
|
|
</KeepAlive>
|
|
</div>
|
|
<ViewerComments
|
|
v-if="resourceItems.length !== 0 && activeControl === 'discussions'"
|
|
class="pointer-events-auto"
|
|
@close="activeControl = 'none'"
|
|
/>
|
|
<ViewerFilters
|
|
v-if="resourceItems.length !== 0 && activeControl === 'filters'"
|
|
class="pointer-events-auto"
|
|
/>
|
|
<div v-show="resourceItems.length !== 0 && activeControl === 'automate'">
|
|
<ViewerAutomatePanel
|
|
:automation-runs="allAutomationRuns"
|
|
:summary="summary"
|
|
@close="activeControl = 'none'"
|
|
/>
|
|
</div>
|
|
|
|
<!-- Empty state -->
|
|
<div v-if="resourceItems.length === 0">
|
|
<div class="flex items-center py-3 px-2">
|
|
<div class="text-sm text-foreground-2">No models loaded.</div>
|
|
<div>
|
|
<FormButton
|
|
size="xs"
|
|
text
|
|
:icon-left="PlusIcon"
|
|
@click="openAddModel = true"
|
|
>
|
|
Add
|
|
</FormButton>
|
|
<ViewerResourcesAddModelDialog v-model:open="openAddModel" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
<script setup lang="ts">
|
|
import {
|
|
CubeIcon,
|
|
ChatBubbleLeftRightIcon,
|
|
ArrowsPointingOutIcon,
|
|
ScissorsIcon,
|
|
PlusIcon
|
|
} from '@heroicons/vue/24/outline'
|
|
import type { Nullable } from '@speckle/shared'
|
|
import {
|
|
useCameraUtilities,
|
|
useSectionBoxUtilities,
|
|
useMeasurementUtilities
|
|
} from '~~/lib/viewer/composables/ui'
|
|
import {
|
|
onKeyboardShortcut,
|
|
ModifierKeys,
|
|
getKeyboardShortcutTitle
|
|
} from '@speckle/ui-components'
|
|
import {
|
|
useInjectedViewerLoadedResources,
|
|
useInjectedViewerInterfaceState
|
|
} from '~~/lib/viewer/composables/setup'
|
|
import { useMixpanel } from '~~/lib/core/composables/mp'
|
|
|
|
const {
|
|
zoomExtentsOrSelection,
|
|
toggleProjection,
|
|
camera: { isOrthoProjection }
|
|
} = useCameraUtilities()
|
|
|
|
import { AutomationRunStatus } from '~~/lib/common/generated/gql/graphql'
|
|
import type { AutomationRun } from '~~/lib/common/generated/gql/graphql'
|
|
import { useIsSmallerOrEqualThanBreakpoint } from '~~/composables/browser'
|
|
|
|
const { resourceItems, modelsAndVersionIds } = useInjectedViewerLoadedResources()
|
|
|
|
const { toggleSectionBox, isSectionBoxEnabled } = useSectionBoxUtilities()
|
|
|
|
const { enableMeasurements } = useMeasurementUtilities()
|
|
|
|
const allAutomationRuns = computed(() => {
|
|
const allAutomationStatuses = modelsAndVersionIds.value
|
|
.map((model) => model.model.loadedVersion.items[0].automationStatus)
|
|
.flat()
|
|
.filter((run) => !!run)
|
|
return allAutomationStatuses
|
|
.map((status) => status?.automationRuns)
|
|
.flat() as AutomationRun[]
|
|
})
|
|
|
|
const allFunctionRuns = computed(() => {
|
|
return allAutomationRuns.value.map((run) => run.functionRuns).flat()
|
|
})
|
|
|
|
const summary = computed(() => {
|
|
const result = {
|
|
failed: 0,
|
|
passed: 0,
|
|
inProgress: 0,
|
|
total: allFunctionRuns.value.length,
|
|
title: 'All runs passed.',
|
|
titleColor: 'text-success',
|
|
longSummary: ''
|
|
}
|
|
|
|
for (const run of allFunctionRuns.value) {
|
|
switch (run?.status) {
|
|
case AutomationRunStatus.Succeeded:
|
|
result.passed++
|
|
break
|
|
case AutomationRunStatus.Failed:
|
|
result.title = 'Some runs failed.'
|
|
result.titleColor = 'text-danger'
|
|
result.failed++
|
|
break
|
|
default:
|
|
if (result.failed === 0) {
|
|
result.title = 'Some runs are still in progress.'
|
|
result.titleColor = 'text-warning'
|
|
}
|
|
result.inProgress++
|
|
break
|
|
}
|
|
}
|
|
|
|
// format:
|
|
// 2 failed, 1 passed runs
|
|
// 1 passed, 2 in progress, 1 failed runs
|
|
// 1 passed run
|
|
const longSummarySegments = []
|
|
if (result.passed > 0) longSummarySegments.push(`${result.passed} passed`)
|
|
if (result.inProgress > 0)
|
|
longSummarySegments.push(`${result.inProgress} in progress`)
|
|
if (result.failed > 0) longSummarySegments.push(`${result.failed} failed`)
|
|
|
|
result.longSummary = (
|
|
longSummarySegments.join(', ') + ` run${result.total > 1 ? 's' : ''}.`
|
|
).replace(/,(?=[^,]+$)/, ', and')
|
|
|
|
return result
|
|
})
|
|
|
|
type ActiveControl =
|
|
| 'none'
|
|
| 'models'
|
|
| 'explorer'
|
|
| 'filters'
|
|
| 'discussions'
|
|
| 'automate'
|
|
| 'measurements'
|
|
|
|
const openAddModel = ref(false)
|
|
|
|
const activeControl = ref<ActiveControl>('models')
|
|
|
|
const scrollableControlsContainer = ref(null as Nullable<HTMLDivElement>)
|
|
const {
|
|
diff: { enabled }
|
|
} = useInjectedViewerInterfaceState()
|
|
|
|
const modelsShortcut = ref(
|
|
`Models (${getKeyboardShortcutTitle([ModifierKeys.AltOrOpt, 'm'])})`
|
|
)
|
|
const explorerShortcut = ref(
|
|
`Scene Explorer (${getKeyboardShortcutTitle([ModifierKeys.AltOrOpt, 'e'])})`
|
|
)
|
|
const discussionsShortcut = ref(
|
|
`Discussions (${getKeyboardShortcutTitle([ModifierKeys.AltOrOpt, 't'])})`
|
|
)
|
|
const zoomExtentsShortcut = ref(
|
|
`Fit to screen (${getKeyboardShortcutTitle([ModifierKeys.AltOrOpt, 'Space'])})`
|
|
)
|
|
const projectionShortcut = ref(
|
|
`Projection (${getKeyboardShortcutTitle([ModifierKeys.AltOrOpt, 'p'])})`
|
|
)
|
|
const sectionBoxShortcut = ref(
|
|
`Section Box (${getKeyboardShortcutTitle([ModifierKeys.AltOrOpt, 'b'])})`
|
|
)
|
|
const measureShortcut = ref(
|
|
`Measure Mode (${getKeyboardShortcutTitle([ModifierKeys.AltOrOpt, 'd'])})`
|
|
)
|
|
|
|
const { isSmallerOrEqualSm } = useIsSmallerOrEqualThanBreakpoint()
|
|
|
|
const toggleActiveControl = (control: ActiveControl) =>
|
|
activeControl.value === control
|
|
? (activeControl.value = 'none')
|
|
: (activeControl.value = control)
|
|
|
|
onKeyboardShortcut([ModifierKeys.AltOrOpt], 'm', () => {
|
|
toggleActiveControl('models')
|
|
})
|
|
onKeyboardShortcut([ModifierKeys.AltOrOpt], 'e', () => {
|
|
toggleActiveControl('explorer')
|
|
})
|
|
onKeyboardShortcut([ModifierKeys.AltOrOpt], 'f', () => {
|
|
toggleActiveControl('filters')
|
|
})
|
|
onKeyboardShortcut([ModifierKeys.AltOrOpt], ['t'], () => {
|
|
toggleActiveControl('discussions')
|
|
})
|
|
onKeyboardShortcut([ModifierKeys.AltOrOpt], 'd', () => {
|
|
toggleActiveControl('measurements')
|
|
})
|
|
|
|
// Viewer actions kbd shortcuts
|
|
onKeyboardShortcut([ModifierKeys.AltOrOpt], ' ', () => {
|
|
trackAndzoomExtentsOrSelection()
|
|
})
|
|
onKeyboardShortcut([ModifierKeys.AltOrOpt], 'p', () => {
|
|
toggleProjection()
|
|
})
|
|
onKeyboardShortcut([ModifierKeys.AltOrOpt], 'b', () => {
|
|
toggleSectionBox()
|
|
})
|
|
|
|
const mp = useMixpanel()
|
|
watch(activeControl, (newVal) => {
|
|
mp.track('Viewer Action', { type: 'action', name: 'controls-toggle', action: newVal })
|
|
})
|
|
|
|
const trackAndzoomExtentsOrSelection = () => {
|
|
zoomExtentsOrSelection()
|
|
mp.track('Viewer Action', { type: 'action', name: 'zoom', source: 'button' })
|
|
}
|
|
|
|
const trackAndtoggleProjection = () => {
|
|
toggleProjection()
|
|
mp.track('Viewer Action', {
|
|
type: 'action',
|
|
name: 'camera',
|
|
camera: isOrthoProjection ? 'ortho' : 'perspective'
|
|
})
|
|
}
|
|
|
|
watch(isSectionBoxEnabled, (val) => {
|
|
mp.track('Viewer Action', {
|
|
type: 'action',
|
|
name: 'section-box',
|
|
status: val
|
|
})
|
|
})
|
|
|
|
const scrollControlsToBottom = () => {
|
|
// TODO: Currently this will scroll to the very bottom, which doesn't make sense when there are multiple models loaded
|
|
// if (scrollableControlsContainer.value)
|
|
// scrollToBottom(scrollableControlsContainer.value)
|
|
}
|
|
|
|
const toggleMeasurements = () => {
|
|
const isMeasurementsActive = activeControl.value === 'measurements'
|
|
enableMeasurements(!isMeasurementsActive)
|
|
activeControl.value = isMeasurementsActive ? 'none' : 'measurements'
|
|
}
|
|
|
|
onMounted(() => {
|
|
activeControl.value = isSmallerOrEqualSm.value ? 'none' : 'models'
|
|
})
|
|
|
|
watch(isSmallerOrEqualSm, (newVal) => {
|
|
activeControl.value = newVal ? 'none' : 'models'
|
|
})
|
|
</script>
|