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:
Alexandru Popovici
2025-09-02 09:46:30 +03:00
committed by GitHub
parent c7c4d7be11
commit e80e0de74c
27 changed files with 543 additions and 331 deletions
@@ -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: [] }
}
}
}
+29 -12
View File
@@ -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 || []
}
}
}
+1
View File
@@ -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'
+2 -2
View File
@@ -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))
})
+1 -1
View File
@@ -4,7 +4,7 @@
"useDefineForClassFields": true,
"module": "ESNext",
"lib": ["ESNext", "DOM"],
"moduleResolution": "Node",
"moduleResolution": "bundler",
"strict": true,
"sourceMap": true,
"resolveJsonModule": true,
-4
View File
@@ -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,
@@ -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 = {}
}
}
+2 -4
View File
@@ -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>) {
+1
View File
@@ -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"