From d1d7beddfd8748af95d71a86c2f049e320f5e0b5 Mon Sep 17 00:00:00 2001 From: andrewwallacespeckle Date: Wed, 10 Sep 2025 15:04:48 +0100 Subject: [PATCH] highlighting --- .../viewer/composables/setup/highlighting.ts | 90 +++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 packages/frontend-2/lib/viewer/composables/setup/highlighting.ts diff --git a/packages/frontend-2/lib/viewer/composables/setup/highlighting.ts b/packages/frontend-2/lib/viewer/composables/setup/highlighting.ts new file mode 100644 index 000000000..55dfd97b1 --- /dev/null +++ b/packages/frontend-2/lib/viewer/composables/setup/highlighting.ts @@ -0,0 +1,90 @@ +import { MathUtils } from 'three' +import { isEqual } from 'lodash-es' +import { + type CameraController, + SelectionExtension, + type SelectionExtensionOptions, + type IViewer, + StencilOutlineType +} from '@speckle/viewer' +import { useInjectedViewerState } from '~/lib/viewer/composables/setup' +import { useOnViewerLoadComplete } from '~/lib/viewer/composables/viewer' + +/** + * Highlighting extension that replicates LegacyViewer's HighlightExtension + * Uses SelectionExtension but disables default events for UI-only highlighting + */ +class HighlightExtension extends SelectionExtension { + public constructor(viewer: IViewer, cameraProvider: CameraController) { + super(viewer, cameraProvider) + + // Configure highlighting material to match LegacyViewer's HighlightExtension + const highlightMaterialData: SelectionExtensionOptions = { + selectionMaterialData: { + id: MathUtils.generateUUID(), + color: 0x04cbfb, + emissive: 0x0, + opacity: 1, + roughness: 1, + metalness: 0, + vertexColors: false, + lineWeight: 1, + stencilOutlines: StencilOutlineType.OVERLAY, + pointSize: 4 + } + } + this.options = highlightMaterialData + } +} + +/** + * Post-setup integration that sets up highlighting extension and watches state + * This should only be called once during post-setup after the viewer is initialized. + */ +export const useHighlightingPostSetup = () => { + const { + ui: { highlightedObjectIds }, + viewer: { instance } + } = useInjectedViewerState() + + const highlightExtension = ref(null) + + // Get the highlighting extension instance + const getHighlightExtension = () => { + if (!highlightExtension.value) { + highlightExtension.value = instance.createExtension(HighlightExtension) + } + return highlightExtension.value + } + + // Initialize extension on viewer load + useOnViewerLoadComplete( + ({ isInitial }) => { + if (!isInitial) return + getHighlightExtension() + }, + { initialOnly: true } + ) + + // state -> viewer: Update highlighted objects when state changes + watch( + highlightedObjectIds, + (newIds, oldIds) => { + const extension = getHighlightExtension() + if (!extension) return + + // Clear all current highlights if new list is empty + if (!newIds.length) { + extension.clearSelection() + return + } + + if (oldIds && isEqual(newIds, oldIds)) return + + if (newIds.length > 0) { + extension.selectObjects(newIds) + } + }, + { immediate: true, flush: 'sync' } + ) +}