Programmatic Measurements (#5346)
* feat(viewer-lib): Added MeasurementData and stuck with shared library defined measurement related types * feat(viewer-lib): Some updates: - Removed unnecessary calculations in point to point measurement. More lean now. Implemented serialization/deserialization - Tempoarary serialization/deserializaton for the rest of the measurement types - MeasurementsExtension now is able to load measurements from MeasurementData objects - Updated viewer's export list to not export mesurements related types that are now exclusively exported by the shared library * feat(viewer): Perpendicular measurements simplification (a little bit) and serialization/deserialization * chore(frontend): Updated measurement types imports * chore(viewer-lib): Removed the old normal indicator line from the perpendicular measurement * feat(viewer-lib): Updates: - Generic fromMeasurementData and toMeasurementData for all measurements since it's unniversal - Each measurement type serializes/deserializes only specialized data - Implemented ponint measurement serializing/deserializing and programmatic functionining * feat(viewer-lib): Area mesurement serialization/deserialization * feat(viewer-lib): Updates: - Each measurement subtype now reports the MeasurementType it belongs to - MeasurementsExtension now emits a MeasurementsChanged event with all the measurements as payload whenever the measurements change - units and precision are no longer serialized/deserialized on a per-measurement basis - Added sync API member addMeasurement * chore(viewer-lib): Fix compiler error * chore(viewer-lib): Added measurements getter in MeasurementExtension * feat(fe2): save/reload measurements integration (#5351) * measurements idempotent api * extra adjustments, stuff seems to work * lint fix * more lint fix * fix for visible going false * better identification * fix FlyControls change action --------- Co-authored-by: Kristaps Fabians Geikins <fabians@speckle.systems>
This commit is contained in:
committed by
GitHub
parent
c7c4d7be11
commit
e80e0de74c
@@ -90,8 +90,7 @@ const {
|
||||
isSectionBoxEnabled,
|
||||
isSectionBoxVisible
|
||||
} = useSectionBoxUtilities()
|
||||
const { getActiveMeasurement, removeMeasurement, enableMeasurements, hasMeasurements } =
|
||||
useMeasurementUtilities()
|
||||
const { enableMeasurements, hasMeasurements, measurements } = useMeasurementUtilities()
|
||||
const { resetExplode } = useFilterUtilities()
|
||||
const {
|
||||
viewMode: { mode: currentViewMode },
|
||||
@@ -273,12 +272,9 @@ registerShortcuts({
|
||||
})
|
||||
|
||||
onKeyStroke('Escape', () => {
|
||||
const isActiveMeasurement = getActiveMeasurement()
|
||||
const hasActiveMeasurements = measurements.value.length > 0
|
||||
if (hasActiveMeasurements) return
|
||||
|
||||
if (isActiveMeasurement) {
|
||||
removeMeasurement()
|
||||
return
|
||||
}
|
||||
// Only close panels if there's no active measurement
|
||||
if (activePanel.value === ActivePanel.measurements) {
|
||||
toggleMeasurements()
|
||||
|
||||
@@ -83,7 +83,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { MeasurementType } from '@speckle/viewer'
|
||||
import { MeasurementType } from '@speckle/shared/viewer/state'
|
||||
import { useMeasurementUtilities } from '~~/lib/viewer/composables/ui'
|
||||
|
||||
interface MeasurementTypeOption {
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
import { CameraController, VisualDiffMode } from '@speckle/viewer'
|
||||
import type { NumericPropertyInfo } from '@speckle/viewer'
|
||||
import type { Merge, PartialDeep } from 'type-fest'
|
||||
import { defaultMeasurementOptions } from '@speckle/shared/viewer/state'
|
||||
import { useViewerRealtimeActivityTracker } from '~/lib/viewer/composables/activity'
|
||||
import {
|
||||
isModelResource,
|
||||
@@ -129,7 +130,8 @@ export function useStateSerialization() {
|
||||
selection: state.ui.selection.value?.toArray() || null,
|
||||
measurement: {
|
||||
enabled: state.ui.measurement.enabled.value,
|
||||
options: state.ui.measurement.options.value
|
||||
options: state.ui.measurement.options.value,
|
||||
measurements: state.ui.measurement.measurements.value.slice()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -166,6 +168,7 @@ export function useApplySerializedState() {
|
||||
lightConfig,
|
||||
diff,
|
||||
viewMode,
|
||||
measurement,
|
||||
sectionBoxContext
|
||||
},
|
||||
resources: {
|
||||
@@ -391,6 +394,23 @@ export function useApplySerializedState() {
|
||||
...(state.ui?.lightConfig || {})
|
||||
}
|
||||
|
||||
// Apply measurements
|
||||
const incomingMeasurement = state.ui?.measurement
|
||||
if (incomingMeasurement) {
|
||||
if (!isUndefinedOrVoid(incomingMeasurement.enabled)) {
|
||||
measurement.enabled.value = incomingMeasurement.enabled
|
||||
}
|
||||
if (!isUndefinedOrVoid(incomingMeasurement.options)) {
|
||||
measurement.options.value = {
|
||||
...defaultMeasurementOptions,
|
||||
...incomingMeasurement.options
|
||||
}
|
||||
}
|
||||
if (!isUndefinedOrVoid(incomingMeasurement.measurements)) {
|
||||
measurement.measurements.value = incomingMeasurement.measurements
|
||||
}
|
||||
}
|
||||
|
||||
// Trigger activity update
|
||||
update()
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ import {
|
||||
ViewerEvent,
|
||||
DefaultLightConfiguration,
|
||||
LegacyViewer,
|
||||
MeasurementType,
|
||||
FilteringExtension
|
||||
} from '@speckle/viewer'
|
||||
import type {
|
||||
@@ -12,7 +11,6 @@ import type {
|
||||
PropertyInfo,
|
||||
SunLightConfiguration,
|
||||
SpeckleView,
|
||||
MeasurementOptions,
|
||||
DiffResult,
|
||||
Viewer,
|
||||
WorldTree,
|
||||
@@ -57,7 +55,11 @@ import { writableAsyncComputed } from '~~/lib/common/composables/async'
|
||||
import type { AsyncWritableComputedRef } from '~~/lib/common/composables/async'
|
||||
import { setupUiDiffState } from '~~/lib/viewer/composables/setup/diff'
|
||||
import type { DiffStateCommand } from '~~/lib/viewer/composables/setup/diff'
|
||||
import { useDiffUtilities, useFilterUtilities } from '~~/lib/viewer/composables/ui'
|
||||
import {
|
||||
useDiffUtilities,
|
||||
useFilterUtilities,
|
||||
useMeasurementUtilities
|
||||
} from '~~/lib/viewer/composables/ui'
|
||||
import { flatten, isUndefined, reduce } from 'lodash-es'
|
||||
import { setupViewerCommentBubbles } from '~~/lib/viewer/composables/setup/comments'
|
||||
import {
|
||||
@@ -67,7 +69,11 @@ import {
|
||||
import { useSynchronizedCookie } from '~~/lib/common/composables/reactiveCookie'
|
||||
import { buildManualPromise } from '@speckle/ui-components'
|
||||
import { PassReader } from '../extensions/PassReader'
|
||||
import type { SectionBoxData } from '@speckle/shared/viewer/state'
|
||||
import type {
|
||||
MeasurementData,
|
||||
MeasurementOptions,
|
||||
SectionBoxData
|
||||
} from '@speckle/shared/viewer/state'
|
||||
import {
|
||||
createGetParamFromResources,
|
||||
isAllModelsResource,
|
||||
@@ -87,6 +93,7 @@ import {
|
||||
} from '~/lib/viewer/composables/savedViews/state'
|
||||
import type { defaultEdgeColorValue } from '~/lib/viewer/composables/setup/viewMode'
|
||||
import { useViewModesSetup } from '~/lib/viewer/composables/setup/viewMode'
|
||||
import { useMeasurementsSetup } from '~/lib/viewer/composables/setup/measurements'
|
||||
|
||||
export type LoadedModel = NonNullable<
|
||||
Get<ViewerLoadedResourcesQuery, 'project.models.items[0]'>
|
||||
@@ -347,6 +354,7 @@ export type InjectableViewerState = Readonly<{
|
||||
measurement: {
|
||||
enabled: Ref<boolean>
|
||||
options: Ref<MeasurementOptions>
|
||||
measurements: Ref<Array<MeasurementData>>
|
||||
}
|
||||
/**
|
||||
* Various saved views UI settings
|
||||
@@ -1197,16 +1205,7 @@ function setupInterfaceState(
|
||||
hasAnyFiltersApplied
|
||||
},
|
||||
highlightedObjectIds,
|
||||
measurement: {
|
||||
enabled: ref(false),
|
||||
options: ref<MeasurementOptions>({
|
||||
visible: true,
|
||||
type: MeasurementType.POINTTOPOINT,
|
||||
units: 'm',
|
||||
vertexSnap: true,
|
||||
precision: 2
|
||||
})
|
||||
},
|
||||
measurement: useMeasurementsSetup(),
|
||||
savedViews: useBuildSavedViewsUIState()
|
||||
}
|
||||
}
|
||||
@@ -1268,6 +1267,7 @@ export function useResetUiState() {
|
||||
} = useInjectedViewerState()
|
||||
const { resetFilters } = useFilterUtilities()
|
||||
const { endDiff } = useDiffUtilities()
|
||||
const { reset: resetMeasurements } = useMeasurementUtilities()
|
||||
|
||||
return () => {
|
||||
camera.isOrthoProjection.value = false
|
||||
@@ -1276,6 +1276,7 @@ export function useResetUiState() {
|
||||
lightConfig.value = { ...DefaultLightConfiguration }
|
||||
viewMode.resetViewMode()
|
||||
resetFilters()
|
||||
resetMeasurements()
|
||||
endDiff()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
import {
|
||||
defaultMeasurementOptions,
|
||||
type MeasurementData,
|
||||
type MeasurementOptions
|
||||
} from '@speckle/shared/viewer/state'
|
||||
import type { Measurement } from '@speckle/viewer'
|
||||
import {
|
||||
MeasurementEvent,
|
||||
MeasurementsExtension,
|
||||
MeasurementState
|
||||
} from '@speckle/viewer'
|
||||
import { onKeyStroke, watchTriggerable } from '@vueuse/core'
|
||||
import { useInjectedViewerState } from '~/lib/viewer/composables/setup'
|
||||
import { useMeasurementUtilities } from '~/lib/viewer/composables/ui'
|
||||
import { useOnViewerLoadComplete } from '~/lib/viewer/composables/viewer'
|
||||
|
||||
export const useMeasurementsSetup = () => {
|
||||
return {
|
||||
enabled: ref(false),
|
||||
options: ref<MeasurementOptions>({
|
||||
...defaultMeasurementOptions
|
||||
}),
|
||||
measurements: ref([] as MeasurementData[])
|
||||
}
|
||||
}
|
||||
|
||||
export const useMeasurementsPostSetup = () => {
|
||||
const {
|
||||
viewer: {
|
||||
instance,
|
||||
init: { promise: viewerInitialized }
|
||||
},
|
||||
ui: { measurement }
|
||||
} = useInjectedViewerState()
|
||||
const { reset, removeActiveMeasurement } = useMeasurementUtilities()
|
||||
|
||||
const measurementsInstance = () => instance.getExtension(MeasurementsExtension)
|
||||
|
||||
// state -> viewer
|
||||
const { trigger: triggerEnabledWatch } = watchTriggerable(
|
||||
measurement.enabled,
|
||||
(newVal, oldVal) => {
|
||||
if (newVal !== oldVal) {
|
||||
measurementsInstance().enabled = newVal
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
const { trigger: triggerOptionsWatch } = watchTriggerable(
|
||||
measurement.options,
|
||||
(newVal) => {
|
||||
if (newVal) {
|
||||
measurementsInstance().options = newVal
|
||||
}
|
||||
},
|
||||
{ deep: true }
|
||||
)
|
||||
|
||||
const { trigger: triggerMeasurementsWatch, ignoreUpdates: ignoreMeasurementsWatch } =
|
||||
watchTriggerable(measurement.measurements, (newVal) => {
|
||||
measurementsInstance().setMeasurements(newVal)
|
||||
})
|
||||
|
||||
useOnViewerLoadComplete(
|
||||
({ isInitial }) => {
|
||||
if (!isInitial) return
|
||||
|
||||
triggerEnabledWatch()
|
||||
triggerOptionsWatch()
|
||||
triggerMeasurementsWatch()
|
||||
},
|
||||
{ initialOnly: true }
|
||||
)
|
||||
|
||||
// viewer -> state
|
||||
const onMeasurementsChanged = (data: Measurement[]) => {
|
||||
ignoreMeasurementsWatch(() => {
|
||||
measurement.measurements.value = data.map((m) => m.toMeasurementData())
|
||||
})
|
||||
}
|
||||
|
||||
// Set up event handlers
|
||||
viewerInitialized.then(() => {
|
||||
measurementsInstance().on(
|
||||
MeasurementEvent.MeasurementsChanged,
|
||||
onMeasurementsChanged
|
||||
)
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
// Remove handlers
|
||||
measurementsInstance().removeListener(
|
||||
MeasurementEvent.MeasurementsChanged,
|
||||
onMeasurementsChanged
|
||||
)
|
||||
|
||||
// Clear state & viewer instance, incase they dont get to sync
|
||||
reset()
|
||||
measurementsInstance().clearMeasurements()
|
||||
})
|
||||
|
||||
onKeyStroke('Delete', () => {
|
||||
removeActiveMeasurement()
|
||||
})
|
||||
onKeyStroke('Backspace', () => {
|
||||
removeActiveMeasurement()
|
||||
})
|
||||
onKeyStroke('Escape', () => {
|
||||
const activeMeasurement = measurementsInstance().activeMeasurement
|
||||
if (
|
||||
activeMeasurement &&
|
||||
activeMeasurement.state === MeasurementState.DANGLING_END
|
||||
) {
|
||||
removeActiveMeasurement()
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
import { difference, flatten, isEqual, uniq } from 'lodash-es'
|
||||
import {
|
||||
useThrottleFn,
|
||||
onKeyStroke,
|
||||
watchTriggerable,
|
||||
useMagicKeys,
|
||||
useEventListener
|
||||
@@ -52,10 +51,7 @@ import { getTargetObjectIds } from '~~/lib/object-sidebar/helpers'
|
||||
import { Vector3 } from 'three'
|
||||
import { areVectorsLooselyEqual } from '~~/lib/viewer/helpers/three'
|
||||
import { SafeLocalStorage, type Nullable } from '@speckle/shared'
|
||||
import {
|
||||
useCameraUtilities,
|
||||
useMeasurementUtilities
|
||||
} from '~~/lib/viewer/composables/ui'
|
||||
import { useCameraUtilities } from '~~/lib/viewer/composables/ui'
|
||||
import { setupDebugMode } from '~~/lib/viewer/composables/setup/dev'
|
||||
import { useEmbed } from '~/lib/viewer/composables/setup/embed'
|
||||
import { useMixpanel } from '~~/lib/core/composables/mp'
|
||||
@@ -64,6 +60,7 @@ import { graphql } from '~/lib/common/generated/gql'
|
||||
import { useTreeManagement } from '~~/lib/viewer/composables/tree'
|
||||
import { useViewerSavedViewIntegration } from '~/lib/viewer/composables/savedViews/state'
|
||||
import { useViewModesPostSetup } from '~/lib/viewer/composables/setup/viewMode'
|
||||
import { useMeasurementsPostSetup } from '~/lib/viewer/composables/setup/measurements'
|
||||
|
||||
function useViewerLoadCompleteEventHandler() {
|
||||
const state = useInjectedViewerState()
|
||||
@@ -851,46 +848,6 @@ function useDiffingIntegration() {
|
||||
})
|
||||
}
|
||||
|
||||
function useViewerMeasurementIntegration() {
|
||||
const {
|
||||
ui: { measurement },
|
||||
viewer: { instance }
|
||||
} = useInjectedViewerState()
|
||||
|
||||
const { clearMeasurements, removeMeasurement } = useMeasurementUtilities()
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
clearMeasurements()
|
||||
})
|
||||
|
||||
watch(
|
||||
() => measurement.enabled.value,
|
||||
(newVal, oldVal) => {
|
||||
if (newVal !== oldVal) {
|
||||
instance.enableMeasurements(newVal)
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
watch(
|
||||
() => ({ ...measurement.options.value }),
|
||||
(newMeasurementState) => {
|
||||
if (newMeasurementState) {
|
||||
instance.setMeasurementOptions(newMeasurementState)
|
||||
}
|
||||
},
|
||||
{ immediate: true, deep: true }
|
||||
)
|
||||
|
||||
onKeyStroke('Delete', () => {
|
||||
removeMeasurement()
|
||||
})
|
||||
onKeyStroke('Backspace', () => {
|
||||
removeMeasurement()
|
||||
})
|
||||
}
|
||||
|
||||
function useDisableZoomOnEmbed() {
|
||||
const { viewer } = useInjectedViewerState()
|
||||
const embedOptions = useEmbed()
|
||||
@@ -993,7 +950,7 @@ export function useViewerPostSetup() {
|
||||
useLightConfigIntegration()
|
||||
useExplodeFactorIntegration()
|
||||
useDiffingIntegration()
|
||||
useViewerMeasurementIntegration()
|
||||
useMeasurementsPostSetup()
|
||||
useDisableZoomOnEmbed()
|
||||
useViewerCursorIntegration()
|
||||
useViewerTreeIntegration()
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { MeasurementType } from '@speckle/viewer'
|
||||
import { MeasurementType } from '@speckle/shared/viewer/state'
|
||||
import type { SpeckleObject } from '~/lib/viewer/helpers/sceneExplorer'
|
||||
import { useMixpanel } from '~~/lib/core/composables/mp'
|
||||
import { useInjectedViewerState } from '~~/lib/viewer/composables/setup'
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
import { SpeckleViewer, TIME_MS, timeoutAt } from '@speckle/shared'
|
||||
import type {
|
||||
TreeNode,
|
||||
MeasurementOptions,
|
||||
PropertyInfo,
|
||||
ViewMode
|
||||
} from '@speckle/viewer'
|
||||
import { MeasurementsExtension, MeasurementEvent } from '@speckle/viewer'
|
||||
import type { TreeNode, PropertyInfo, ViewMode } from '@speckle/viewer'
|
||||
import { until } from '@vueuse/shared'
|
||||
import { useActiveElement } from '@vueuse/core'
|
||||
import { difference, isString, uniq } from 'lodash-es'
|
||||
@@ -28,6 +22,10 @@ import type {
|
||||
import { useMixpanel } from '~/lib/core/composables/mp'
|
||||
import { isStringPropertyInfo } from '~/lib/viewer/helpers/sceneExplorer'
|
||||
import type { defaultEdgeColorValue } from '~/lib/viewer/composables/setup/viewMode'
|
||||
import {
|
||||
defaultMeasurementOptions,
|
||||
type MeasurementOptions
|
||||
} from '@speckle/shared/viewer/state'
|
||||
|
||||
export function useSectionBoxUtilities() {
|
||||
const { instance } = useInjectedViewer()
|
||||
@@ -635,9 +633,10 @@ export function useThreadUtilities() {
|
||||
export function useMeasurementUtilities() {
|
||||
const state = useInjectedViewerState()
|
||||
|
||||
const measurementCount = ref(0)
|
||||
|
||||
const measurementOptions = computed(() => state.ui.measurement.options.value)
|
||||
const hasMeasurements = computed(
|
||||
() => state.ui.measurement.measurements.value.length > 0
|
||||
)
|
||||
|
||||
const enableMeasurements = (enabled: boolean) => {
|
||||
state.ui.measurement.enabled.value = enabled
|
||||
@@ -647,54 +646,31 @@ export function useMeasurementUtilities() {
|
||||
state.ui.measurement.options.value = options
|
||||
}
|
||||
|
||||
const removeMeasurement = () => {
|
||||
const removeActiveMeasurement = () => {
|
||||
if (state.viewer.instance?.removeMeasurement) {
|
||||
state.viewer.instance.removeMeasurement()
|
||||
}
|
||||
}
|
||||
|
||||
const clearMeasurements = () => {
|
||||
state.viewer.instance.getExtension(MeasurementsExtension).clearMeasurements()
|
||||
state.ui.measurement.measurements.value = []
|
||||
}
|
||||
|
||||
const getActiveMeasurement = () => {
|
||||
const measurementsExtension =
|
||||
state.viewer.instance.getExtension(MeasurementsExtension)
|
||||
const activeMeasurement = measurementsExtension?.activeMeasurement
|
||||
return activeMeasurement && activeMeasurement.state === 2
|
||||
}
|
||||
|
||||
const hasMeasurements = computed(() => measurementCount.value > 0)
|
||||
|
||||
const setupMeasurementListener = () => {
|
||||
const extension = state.viewer.instance?.getExtension(MeasurementsExtension)
|
||||
if (!extension) return
|
||||
|
||||
const updateCount = () => {
|
||||
measurementCount.value = (
|
||||
extension as unknown as { measurementCount: number }
|
||||
).measurementCount
|
||||
}
|
||||
|
||||
// Set initial count
|
||||
updateCount()
|
||||
|
||||
// Listen for changes
|
||||
extension.on(MeasurementEvent.CountChanged, updateCount)
|
||||
}
|
||||
|
||||
if (state.viewer.instance) {
|
||||
setupMeasurementListener()
|
||||
const reset = () => {
|
||||
state.ui.measurement.enabled.value = false
|
||||
state.ui.measurement.measurements.value = []
|
||||
state.ui.measurement.options.value = { ...defaultMeasurementOptions }
|
||||
}
|
||||
|
||||
return {
|
||||
measurementOptions,
|
||||
enableMeasurements,
|
||||
setMeasurementOptions,
|
||||
removeMeasurement,
|
||||
removeActiveMeasurement,
|
||||
clearMeasurements,
|
||||
getActiveMeasurement,
|
||||
hasMeasurements
|
||||
hasMeasurements,
|
||||
reset,
|
||||
measurements: state.ui.measurement.measurements
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -177,7 +177,8 @@ export const convertLegacyDataToStateFactory =
|
||||
},
|
||||
measurement: {
|
||||
enabled: false,
|
||||
options: null
|
||||
options: null,
|
||||
measurements: []
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -115,7 +115,13 @@ export const storeSavedViewFactory =
|
||||
const [insertedItem] = await tables.savedViews(deps.db).insert(
|
||||
{
|
||||
id: generateId(),
|
||||
...view
|
||||
...view,
|
||||
// weird ts error:
|
||||
...(view.viewerState
|
||||
? {
|
||||
viewerState: view.viewerState as SavedView['viewerState']
|
||||
}
|
||||
: {})
|
||||
},
|
||||
'*'
|
||||
)
|
||||
@@ -602,7 +608,16 @@ export const updateSavedViewRecordFactory =
|
||||
[SavedViews.col.projectId]: projectId
|
||||
})
|
||||
.update(
|
||||
{ ...update, ...(skipUpdatingDate ? {} : { updatedAt: new Date() }) },
|
||||
{
|
||||
...update,
|
||||
...(skipUpdatingDate ? {} : { updatedAt: new Date() }),
|
||||
// weird ts error:
|
||||
...(update.viewerState
|
||||
? {
|
||||
viewerState: update.viewerState as SavedView['viewerState']
|
||||
}
|
||||
: {})
|
||||
},
|
||||
'*'
|
||||
)
|
||||
|
||||
|
||||
@@ -54,7 +54,7 @@ describe('Viewer State helpers', () => {
|
||||
lightConfig: {},
|
||||
explodeFactor: 0,
|
||||
selection: null,
|
||||
measurement: { enabled: false, options: null }
|
||||
measurement: { enabled: false, options: null, measurements: [] }
|
||||
}
|
||||
}
|
||||
expect(isSerializedViewerState(valid)).toBe(true)
|
||||
@@ -166,7 +166,7 @@ describe('Viewer State helpers', () => {
|
||||
lightConfig: {},
|
||||
explodeFactor: 0,
|
||||
selection: null,
|
||||
measurement: { enabled: false, options: null }
|
||||
measurement: { enabled: false, options: null, measurements: [] }
|
||||
}
|
||||
}
|
||||
const result = inputToVersionedState(valid)
|
||||
@@ -225,7 +225,7 @@ describe('Viewer State helpers', () => {
|
||||
lightConfig: {},
|
||||
explodeFactor: 0,
|
||||
selection: null,
|
||||
measurement: { enabled: false, options: null }
|
||||
measurement: { enabled: false, options: null, measurements: [] }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,14 +7,14 @@ import { coerceUndefinedValuesToNull } from '../../core/index.js'
|
||||
export const defaultViewModeEdgeColorValue = 'DEFAULT_EDGE_COLOR'
|
||||
|
||||
/** Redefining these is unfortunate. Especially since they are not part of viewer-core */
|
||||
enum MeasurementType {
|
||||
export enum MeasurementType {
|
||||
PERPENDICULAR = 0,
|
||||
POINTTOPOINT = 1,
|
||||
AREA = 2,
|
||||
POINT = 3
|
||||
}
|
||||
|
||||
interface MeasurementOptions {
|
||||
export interface MeasurementOptions {
|
||||
visible: boolean
|
||||
type?: MeasurementType
|
||||
vertexSnap?: boolean
|
||||
@@ -23,6 +23,27 @@ interface MeasurementOptions {
|
||||
chain?: boolean
|
||||
}
|
||||
|
||||
export interface MeasurementData {
|
||||
type: MeasurementType
|
||||
startPoint: readonly [number, number, number] // vec3
|
||||
endPoint: readonly [number, number, number] // vec3
|
||||
startNormal: readonly [number, number, number] // vec3
|
||||
endNormal: readonly [number, number, number] // vec3
|
||||
value: number
|
||||
innerPoints?: (readonly [number, number, number])[] // array of vec3
|
||||
units?: string
|
||||
precision?: number
|
||||
uuid: string
|
||||
}
|
||||
|
||||
export const defaultMeasurementOptions: Readonly<MeasurementOptions> = Object.freeze({
|
||||
visible: true,
|
||||
type: MeasurementType.POINTTOPOINT,
|
||||
vertexSnap: false,
|
||||
units: 'm',
|
||||
precision: 2
|
||||
})
|
||||
|
||||
export interface SectionBoxData {
|
||||
min: number[]
|
||||
max: number[]
|
||||
@@ -40,8 +61,10 @@ export interface SectionBoxData {
|
||||
* v1.3 -> 1.4
|
||||
* - ui.viewMode -> ui.viewMode.mode
|
||||
* - ui.viewMode has new keys: edgesEnabled, edgesWeight, outlineOpacity, edgesColor
|
||||
* v1.4 -> 1.5
|
||||
* - ui.measurement.measurements added
|
||||
*/
|
||||
export const SERIALIZED_VIEWER_STATE_VERSION = 1.3
|
||||
export const SERIALIZED_VIEWER_STATE_VERSION = 1.5
|
||||
|
||||
export type SerializedViewerState = {
|
||||
projectId: string
|
||||
@@ -112,6 +135,7 @@ export type SerializedViewerState = {
|
||||
measurement: {
|
||||
enabled: boolean
|
||||
options: Nullable<MeasurementOptions>
|
||||
measurements: Array<MeasurementData>
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -160,14 +184,6 @@ const initializeMissingData = (state: UnformattedState): SerializedViewerState =
|
||||
)
|
||||
}
|
||||
|
||||
const defaultMeasurementOptions: MeasurementOptions = {
|
||||
visible: false,
|
||||
type: MeasurementType.POINTTOPOINT,
|
||||
vertexSnap: false,
|
||||
units: 'm',
|
||||
precision: 2
|
||||
}
|
||||
|
||||
const measurementOptions = {
|
||||
...defaultMeasurementOptions,
|
||||
...state.ui?.measurement?.options
|
||||
@@ -276,7 +292,8 @@ const initializeMissingData = (state: UnformattedState): SerializedViewerState =
|
||||
selection: state.ui?.selection || null,
|
||||
measurement: {
|
||||
enabled: state.ui?.measurement?.enabled ?? false,
|
||||
options: measurementOptions
|
||||
options: measurementOptions,
|
||||
measurements: state.ui?.measurement?.measurements || []
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@speckle/objectloader2": "workspace:^",
|
||||
"@speckle/shared": "workspace:^",
|
||||
"@speckle/viewer": "workspace:^",
|
||||
"tweakpane": "^3.0.8"
|
||||
},
|
||||
|
||||
@@ -14,10 +14,10 @@ import {
|
||||
GeometryType,
|
||||
SpeckleStandardMaterial,
|
||||
Assets,
|
||||
AssetType
|
||||
AssetType,
|
||||
SpeckleMesh
|
||||
} from '@speckle/viewer'
|
||||
import SnowMaterial from './SnowMaterial'
|
||||
import SpeckleMesh from '@speckle/viewer/dist/modules/objects/SpeckleMesh'
|
||||
import { RepeatWrapping, NearestFilter } from 'three'
|
||||
import snowTex from '../../../assets/snow.png'
|
||||
import { SnowFallPass } from './SnowFallPass'
|
||||
|
||||
@@ -28,7 +28,6 @@ import {
|
||||
ViewerEvent,
|
||||
BatchObject,
|
||||
VisualDiffMode,
|
||||
MeasurementType,
|
||||
ExplodeExtension,
|
||||
DiffExtension,
|
||||
SpeckleLoader,
|
||||
@@ -60,6 +59,7 @@ import {
|
||||
ObjectLoader2Factory
|
||||
} from '@speckle/objectloader2'
|
||||
import { SectionCaps } from './Extensions/SectionCaps.ts/SectionCaps'
|
||||
import { MeasurementType } from '@speckle/shared/viewer/state'
|
||||
|
||||
export default class Sandbox {
|
||||
private viewer: Viewer
|
||||
@@ -497,6 +497,7 @@ export default class Sandbox {
|
||||
})
|
||||
screenshot.on('click', async () => {
|
||||
console.warn(await this.viewer.screenshot())
|
||||
|
||||
/** Read depth */
|
||||
// const pass = [
|
||||
// ...this.viewer.getRenderer().pipeline.getPass('DEPTH'),
|
||||
@@ -505,7 +506,6 @@ export default class Sandbox {
|
||||
// const [depthData, width, height] = await this.viewer
|
||||
// .getExtension(PassReader)
|
||||
// .read(pass)
|
||||
|
||||
// console.log(PassReader.toBase64(PassReader.decodeDepth(depthData), width, height))
|
||||
})
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"useDefineForClassFields": true,
|
||||
"module": "ESNext",
|
||||
"lib": ["ESNext", "DOM"],
|
||||
"moduleResolution": "Node",
|
||||
"moduleResolution": "bundler",
|
||||
"strict": true,
|
||||
"sourceMap": true,
|
||||
"resolveJsonModule": true,
|
||||
|
||||
@@ -34,8 +34,6 @@ import type {
|
||||
import { type Utils } from './modules/Utils.js'
|
||||
import { BatchObject } from './modules/batching/BatchObject.js'
|
||||
import {
|
||||
type MeasurementOptions,
|
||||
MeasurementType,
|
||||
MeasurementsExtension,
|
||||
MeasurementEvent,
|
||||
MeasurementEventPayload
|
||||
@@ -206,7 +204,6 @@ export {
|
||||
PerpendicularMeasurement,
|
||||
AreaMeasurement,
|
||||
PointMeasurement,
|
||||
MeasurementType,
|
||||
MeasurementEvent,
|
||||
MeasurementState,
|
||||
Units,
|
||||
@@ -333,7 +330,6 @@ export type {
|
||||
IntersectionQueryResult,
|
||||
Utils,
|
||||
DiffResult,
|
||||
MeasurementOptions,
|
||||
FilteringState,
|
||||
ExtendedIntersection,
|
||||
ViewerEventPayload,
|
||||
|
||||
+9
-7
@@ -1,12 +1,15 @@
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-function-type */
|
||||
/*
|
||||
* https://medium.com/better-programming/how-to-create-your-own-event-emitter-in-javascript-fbd5db2447c4
|
||||
*/
|
||||
export default class EventEmitter {
|
||||
protected _events: Record<string, Function[]>
|
||||
|
||||
constructor() {
|
||||
this._events = {}
|
||||
}
|
||||
|
||||
on(name, listener) {
|
||||
on(name: string, listener: Function) {
|
||||
if (!this._events[name]) {
|
||||
this._events[name] = []
|
||||
}
|
||||
@@ -14,19 +17,18 @@ export default class EventEmitter {
|
||||
this._events[name].push(listener)
|
||||
}
|
||||
|
||||
removeListener(name, listenerToRemove) {
|
||||
removeListener(name: string, listenerToRemove: Function) {
|
||||
if (!this._events[name]) return
|
||||
|
||||
const filterListeners = (listener) => listener !== listenerToRemove
|
||||
const filterListeners = (listener: Function) => listener !== listenerToRemove
|
||||
|
||||
this._events[name] = this._events[name].filter(filterListeners)
|
||||
}
|
||||
|
||||
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/rest_parameters
|
||||
emit(name, ...args) {
|
||||
emit(name: string, ...args: unknown[]) {
|
||||
if (!this._events[name]) return
|
||||
|
||||
const fireCallbacks = (callback) => {
|
||||
const fireCallbacks = (callback: Function) => {
|
||||
callback(...args)
|
||||
}
|
||||
|
||||
@@ -34,6 +36,6 @@ export default class EventEmitter {
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this._events = null
|
||||
this._events = {}
|
||||
}
|
||||
}
|
||||
@@ -30,10 +30,7 @@ import {
|
||||
import { Viewer } from './Viewer.js'
|
||||
import { SectionOutlines } from './extensions/sections/SectionOutlines.js'
|
||||
import { type TreeNode, WorldTree } from './tree/WorldTree.js'
|
||||
import {
|
||||
type MeasurementOptions,
|
||||
MeasurementsExtension
|
||||
} from './extensions/measurements/MeasurementsExtension.js'
|
||||
import { MeasurementsExtension } from './extensions/measurements/MeasurementsExtension.js'
|
||||
import { ExplodeExtension } from './extensions/ExplodeExtension.js'
|
||||
import {
|
||||
DiffExtension,
|
||||
@@ -49,6 +46,7 @@ import { HybridCameraController } from './extensions/HybridCameraController.js'
|
||||
import { SectionTool } from './extensions/sections/SectionTool.js'
|
||||
import { OBB } from 'three/examples/jsm/math/OBB.js'
|
||||
import { SpeckleViewer } from '@speckle/shared'
|
||||
import { MeasurementOptions } from '@speckle/shared/viewer/state'
|
||||
|
||||
class LegacySelectionExtension extends SelectionExtension {
|
||||
/** FE2 'manually' selects objects pon it's own, so we're disabling the extension's event handler
|
||||
|
||||
@@ -15,7 +15,6 @@ import { AngleDamper } from '../../utils/AngleDamper.js'
|
||||
import { TIME_MS } from '@speckle/shared'
|
||||
|
||||
const _vectorBuff0 = new Vector3()
|
||||
const _changeEvent = { type: 'change' }
|
||||
|
||||
const _PI_2 = Math.PI / 2
|
||||
type MoveType = 'forward' | 'back' | 'left' | 'right' | 'up' | 'down'
|
||||
@@ -366,7 +365,7 @@ class FlyControls extends SpeckleControls {
|
||||
amount.x = movementY * 0.005 * this._options.lookSpeed
|
||||
|
||||
this.rotateBy(amount)
|
||||
this.emit(_changeEvent)
|
||||
this.emit('change')
|
||||
}
|
||||
|
||||
protected onKeyDown = (event: KeyboardEvent) => {
|
||||
|
||||
@@ -29,6 +29,7 @@ import polylabel from 'polylabel'
|
||||
import SpeckleBasicMaterial from '../../materials/SpeckleBasicMaterial.js'
|
||||
import { MeasurementPointGizmo } from './MeasurementPointGizmo.js'
|
||||
import { ExtendedMeshIntersection } from '../../objects/SpeckleRaycaster.js'
|
||||
import { MeasurementData, MeasurementType } from '@speckle/shared/viewer/state'
|
||||
|
||||
const _vec30 = new Vector3()
|
||||
const _vec31 = new Vector3()
|
||||
@@ -42,7 +43,9 @@ export class AreaMeasurement extends Measurement {
|
||||
private surfacePoint: Vector3 = new Vector3()
|
||||
private surfaceNormal: Vector3 = new Vector3()
|
||||
|
||||
/** The plane params defined by the first placed point */
|
||||
/** The plane params defined by the first placed point
|
||||
* When serialized they will go in the measurements startPoint and startNormal
|
||||
*/
|
||||
private planeOrigin: Vector3 = new Vector3()
|
||||
private planeNormal: Vector3 = new Vector3()
|
||||
|
||||
@@ -73,6 +76,10 @@ export class AreaMeasurement extends Measurement {
|
||||
return box
|
||||
}
|
||||
|
||||
public get measurementType(): MeasurementType {
|
||||
return MeasurementType.AREA
|
||||
}
|
||||
|
||||
public constructor() {
|
||||
super()
|
||||
|
||||
@@ -126,24 +133,24 @@ export class AreaMeasurement extends Measurement {
|
||||
if (this.pointIndex === 0) {
|
||||
this.planeOrigin.copy(this.surfacePoint)
|
||||
this.planeNormal.copy(this.surfaceNormal)
|
||||
this.startPoint.copy(this.surfacePoint)
|
||||
this.startNormal.copy(this.startNormal)
|
||||
}
|
||||
|
||||
this.addPoint()
|
||||
this.addPoint(this.surfacePoint)
|
||||
}
|
||||
|
||||
/** Adds a point to the area measurement */
|
||||
public addPoint(): number {
|
||||
const measuredPoint = new Vector3().copy(this.surfacePoint)
|
||||
public addPoint(point: Vector3): number {
|
||||
const measuredPoint = new Vector3().copy(point)
|
||||
if (this.pointIndex > 0) {
|
||||
measuredPoint.copy(
|
||||
this.projectOnPlane(this.surfacePoint, this.planeOrigin, this.planeNormal)
|
||||
)
|
||||
measuredPoint.copy(this.projectOnPlane(point, this.planeOrigin, this.planeNormal))
|
||||
/** Check to see if added location coincides with the first one. If yes, close the measurement */
|
||||
const distanceToFirst = this.surfacePoint.distanceTo(this.points[0])
|
||||
const distanceToFirst = point.distanceTo(this.points[0])
|
||||
if (distanceToFirst < 1e-10) {
|
||||
this._state = MeasurementState.COMPLETE
|
||||
measuredPoint.copy(this.measuredPoints[0])
|
||||
this.surfacePoint.copy(this.points[0])
|
||||
point.copy(this.points[0])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -155,7 +162,7 @@ export class AreaMeasurement extends Measurement {
|
||||
this.add(gizmo)
|
||||
|
||||
/** Push the points */
|
||||
this.points.push(this.surfacePoint.clone())
|
||||
this.points.push(point.clone())
|
||||
this.measuredPoints.push(measuredPoint)
|
||||
this.polygonPoints.push(measuredPoint)
|
||||
this.pointIndex++
|
||||
@@ -165,7 +172,7 @@ export class AreaMeasurement extends Measurement {
|
||||
/** Update polygon and label if required */
|
||||
if (this.points.length >= 2) {
|
||||
this.projectOnPlane(
|
||||
this.surfacePoint,
|
||||
point,
|
||||
this.planeOrigin,
|
||||
this.planeNormal,
|
||||
this.polygonPoints[0]
|
||||
@@ -269,12 +276,29 @@ export class AreaMeasurement extends Measurement {
|
||||
}
|
||||
|
||||
if (this._state === MeasurementState.COMPLETE) {
|
||||
this.pointGizmos[this.pointIndex - 1].updateLine([
|
||||
this.points[this.pointIndex - 2],
|
||||
for (let k = 0; k < this.points.length - 1; k++) {
|
||||
this.pointGizmos[k].updatePoint(this.points[k])
|
||||
this.pointGizmos[k].updateLine([this.points[k], this.points[k + 1]])
|
||||
this.pointGizmos[k].enable(false, true, true, false)
|
||||
}
|
||||
|
||||
this.pointGizmos[this.points.length - 1].updateLine([
|
||||
this.points[this.points.length - 1],
|
||||
this.points[0]
|
||||
])
|
||||
this.pointGizmos[this.pointIndex - 1].enable(false, true, false, false)
|
||||
this.pointGizmos[this.pointIndex].enable(false, false, false, false)
|
||||
/** There is always an extra gizmo, so gizmo count is point count + 1 */
|
||||
this.pointGizmos[this.points.length].enable(false, false, false, false)
|
||||
this.pointGizmos[this.points.length - 1].enable(false, false, false, false)
|
||||
this.pointGizmos[0].enable(false, true, true, true)
|
||||
|
||||
/** We force a sync so that we get correct timing on text finshing */
|
||||
this.pointGizmos[0].text._needsSync = true
|
||||
ret = this.pointGizmos[0].updateText(
|
||||
`${(this.value * getConversionFactor('m', this.units)).toFixed(
|
||||
this.precision
|
||||
)} ${this.units}²`,
|
||||
this.labelPoint
|
||||
)
|
||||
}
|
||||
|
||||
return ret
|
||||
@@ -468,4 +492,23 @@ export class AreaMeasurement extends Measurement {
|
||||
|
||||
return this.shoelaceArea(projectedPoints)
|
||||
}
|
||||
|
||||
public toMeasurementData(): MeasurementData {
|
||||
const data = super.toMeasurementData()
|
||||
data.startPoint = [this.planeOrigin.x, this.planeOrigin.y, this.planeOrigin.z]
|
||||
data.startNormal = [this.planeNormal.x, this.planeNormal.y, this.planeNormal.z]
|
||||
data.innerPoints = this.points.map((value) => [value.x, value.y, value.z])
|
||||
return data
|
||||
}
|
||||
|
||||
public fromMeasurementData(data: MeasurementData): void {
|
||||
super.fromMeasurementData(data)
|
||||
this.planeOrigin.fromArray(data.startPoint)
|
||||
this.planeNormal.fromArray(data.startNormal)
|
||||
if (data.innerPoints) {
|
||||
for (let k = 0; k < data.innerPoints?.length; k++) {
|
||||
this.addPoint(new Vector3().fromArray(data.innerPoints[k]))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import {
|
||||
Box3,
|
||||
Camera,
|
||||
MathUtils,
|
||||
Object3D,
|
||||
Plane,
|
||||
Raycaster,
|
||||
@@ -9,6 +10,7 @@ import {
|
||||
type Intersection
|
||||
} from 'three'
|
||||
import { ExtendedMeshIntersection } from '../../objects/SpeckleRaycaster.js'
|
||||
import { MeasurementData, MeasurementType } from '@speckle/shared/viewer/state'
|
||||
|
||||
export enum MeasurementState {
|
||||
HIDDEN,
|
||||
@@ -27,11 +29,17 @@ export abstract class Measurement extends Object3D {
|
||||
public value = 0
|
||||
public units = 'm'
|
||||
public precision = 2
|
||||
public measurementId: string
|
||||
|
||||
protected _state: MeasurementState = MeasurementState.HIDDEN
|
||||
protected renderingCamera: Camera | null
|
||||
protected renderingSize: Vector2 = new Vector2()
|
||||
|
||||
constructor() {
|
||||
super()
|
||||
this.measurementId = MathUtils.generateUUID()
|
||||
}
|
||||
|
||||
public set state(value: MeasurementState) {
|
||||
this._state = value
|
||||
}
|
||||
@@ -41,6 +49,7 @@ export abstract class Measurement extends Object3D {
|
||||
}
|
||||
|
||||
public abstract set isVisible(value: boolean)
|
||||
public abstract get measurementType(): MeasurementType
|
||||
|
||||
public get bounds(): Box3 {
|
||||
return new Box3().expandByPoint(this.startPoint).expandByPoint(this.endPoint)
|
||||
@@ -72,4 +81,32 @@ export abstract class Measurement extends Object3D {
|
||||
outPoint: Vector3,
|
||||
outNormal: Vector3
|
||||
): boolean
|
||||
|
||||
public toMeasurementData(): MeasurementData {
|
||||
const measurementData: MeasurementData = {
|
||||
type: this.measurementType,
|
||||
startPoint: [this.startPoint.x, this.startPoint.y, this.startPoint.z],
|
||||
endPoint: [this.endPoint.x, this.endPoint.y, this.endPoint.z],
|
||||
startNormal: [this.startNormal.x, this.startNormal.y, this.startNormal.z],
|
||||
endNormal: [this.endNormal.x, this.endNormal.y, this.endNormal.z],
|
||||
value: this.value,
|
||||
uuid: this.measurementId
|
||||
// units: this.units, // We don't write units per measurement
|
||||
// precision: this.precision // We don't write precision per measurement
|
||||
}
|
||||
|
||||
return measurementData
|
||||
}
|
||||
|
||||
public fromMeasurementData(data: MeasurementData): void {
|
||||
this.measurementId = data.uuid
|
||||
this.startPoint.set(data.startPoint[0], data.startPoint[1], data.startPoint[2])
|
||||
this.endPoint.set(data.endPoint[0], data.endPoint[1], data.endPoint[2])
|
||||
this.startNormal.set(data.startNormal[0], data.startNormal[1], data.startNormal[2])
|
||||
this.endNormal.set(data.endNormal[0], data.endNormal[1], data.endNormal[2])
|
||||
this.value = data.value
|
||||
// this.units = data.units // We don't read units per measurement
|
||||
// this.precision = data.precision // We don't read precision per measurement
|
||||
this._state = MeasurementState.COMPLETE
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,29 +13,21 @@ import { CameraController } from '../CameraController.js'
|
||||
import Logger from '../../utils/Logger.js'
|
||||
import { AreaMeasurement } from './AreaMeasurement.js'
|
||||
import { PointMeasurement } from './PointMeasurement.js'
|
||||
|
||||
export enum MeasurementType {
|
||||
PERPENDICULAR,
|
||||
POINTTOPOINT,
|
||||
AREA,
|
||||
POINT
|
||||
}
|
||||
import {
|
||||
MeasurementData,
|
||||
MeasurementOptions,
|
||||
MeasurementType
|
||||
} from '@speckle/shared/viewer/state'
|
||||
import { differenceBy } from 'lodash-es'
|
||||
|
||||
export enum MeasurementEvent {
|
||||
CountChanged = 'measurement-count-changed'
|
||||
CountChanged = 'measurement-count-changed',
|
||||
MeasurementsChanged = 'measurements-changed'
|
||||
}
|
||||
|
||||
export interface MeasurementEventPayload {
|
||||
[MeasurementEvent.CountChanged]: number
|
||||
}
|
||||
|
||||
export interface MeasurementOptions {
|
||||
visible: boolean
|
||||
type?: MeasurementType
|
||||
vertexSnap?: boolean
|
||||
units?: string
|
||||
precision?: number
|
||||
chain?: boolean
|
||||
[MeasurementEvent.MeasurementsChanged]: Measurement[]
|
||||
}
|
||||
|
||||
const DefaultMeasurementsOptions = {
|
||||
@@ -54,16 +46,17 @@ export class MeasurementsExtension extends Extension {
|
||||
|
||||
protected renderer: SpeckleRenderer
|
||||
|
||||
protected measurements: Measurement[] = []
|
||||
protected _measurements: Measurement[] = []
|
||||
protected _activeMeasurement: Measurement | null = null
|
||||
protected _selectedMeasurement: Measurement | null = null
|
||||
protected raycaster: Raycaster
|
||||
protected _options: MeasurementOptions = Object.assign({}, DefaultMeasurementsOptions)
|
||||
|
||||
private _frameLock = false
|
||||
private _paused = false
|
||||
private _sceneHit = false
|
||||
|
||||
protected raycaster: Raycaster
|
||||
|
||||
private pointBuff: Vector3 = new Vector3()
|
||||
private normalBuff: Vector3 = new Vector3()
|
||||
private screenBuff0: Vector2 = new Vector2()
|
||||
@@ -108,11 +101,16 @@ export class MeasurementsExtension extends Extension {
|
||||
}
|
||||
|
||||
public get measurementCount(): number {
|
||||
return this.measurements.length
|
||||
return this._measurements.length
|
||||
}
|
||||
|
||||
public get mesurements(): Measurement[] {
|
||||
return this._measurements
|
||||
}
|
||||
|
||||
private emitMeasurementCountChanged() {
|
||||
this.emit(MeasurementEvent.CountChanged, this.measurements.length)
|
||||
this.emit(MeasurementEvent.CountChanged, this._measurements.length)
|
||||
this.emit(MeasurementEvent.MeasurementsChanged, this._measurements)
|
||||
}
|
||||
|
||||
public constructor(viewer: IViewer, protected cameraProvider: CameraController) {
|
||||
@@ -147,7 +145,7 @@ export class MeasurementsExtension extends Extension {
|
||||
this.screenBuff0,
|
||||
this.renderer.sceneBox
|
||||
)
|
||||
this.measurements.forEach((value: Measurement) => {
|
||||
this._measurements.forEach((value: Measurement) => {
|
||||
;(this._enabled || value instanceof PointMeasurement) &&
|
||||
value.frameUpdate(camera, this.screenBuff0, this.renderer.sceneBox)
|
||||
})
|
||||
@@ -375,13 +373,7 @@ export class MeasurementsExtension extends Extension {
|
||||
if (!this._activeMeasurement) return
|
||||
|
||||
void this._activeMeasurement.update()
|
||||
if (this._activeMeasurement.value > 0) {
|
||||
this.measurements.push(this._activeMeasurement)
|
||||
this.emitMeasurementCountChanged()
|
||||
} else {
|
||||
this.renderer.scene.remove(this._activeMeasurement)
|
||||
Logger.error('Ignoring zero value measurement!')
|
||||
}
|
||||
this.pushMeasurement(this._activeMeasurement)
|
||||
|
||||
if (this._options.chain) {
|
||||
const startPoint = new Vector3()
|
||||
@@ -407,10 +399,30 @@ export class MeasurementsExtension extends Extension {
|
||||
this.viewer.requestRender()
|
||||
}
|
||||
|
||||
public removeMeasurement() {
|
||||
if (this._selectedMeasurement) {
|
||||
this.measurements.splice(this.measurements.indexOf(this._selectedMeasurement), 1)
|
||||
this.renderer.scene.remove(this._selectedMeasurement)
|
||||
protected pushMeasurement(measurement: Measurement) {
|
||||
if (measurement.value > 0) {
|
||||
this._measurements.push(measurement)
|
||||
this.emitMeasurementCountChanged()
|
||||
} else {
|
||||
this.renderer.scene.remove(measurement)
|
||||
Logger.error('Ignoring zero value measurement!')
|
||||
}
|
||||
}
|
||||
|
||||
protected findMeasurementFromData(measurementData: MeasurementData) {
|
||||
return this._measurements.find(
|
||||
(measurement) => measurement.measurementId === measurementData.uuid
|
||||
)
|
||||
}
|
||||
|
||||
public removeMeasurement(measurementData?: MeasurementData) {
|
||||
const targetMeasurement = measurementData
|
||||
? this.findMeasurementFromData(measurementData)
|
||||
: this._selectedMeasurement
|
||||
|
||||
if (targetMeasurement) {
|
||||
this._measurements.splice(this._measurements.indexOf(targetMeasurement), 1)
|
||||
this.renderer.scene.remove(targetMeasurement)
|
||||
this._selectedMeasurement = null
|
||||
this.emitMeasurementCountChanged()
|
||||
this.viewer.requestRender()
|
||||
@@ -419,12 +431,44 @@ export class MeasurementsExtension extends Extension {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Idempotent way of setting measurements
|
||||
*/
|
||||
public setMeasurements(measurements: MeasurementData[]) {
|
||||
if (!measurements.length) {
|
||||
if (this._measurements.length) {
|
||||
this.clearMeasurements()
|
||||
}
|
||||
if (this._activeMeasurement) {
|
||||
this.cancelMeasurement()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
const currentMeasurements = this._measurements.map((m) => m.toMeasurementData())
|
||||
const removableMeasurements = differenceBy(
|
||||
currentMeasurements,
|
||||
measurements,
|
||||
(m) => m.uuid
|
||||
)
|
||||
|
||||
for (const removableMeasurement of removableMeasurements) {
|
||||
this.removeMeasurement(removableMeasurement)
|
||||
}
|
||||
|
||||
for (const measurementData of measurements) {
|
||||
if (!this.findMeasurementFromData(measurementData)) {
|
||||
this.addMeasurement(measurementData)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public clearMeasurements(): void {
|
||||
this.removeMeasurement()
|
||||
this.measurements.forEach((measurement: Measurement) => {
|
||||
this._measurements.forEach((measurement: Measurement) => {
|
||||
this.renderer.scene.remove(measurement)
|
||||
})
|
||||
this.measurements = []
|
||||
this._measurements = []
|
||||
this.emitMeasurementCountChanged()
|
||||
this.viewer.requestRender()
|
||||
}
|
||||
@@ -446,11 +490,11 @@ export class MeasurementsExtension extends Extension {
|
||||
protected pickMeasurement(data: Vector2): Measurement | null {
|
||||
if (!this.renderer.renderingCamera) return null
|
||||
|
||||
this.measurements.forEach((value) => {
|
||||
this._measurements.forEach((value) => {
|
||||
value.highlight(false)
|
||||
})
|
||||
this.raycaster.setFromCamera(data, this.renderer.renderingCamera)
|
||||
const res = this.raycaster.intersectObjects(this.measurements, false)
|
||||
const res = this.raycaster.intersectObjects(this._measurements, false)
|
||||
return res[0]?.object as Measurement
|
||||
}
|
||||
|
||||
@@ -499,13 +543,13 @@ export class MeasurementsExtension extends Extension {
|
||||
}
|
||||
|
||||
protected updateClippingPlanes(planes: Plane[]): void {
|
||||
this.measurements.forEach((value) => {
|
||||
this._measurements.forEach((value) => {
|
||||
value.updateClippingPlanes(planes)
|
||||
})
|
||||
}
|
||||
|
||||
protected applyOptions() {
|
||||
const all = [this._activeMeasurement, ...this.measurements]
|
||||
const all = [this._activeMeasurement, ...this._measurements]
|
||||
const updatePromises: Promise<void>[] = []
|
||||
all.forEach((value) => {
|
||||
if (value) {
|
||||
@@ -531,19 +575,26 @@ export class MeasurementsExtension extends Extension {
|
||||
})
|
||||
}
|
||||
|
||||
public async fromMeasurementData(startPoint: Vector3, endPoint: Vector3) {
|
||||
/** Only point to point programatic measurements for now */
|
||||
const cacheType = this._options.type
|
||||
this._options.type = MeasurementType.POINTTOPOINT
|
||||
this._activeMeasurement = this.startMeasurement()
|
||||
this._activeMeasurement.isVisible = true
|
||||
this._activeMeasurement.startPoint.copy(startPoint)
|
||||
this._activeMeasurement.startNormal.copy(new Vector3(0, 0, 1))
|
||||
await this._activeMeasurement.update()
|
||||
this._activeMeasurement.state = MeasurementState.DANGLING_END
|
||||
this._activeMeasurement.endPoint.copy(endPoint)
|
||||
this._activeMeasurement.endNormal.copy(new Vector3(0, 0, 1))
|
||||
await this._activeMeasurement.update()
|
||||
this._options.type = cacheType
|
||||
public addMeasurement(measurementData: MeasurementData) {
|
||||
const cacheOptions = this._options
|
||||
this._options.type = measurementData.type
|
||||
this._options.chain = false
|
||||
this._options.vertexSnap = false
|
||||
|
||||
const measurement = this.startMeasurement()
|
||||
measurement.fromMeasurementData(measurementData)
|
||||
measurement.isVisible = true
|
||||
void measurement.update().then(() => {
|
||||
this.viewer.requestRender()
|
||||
})
|
||||
this.pushMeasurement(measurement)
|
||||
|
||||
this._options.type = cacheOptions.type
|
||||
this._options.chain = cacheOptions.chain
|
||||
this._options.vertexSnap = cacheOptions.vertexSnap
|
||||
}
|
||||
|
||||
public toMeasurementData(): MeasurementData[] {
|
||||
return this._measurements.map((val) => val.toMeasurementData())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,13 +5,13 @@ import {
|
||||
Raycaster,
|
||||
Vector2,
|
||||
Vector3,
|
||||
Vector4,
|
||||
type Intersection
|
||||
} from 'three'
|
||||
import { getConversionFactor } from '../../converter/Units.js'
|
||||
import { Measurement, MeasurementState } from './Measurement.js'
|
||||
import { ObjectLayers } from '../../../IViewer.js'
|
||||
import { MeasurementPointGizmo } from './MeasurementPointGizmo.js'
|
||||
import { MeasurementData, MeasurementType } from '@speckle/shared/viewer/state'
|
||||
|
||||
const vec3Buff0: Vector3 = new Vector3()
|
||||
const vec3Buff1: Vector3 = new Vector3()
|
||||
@@ -19,15 +19,10 @@ const vec3Buff2: Vector3 = new Vector3()
|
||||
const vec3Buff3: Vector3 = new Vector3()
|
||||
const vec3Buff4: Vector3 = new Vector3()
|
||||
const vec3Buff5: Vector3 = new Vector3()
|
||||
const vec4Buff0: Vector4 = new Vector4()
|
||||
const vec4Buff1: Vector4 = new Vector4()
|
||||
const vec4Buff2: Vector4 = new Vector4()
|
||||
const vec2Buff0: Vector2 = new Vector2()
|
||||
|
||||
export class PerpendicularMeasurement extends Measurement {
|
||||
private startGizmo: MeasurementPointGizmo | null = null
|
||||
private endGizmo: MeasurementPointGizmo | null = null
|
||||
private normalIndicatorPixelSize = 15 * window.devicePixelRatio
|
||||
public flipStartNormal: boolean = false
|
||||
public midPoint: Vector3 = new Vector3()
|
||||
|
||||
@@ -40,6 +35,10 @@ export class PerpendicularMeasurement extends Measurement {
|
||||
return new Box3().expandByPoint(this.startPoint).expandByPoint(this.midPoint)
|
||||
}
|
||||
|
||||
public get measurementType(): MeasurementType {
|
||||
return MeasurementType.PERPENDICULAR
|
||||
}
|
||||
|
||||
public constructor() {
|
||||
super()
|
||||
this.type = 'PerpendicularMeasurement'
|
||||
@@ -89,74 +88,41 @@ export class PerpendicularMeasurement extends Measurement {
|
||||
}
|
||||
|
||||
public update(): Promise<void> {
|
||||
let ret = Promise.resolve()
|
||||
let ret: Promise<void> | undefined
|
||||
|
||||
if (isNaN(this.startPoint.length())) return ret
|
||||
if (!this.renderingCamera) return ret
|
||||
// Not sure this is needed anymore
|
||||
if (isNaN(this.startPoint.length())) return Promise.resolve()
|
||||
if (!this.renderingCamera) return Promise.resolve()
|
||||
|
||||
this.startGizmo?.updateNormalIndicator(this.startPoint, this.startNormal)
|
||||
this.startGizmo?.updatePoint(this.startPoint)
|
||||
this.endGizmo?.updateNormalIndicator(this.endPoint, this.endNormal)
|
||||
|
||||
vec3Buff5.copy(this.startNormal)
|
||||
if (this.flipStartNormal) vec3Buff5.negate()
|
||||
|
||||
const startEndDist = this.startPoint.distanceTo(this.endPoint)
|
||||
const endStartDir = vec3Buff0.copy(this.startPoint).sub(this.endPoint).normalize()
|
||||
let dot = vec3Buff5.dot(endStartDir)
|
||||
const angle = Math.acos(Math.min(Math.max(dot, -1), 1))
|
||||
this.startLineLength = Math.abs(startEndDist * Math.cos(angle))
|
||||
|
||||
this.midPoint.copy(
|
||||
vec3Buff0
|
||||
.copy(this.startPoint)
|
||||
.add(vec3Buff1.copy(vec3Buff5).multiplyScalar(this.startLineLength))
|
||||
)
|
||||
|
||||
const textPos = vec3Buff0
|
||||
.copy(this.startPoint)
|
||||
.add(this.midPoint)
|
||||
.multiplyScalar(0.5)
|
||||
|
||||
if (this._state === MeasurementState.DANGLING_START) {
|
||||
const startLine0 = vec3Buff0.copy(this.startPoint)
|
||||
|
||||
// Compute start point in clip space
|
||||
const startNDC = vec4Buff0
|
||||
.set(this.startPoint.x, this.startPoint.y, this.startPoint.z, 1)
|
||||
.applyMatrix4(this.renderingCamera.matrixWorldInverse)
|
||||
.applyMatrix4(this.renderingCamera.projectionMatrix)
|
||||
// Move to NDC
|
||||
const startpDiv = startNDC.w
|
||||
startNDC.multiplyScalar(1 / startpDiv)
|
||||
|
||||
// Compute start point normal in clip space
|
||||
const normalNDC = vec4Buff1
|
||||
.set(this.startNormal.x, this.startNormal.y, this.startNormal.z, 0)
|
||||
.applyMatrix4(this.renderingCamera.matrixWorldInverse)
|
||||
.applyMatrix4(this.renderingCamera.projectionMatrix)
|
||||
.normalize()
|
||||
|
||||
const pixelScale = vec2Buff0.set(
|
||||
(this.normalIndicatorPixelSize / this.renderingSize.x) * 2,
|
||||
(this.normalIndicatorPixelSize / this.renderingSize.y) * 2
|
||||
)
|
||||
|
||||
// Add the scaled NDC normal to the NDC start point, we get the end point in NDC
|
||||
const endNDC = vec4Buff2
|
||||
.set(startNDC.x, startNDC.y, startNDC.z, 1)
|
||||
.add(
|
||||
vec4Buff1.set(normalNDC.x * pixelScale.x, normalNDC.y * pixelScale.y, 0, 0)
|
||||
)
|
||||
// Back to clip
|
||||
endNDC.multiplyScalar(startpDiv)
|
||||
// Back to world
|
||||
endNDC
|
||||
.applyMatrix4(this.renderingCamera.projectionMatrixInverse)
|
||||
.applyMatrix4(this.renderingCamera.matrixWorld)
|
||||
this.startGizmo?.updateLine([
|
||||
startLine0,
|
||||
vec3Buff1.set(endNDC.x, endNDC.y, endNDC.z)
|
||||
])
|
||||
|
||||
this.endGizmo?.enable(false, false, false, false)
|
||||
}
|
||||
|
||||
if (this._state === MeasurementState.DANGLING_END) {
|
||||
vec3Buff5.copy(this.startNormal)
|
||||
if (this.flipStartNormal) vec3Buff5.negate()
|
||||
|
||||
const startEndDist = this.startPoint.distanceTo(this.endPoint)
|
||||
const endStartDir = vec3Buff0.copy(this.startPoint).sub(this.endPoint).normalize()
|
||||
let dot = vec3Buff5.dot(endStartDir)
|
||||
const angle = Math.acos(Math.min(Math.max(dot, -1), 1))
|
||||
this.startLineLength = Math.abs(startEndDist * Math.cos(angle))
|
||||
|
||||
this.midPoint.copy(
|
||||
vec3Buff0
|
||||
.copy(this.startPoint)
|
||||
.add(vec3Buff1.copy(vec3Buff5).multiplyScalar(this.startLineLength))
|
||||
)
|
||||
const endLineNormal = vec3Buff1.copy(this.midPoint).sub(this.endPoint).normalize()
|
||||
|
||||
this.endLineLength = this.midPoint.distanceTo(this.endPoint)
|
||||
@@ -187,10 +153,6 @@ export class PerpendicularMeasurement extends Measurement {
|
||||
])
|
||||
this.endGizmo?.updatePoint(this.midPoint)
|
||||
|
||||
const textPos = vec3Buff0
|
||||
.copy(this.startPoint)
|
||||
.add(vec3Buff1.copy(vec3Buff5).multiplyScalar(this.startLineLength * 0.5))
|
||||
|
||||
this.value = this.midPoint.distanceTo(this.startPoint)
|
||||
if (this.startGizmo)
|
||||
ret = this.startGizmo.updateText(
|
||||
@@ -202,17 +164,20 @@ export class PerpendicularMeasurement extends Measurement {
|
||||
this.endGizmo?.enable(true, true, true, true)
|
||||
}
|
||||
if (this._state === MeasurementState.COMPLETE) {
|
||||
if (this.startGizmo)
|
||||
ret = this.startGizmo.updateText(
|
||||
`${(this.value * getConversionFactor('m', this.units)).toFixed(
|
||||
this.precision
|
||||
)} ${this.units}`
|
||||
)
|
||||
this.startGizmo?.updateLine([this.startPoint, this.midPoint])
|
||||
this.endGizmo?.updatePoint(this.midPoint)
|
||||
|
||||
ret = this.startGizmo?.updateText(
|
||||
`${(this.value * getConversionFactor('m', this.units)).toFixed(
|
||||
this.precision
|
||||
)} ${this.units}`,
|
||||
textPos
|
||||
)
|
||||
this.startGizmo?.enable(false, true, true, true)
|
||||
this.endGizmo?.enable(false, false, true, false)
|
||||
}
|
||||
|
||||
return ret
|
||||
return ret ?? Promise.resolve()
|
||||
}
|
||||
|
||||
public raycast(raycaster: Raycaster, intersects: Array<Intersection>) {
|
||||
@@ -240,4 +205,20 @@ export class PerpendicularMeasurement extends Measurement {
|
||||
if (this.startGizmo) this.startGizmo.updateClippingPlanes(planes)
|
||||
if (this.endGizmo) this.endGizmo.updateClippingPlanes(planes)
|
||||
}
|
||||
|
||||
public toMeasurementData(): MeasurementData {
|
||||
const data = super.toMeasurementData()
|
||||
data.innerPoints = [[this.midPoint.x, this.midPoint.y, this.midPoint.z]]
|
||||
return data
|
||||
}
|
||||
|
||||
public fromMeasurementData(data: MeasurementData): void {
|
||||
super.fromMeasurementData(data)
|
||||
if (data.innerPoints)
|
||||
this.midPoint.set(
|
||||
data.innerPoints[0][0],
|
||||
data.innerPoints[0][1],
|
||||
data.innerPoints[0][2]
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ import { Measurement, MeasurementState } from './Measurement.js'
|
||||
import { ObjectLayers } from '../../../IViewer.js'
|
||||
import { TextLabel } from '../../objects/TextLabel.js'
|
||||
import { MeasurementPointGizmo } from './MeasurementPointGizmo.js'
|
||||
import { MeasurementType } from '@speckle/shared/viewer/state'
|
||||
|
||||
const _vec40 = new Vector4()
|
||||
const _vec41 = new Vector4()
|
||||
@@ -44,6 +45,10 @@ export class PointMeasurement extends Measurement {
|
||||
this.zLabel.visible = value
|
||||
}
|
||||
|
||||
public get measurementType(): MeasurementType {
|
||||
return MeasurementType.POINT
|
||||
}
|
||||
|
||||
public constructor() {
|
||||
super()
|
||||
this.type = 'PointMeasurement'
|
||||
|
||||
@@ -11,12 +11,9 @@ import { getConversionFactor } from '../../converter/Units.js'
|
||||
import { Measurement, MeasurementState } from './Measurement.js'
|
||||
import { ObjectLayers } from '../../../IViewer.js'
|
||||
import { MeasurementPointGizmo } from './MeasurementPointGizmo.js'
|
||||
import { MeasurementType } from '@speckle/shared/viewer/state'
|
||||
|
||||
const vec3Buff0: Vector3 = new Vector3()
|
||||
const vec3Buff1: Vector3 = new Vector3()
|
||||
const vec3Buff2: Vector3 = new Vector3()
|
||||
const vec3Buff3: Vector3 = new Vector3()
|
||||
const vec3Buff4: Vector3 = new Vector3()
|
||||
|
||||
export class PointToPointMeasurement extends Measurement {
|
||||
private startGizmo: MeasurementPointGizmo | null = null
|
||||
@@ -27,6 +24,10 @@ export class PointToPointMeasurement extends Measurement {
|
||||
this.endGizmo?.enable(value, value, value, value)
|
||||
}
|
||||
|
||||
public get measurementType(): MeasurementType {
|
||||
return MeasurementType.POINTTOPOINT
|
||||
}
|
||||
|
||||
public constructor() {
|
||||
super()
|
||||
this.type = 'PointToPointMeasurement'
|
||||
@@ -61,54 +62,51 @@ export class PointToPointMeasurement extends Measurement {
|
||||
}
|
||||
|
||||
public update(): Promise<void> {
|
||||
let ret: Promise<void> = Promise.resolve()
|
||||
let ret: Promise<void> | undefined
|
||||
this.startGizmo?.updateNormalIndicator(this.startPoint, this.startNormal)
|
||||
this.startGizmo?.updatePoint(this.startPoint)
|
||||
this.endGizmo?.updateNormalIndicator(this.endPoint, this.endNormal)
|
||||
|
||||
this.startLineLength = this.startPoint.distanceTo(this.endPoint)
|
||||
this.value = this.startLineLength
|
||||
|
||||
const textPos = vec3Buff0
|
||||
.copy(this.startPoint)
|
||||
.add(this.endPoint)
|
||||
.multiplyScalar(0.5)
|
||||
|
||||
if (this._state === MeasurementState.DANGLING_START) {
|
||||
const startLine0 = vec3Buff0.copy(this.startPoint)
|
||||
const startLine1 = vec3Buff1
|
||||
.copy(this.startPoint)
|
||||
.add(vec3Buff2.copy(this.startNormal).multiplyScalar(this.startLineLength))
|
||||
this.startGizmo?.updateLine([startLine0, startLine1])
|
||||
this.startGizmo?.enable(true, false, true, false)
|
||||
this.endGizmo?.enable(false, false, false, false)
|
||||
}
|
||||
if (this._state === MeasurementState.DANGLING_END) {
|
||||
this.startLineLength = this.startPoint.distanceTo(this.endPoint)
|
||||
this.value = this.startLineLength
|
||||
this.startGizmo?.enable(true, true, true, true)
|
||||
this.endGizmo?.enable(true, false, true, false)
|
||||
|
||||
const endStartDir = vec3Buff0.copy(this.endPoint).sub(this.startPoint).normalize()
|
||||
const lineEndPoint = vec3Buff1
|
||||
.copy(this.startPoint)
|
||||
.add(vec3Buff2.copy(endStartDir).multiplyScalar(this.startLineLength))
|
||||
this.startGizmo?.updateLine([this.startPoint, this.endPoint])
|
||||
this.endGizmo?.updatePoint(this.endPoint)
|
||||
|
||||
const textPos = vec3Buff3
|
||||
.copy(this.startPoint)
|
||||
.add(vec3Buff4.copy(endStartDir).multiplyScalar(this.startLineLength * 0.5))
|
||||
|
||||
this.startGizmo?.updateLine([this.startPoint, lineEndPoint])
|
||||
this.endGizmo?.updatePoint(lineEndPoint)
|
||||
if (this.startGizmo)
|
||||
ret = this.startGizmo.updateText(
|
||||
`${(this.value * getConversionFactor('m', this.units)).toFixed(
|
||||
this.precision
|
||||
)} ${this.units}`,
|
||||
textPos
|
||||
)
|
||||
this.endGizmo?.enable(true, true, true, true)
|
||||
ret = this.startGizmo?.updateText(
|
||||
`${(this.value * getConversionFactor('m', this.units)).toFixed(
|
||||
this.precision
|
||||
)} ${this.units}`,
|
||||
textPos
|
||||
)
|
||||
}
|
||||
if (this._state === MeasurementState.COMPLETE) {
|
||||
this.startGizmo?.enable(false, true, true, true)
|
||||
this.endGizmo?.enable(false, false, true, false)
|
||||
if (this.startGizmo)
|
||||
ret = this.startGizmo.updateText(
|
||||
`${(this.value * getConversionFactor('m', this.units)).toFixed(
|
||||
this.precision
|
||||
)} ${this.units}`
|
||||
)
|
||||
|
||||
this.startGizmo?.updateLine([this.startPoint, this.endPoint])
|
||||
this.endGizmo?.updatePoint(this.endPoint)
|
||||
ret = this.startGizmo?.updateText(
|
||||
`${(this.value * getConversionFactor('m', this.units)).toFixed(
|
||||
this.precision
|
||||
)} ${this.units}`,
|
||||
textPos
|
||||
)
|
||||
}
|
||||
return ret
|
||||
return ret ?? Promise.resolve()
|
||||
}
|
||||
|
||||
public raycast(raycaster: Raycaster, intersects: Array<Intersection>) {
|
||||
|
||||
@@ -11321,6 +11321,7 @@ __metadata:
|
||||
resolution: "@speckle/viewer-sandbox@workspace:packages/viewer-sandbox"
|
||||
dependencies:
|
||||
"@speckle/objectloader2": "workspace:^"
|
||||
"@speckle/shared": "workspace:^"
|
||||
"@speckle/viewer": "workspace:^"
|
||||
"@tweakpane/core": "npm:^1.0.9"
|
||||
"@typescript-eslint/eslint-plugin": "npm:^7.12.0"
|
||||
|
||||
Reference in New Issue
Block a user