Files
speckle-server/packages/frontend-2/lib/viewer/composables/setup/selection.ts
T
Alexandru Popovici 0ef0a13979 Pro Measurement Tools (#4452)
* feat(viewer-lib): WIP on area measurement

* feat(viewer-lib): WIP on area measurement. Basics are working. Needs more bling

* feat(viewer-lib): Implemented polygon triangulation for measured area

* chore(shared): Updated MeasurementType

* Add Area control to measure panel

* feat(viewer-lib): Real time updates of the fill polygon and area value

* feat(viewer-lib): Added pole of innacessibility as area label anchor using the polylabel libraryh

* feat(viewer-lib): Fixed fill polygon material

* feat(viewer-lib): Changed the surface normal indicator from the faulty disc to a outlined rectangle. Looks and works much better now

* fix(viewer-lib): Measurements get clipped by sections planes

* faet(viewer-lib): Measurements can now optionally define their own snapping method. Area measurement snaps to first point in screen space. Generic vertex snap still applies if enabled

* fix(viewer-lib): Forgot to project the measured point

* feat(viewer-lib): Double click auto-finishes the area measurmenet by instantly joining with the first point. Right clikc removes current area measurement point so you can 'undo'

* fix(viewer-lib: Fixed a stupid bug relatedto text because somebody thought that making it 'async' would be sooooo cool...

* fix(frontend): Prevent zoom on double click when using area measurement

* chore(viewer-lib): Refined and fomralized a bit now that the general idea of a measurement has got more complex

* chore(viewer-lib): Moved state switching a bit

* chore(viewer-lib): Replaced the old disc normal indicator with the new one and made it standard. Added an option to the gizmo's style that determines the pixels size of the normal indicator

* chore(viewer-lib): Documented the area measurement tool

* chore(viewer-lib): Some updates:
- Implemented proper bounds getter for area measurement
- Got rid of the static vector buffers in Measurement and replaced them with consts where needed
- Reduced the min click timing from 250ms to 150ms
- Other small adjustments

* feat(viewer-lib): Added the option to chain measurements

* chore(frontend-2): Added toggle for measurement chaining

* chore(viewer-lib): Perpendicular measurement chaining now align on the same line as requested

* feat(viewer-lib): Implemented point (coordinate) measurement:
- Added support for billboard offseting in NDC in the shader via vec2 offset uniform. Not a dream come true, but required mostly because of how troika works
- SpeckleText background now follows text anchoring
- Implemented new POINT measurement type

* chore(viewer-lib): Separated label position calculation. We now update only the label transform each frame, instead of updating the entire label redunantly

* chore(viewer-lib): Offsets are now constants. Removed redundant vector and matrix creation

* chore(frontend-2): Placeholder radio button for point measurement type. Fixed compile errors

* fix(viewer-lib): Fixes WEB-3105. Export all measurement types

* updated icons

* Update description

---------

Co-authored-by: andrewwallacespeckle <andrew@speckle.systems>
Co-authored-by: Mike Tasset <mike.tasset@gmail.com>
2025-05-26 12:10:19 +03:00

126 lines
4.4 KiB
TypeScript

import { MeasurementType } from '@speckle/viewer'
import type { SpeckleObject } from '~/lib/viewer/helpers/sceneExplorer'
import { useMixpanel } from '~~/lib/core/composables/mp'
import { useInjectedViewerState } from '~~/lib/viewer/composables/setup'
import { useCameraUtilities, useSelectionUtilities } from '~~/lib/viewer/composables/ui'
import { useSelectionEvents } from '~~/lib/viewer/composables/viewer'
function useCollectSelection() {
const {
ui: { selection }
} = useInjectedViewerState()
const selectionCallback: Parameters<
typeof useSelectionEvents
>[0]['singleClickCallback'] = (_event, { firstVisibleSelectionHit }) => {
if (!firstVisibleSelectionHit) return (selection.value = null) // reset selection location
selection.value = firstVisibleSelectionHit.point
}
useSelectionEvents({
singleClickCallback: selectionCallback,
doubleClickCallback: selectionCallback
})
}
function useSelectOrZoomOnSelection() {
const state = useInjectedViewerState()
const { clearSelection, addToSelection } = useSelectionUtilities()
const { zoom } = useCameraUtilities()
const mp = useMixpanel()
const logger = useLogger()
const trackAndClearSelection = () => {
clearSelection()
mp.track('Viewer Action', {
type: 'action',
name: 'selection',
action: 'clear',
source: 'viewer'
})
}
useSelectionEvents(
{
singleClickCallback: (args, { firstVisibleSelectionHit }) => {
if (!args) return trackAndClearSelection()
if (args.hits.length === 0) return trackAndClearSelection()
if (!args.multiple) clearSelection() // note we're not tracking selectino clearing here
if (!firstVisibleSelectionHit) return clearSelection()
addToSelection(firstVisibleSelectionHit.node.model.raw as SpeckleObject)
// Expands default viewer selection behaviour with a special case in diff mode.
// In diff mode, if we select via a mouse click an object, and that object is
// "modified", we want to select its pair as well.
if (
state.ui.diff.enabled.value &&
state.ui.diff.result.value &&
firstVisibleSelectionHit.node.model.raw.applicationId
) {
const modifiedObjectPairs = state.ui.diff.result.value.modified
const obj = firstVisibleSelectionHit.node.model.raw as SpeckleObject
const pairedItems = modifiedObjectPairs.find((item) => {
if (
(item[0].model.raw as SpeckleObject).id === obj.id ||
(item[1].model.raw as SpeckleObject).id === obj.id
) {
return true
}
})
if (!pairedItems) return
const pair =
(pairedItems[0].model.raw as SpeckleObject).id === obj.id
? (pairedItems[1].model.raw as SpeckleObject)
: (pairedItems[0].model.raw as SpeckleObject)
if (!pair) return
addToSelection(pair)
}
mp.track('Viewer Action', {
type: 'action',
name: 'selection',
action: 'select',
multiple: args.multiple
})
},
doubleClickCallback: (args, { firstVisibleSelectionHit }) => {
const isMeasureMode = state.ui.measurement.enabled.value
const measurementType = state.ui.measurement.options.value.type
if (
isMeasureMode &&
(measurementType === MeasurementType.PERPENDICULAR ||
measurementType === MeasurementType.AREA)
) {
return
}
if (!args) return zoom()
if (!args.hits) return zoom()
if (args.hits.length === 0) return zoom()
const firstVisHit = firstVisibleSelectionHit
if (!firstVisHit) return clearSelection()
if (state.ui.filters.selectedObjects.value.length !== 0) {
const ids = state.ui.filters.selectedObjects.value.map((o) => o.id as string)
zoom(ids)
} // else somethingn is weird.
else {
logger.warn(
"Got a double click event but there's no selected object in the state - this should be impossible :)"
)
}
mp.track('Viewer Action', {
type: 'action',
name: 'zoom',
source: 'object-double-click'
})
}
},
{ state }
)
}
export function useViewerSelectionEventHandler() {
useCollectSelection()
useSelectOrZoomOnSelection()
}