diff --git a/src/tools/Selection.vue b/src/tools/Selection.vue index bef5657..717cdda 100644 --- a/src/tools/Selection.vue +++ b/src/tools/Selection.vue @@ -34,19 +34,21 @@ let selectionMoveListener = (event: MouseEvent) => { let selectionListener = (event: MouseEvent) => { if (!selectionEnabled.value) { - mouseDownAt = undefined; return; } + + // If the mouse moved while clicked (dragging), avoid selection logic if (mouseDownAt) { let [x, y] = mouseDownAt; + mouseDownAt = undefined; if (Math.abs(event.clientX - x) > 5 || Math.abs(event.clientY - y) > 5) { - mouseDownAt = undefined; return; } - mouseDownAt = undefined; } - let scene: ModelScene = props.viewer?.scene; + + // Define the 3D ray from the camera to the mouse // NOTE: Need to access internal as the API has issues with small faces surrounded by edges + let scene: ModelScene = props.viewer?.scene; const ndcCoords = scene.getNDC(event.clientX, event.clientY); //const hit = scene.hitFromPoint(ndcCoords) as Intersection | undefined; raycaster.setFromCamera(ndcCoords, (scene as any).camera); @@ -56,7 +58,9 @@ let selectionListener = (event: MouseEvent) => { raycaster.ray.direction.copy( scene.getTarget().clone().add(scene.target.position).sub((scene as any).camera.position).normalize()); } - // console.log('NDC', ndcCoords, 'Camera', (scene as any).camera, 'Ray', ray_caster.ray); + console.log('Ray', raycaster.ray); + + // Find all hit objects and select the wanted one based on the filter const hits = raycaster.intersectObject(scene, true); let hit = hits.find((hit) => { const kind = hit.object.type @@ -66,11 +70,11 @@ let selectionListener = (event: MouseEvent) => { (kind === 'Points' && selectFilter.value === 'Vertices'); return hit.object.visible && !hit.object.userData.noHit && kindOk; }) as Intersection | undefined; - console.log('Hit', hit) + //console.log('Hit', hit) + if (!highlightNextSelection.value[0]) { - if (!hit) { - deselectAll(); - } else { + // If we are selecting, toggle the selection or deselect all if no hit + if (hit) { // Toggle selection const wasSelected = selected.value.find((m) => m.object.name === hit.object.name) !== undefined; if (wasSelected) { @@ -78,6 +82,8 @@ let selectionListener = (event: MouseEvent) => { } else { select(hit) } + } else { + deselectAll(); } } else { // Otherwise, highlight the model that owns the hit diff --git a/src/viewer/ModelViewerWrapper.vue b/src/viewer/ModelViewerWrapper.vue index 1338410..40f0829 100644 --- a/src/viewer/ModelViewerWrapper.vue +++ b/src/viewer/ModelViewerWrapper.vue @@ -9,15 +9,15 @@ import Loading from "../misc/Loading.vue"; import {ref} from "vue"; import {ModelViewerElement} from '@google/model-viewer'; import type {ModelScene} from "@google/model-viewer/lib/three-components/ModelScene"; +import {Hotspot} from "@google/model-viewer/lib/three-components/Hotspot"; import type {Renderer} from "@google/model-viewer/lib/three-components/Renderer"; +import {Vector3} from "three"; ModelViewerElement.modelCacheSize = 0; // Also needed to avoid tree shaking const emit = defineEmits<{ load: [] }>() -const props = defineProps({ - src: String -}); +const props = defineProps<{ src: string }>(); const elem = ref(null); const scene = ref(null); @@ -25,7 +25,7 @@ const renderer = ref(null); defineExpose({elem, scene, renderer}); onMounted(() => { - elem.value.addEventListener('load', () => { + elem.value.addEventListener('load', async () => { if (elem.value) { // Delete the initial load banner let banner = elem.value.querySelector('.initial-load-banner'); @@ -35,13 +35,102 @@ onMounted(() => { renderer.value = elem.value[$renderer] as Renderer; // Emit the load event emit('load') + + // Test adding a fake 3D line + let lineHandle = await addLine3D(new Vector3(0, 0, 0), new Vector3(0, 100, 0), "Hello!", { + "stroke": "green", + "stroke-width": "2", + }); + setTimeout(() => removeLine3D(lineHandle), 10000); } }); + elem.value.addEventListener('camera-change', onCameraChange); }); + +class Line3DData { + startHotspot: HTMLElement; + endHotspot: HTMLElement; + start2D: [number, number]; + end2D: [number, number]; + lineAttrs: { [key: string]: string }; + centerText?: string; + centerTextSize?: [number, number] +} + +let nextLineId = 0; +let lines = ref<{ [id: number]: Line3DData }>({}); + +function positionToHotspot(position: Vector3): string { + return position.x + ' ' + position.y + ' ' + position.z; +} + +async function addLine3D(p1: Vector3, p2: Vector3, centerText: string = "", lineAttrs: { + [key: string]: string +} = {}): Promise { + if (!scene.value) return -1; + let id = nextLineId++; + let hotspotName1 = 'line' + id + '_start'; + let hotspotName2 = 'line' + id + '_end'; + scene.value.addHotspot(new Hotspot({name: hotspotName1, position: positionToHotspot(p1)})); + scene.value.addHotspot(new Hotspot({name: hotspotName2, position: positionToHotspot(p2)})); + lines.value[id] = { + startHotspot: elem.value.shadowRoot.querySelector('slot[name="' + hotspotName1 + '"]').parentElement, + endHotspot: elem.value.shadowRoot.querySelector('slot[name="' + hotspotName2 + '"]').parentElement, + start2D: [0, 0], + end2D: [20, 20], + centerText: centerText, + centerTextSize: [0, 0], + lineAttrs: lineAttrs + }; + requestIdleCallback(() => onCameraChangeLine(id)); + return id; +} + +function removeLine3D(id: number) { + if (!scene.value) return; + if (lines.value[id].startHotspot) { + scene.value.removeHotspot(new Hotspot({name: 'line' + id + '_start'})); + lines.value[id].startHotspot.parentElement.remove() + } + if (lines.value[id].endHotspot) { + scene.value.removeHotspot(new Hotspot({name: 'line' + id + '_end'})); + lines.value[id].endHotspot.parentElement.remove() + } + delete lines.value[id]; +} + +function onCameraChange() { + // Need to update the SVG overlay + for (let lineId in lines.value) { + onCameraChangeLine(lineId as any); + } +} + +let svg = ref(null); + +function onCameraChangeLine(lineId: number) { + // Update start and end 2D positions + let {x: xB, y: yB} = elem.value.getBoundingClientRect(); + let {x, y} = lines.value[lineId].startHotspot.getBoundingClientRect(); + lines.value[lineId].start2D = [x - xB, y - yB]; + let {x: x2, y: y2} = lines.value[lineId].endHotspot.getBoundingClientRect(); + lines.value[lineId].end2D = [x2 - xB, y2 - yB]; + + // Update the center text size if needed + if (lines.value[lineId].centerText && lines.value[lineId].centerTextSize[0] == 0) { + let text = svg.value.getElementsByClassName('line' + lineId + '_text')[0] as SVGTextElement | undefined; + if (text) { + let bbox = text.getBBox(); + lines.value[lineId].centerTextSize = [bbox.width, bbox.height]; + } + } +} +