import { core } from "../core.js"; import THREE from "../init.js"; function getNormalizedPointerPosition(Viewer, clientX, clientY, targetVector) { targetVector.x = ((clientX - Viewer.mainCanvas.getBoundingClientRect().left) / core.renderer.domElement.clientWidth) * 2 - 1; targetVector.y = -( (clientY - Viewer.mainCanvas.getBoundingClientRect().top) / core.renderer.domElement.clientHeight ) * 2 + 1; } function getPrimaryModelIntersection(Viewer, pointerVector) { Viewer.raycaster.setFromCamera(pointerVector, core.camera); let intersects = []; if (core.mainObject.length > 1) { for (let ii = 0; ii < core.mainObject.length; ii++) { intersects.push( ...Viewer.raycaster.intersectObjects( core.mainObject[ii].children, true ) ); } if (intersects.length <= 0) { intersects = Viewer.raycaster.intersectObjects(core.mainObject, true); } } else if (core.mainObject[0]) { intersects = Viewer.raycaster.intersectObject(core.mainObject[0], true); } return Viewer.getPrimaryIntersection(intersects); } function getPoiHit(Viewer, pointerVector) { Viewer.raycaster.setFromCamera(pointerVector, core.camera); const poiIntersects = Viewer.raycaster.intersectObjects( Viewer.annotationPOIMarkers || [], true ); return poiIntersects.find( (entry) => entry?.object?.userData?.isAnnotationPOI === true ) || null; } export function attachPicking(Viewer) { Object.assign(Viewer, { createTriangleGeometry(intersection) { const position = intersection?.object?.geometry?.attributes?.position; const face = intersection?.face; if (!position || !face) return null; const trianglePositions = new Float32Array([ position.getX(face.a), position.getY(face.a), position.getZ(face.a), position.getX(face.b), position.getY(face.b), position.getZ(face.b), position.getX(face.c), position.getY(face.c), position.getZ(face.c), ]); const triangleGeometry = new THREE.BufferGeometry(); triangleGeometry.setAttribute("position", new THREE.BufferAttribute(trianglePositions, 3)); triangleGeometry.computeVertexNormals(); return triangleGeometry; }, createPickingFaceOverlay(intersection, options = {}) { const triangleGeometry = Viewer.createTriangleGeometry(intersection); if (!triangleGeometry) return null; const fillColor = options.fillColor ?? 0xff0000; const lineColor = options.lineColor ?? 0xffffff; const opacity = options.opacity ?? 0.65; const overlayMaterial = new THREE.MeshBasicMaterial({ color: fillColor, side: THREE.DoubleSide, transparent: true, opacity, depthTest: true, depthWrite: false, polygonOffset: true, polygonOffsetFactor: -1, polygonOffsetUnits: -2, toneMapped: false, }); const fillMesh = new THREE.Mesh(triangleGeometry, overlayMaterial); fillMesh.renderOrder = 999; const lineGeometry = new THREE.EdgesGeometry(triangleGeometry); const lineMaterial = new THREE.LineBasicMaterial({ color: lineColor, transparent: true, opacity: Math.min(opacity + 0.2, 1), depthTest: false, depthWrite: false, toneMapped: false, }); const lineSegments = new THREE.LineSegments(lineGeometry, lineMaterial); lineSegments.renderOrder = 1000; const overlayGroup = new THREE.Group(); overlayGroup.name = "picking-face-overlay"; overlayGroup.userData.isPickingOverlay = true; fillMesh.userData.isPickingOverlay = true; lineSegments.userData.isPickingOverlay = true; overlayGroup.add(fillMesh); overlayGroup.add(lineSegments); return overlayGroup; }, isPickingOverlayObject(object) { let current = object; while (current) { if (current.userData?.isPickingOverlay === true || current.name === "picking-face-overlay") { return true; } current = current.parent; } return false; }, getPrimaryIntersection(intersections) { if (!Array.isArray(intersections) || intersections.length === 0) return null; return intersections.find((entry) => !Viewer.isPickingOverlayObject(entry?.object)) ?? null; }, getFaceSelectionKey(targetId, faceIndex) { if (!targetId || faceIndex === null || faceIndex === undefined) return ""; return `${targetId}:${faceIndex}`; }, findSelectedFaceIndex(targetId, faceIndex) { const key = Viewer.getFaceSelectionKey(targetId, faceIndex); return Viewer.selectedFaces.findIndex((entry) => entry.key === key); }, updateSelectedFacesCount() { Viewer.pickingStats["Selected faces"] = Array.isArray(Viewer.selectedFaces) ? Viewer.selectedFaces.length : 0; const selectedFacesCount = Array.isArray(Viewer.selectedFaces) ? Viewer.selectedFaces.length : 0; if (selectedFacesCount < 1 && Viewer.annotationDialog && Viewer.annotationDialog.hidden === false) { Viewer.closeAnnotationDialog(); } Viewer.updateAddAnnotationControllerState(); Viewer.updatePickingHintVisibility(); }, toStableIdToken(value) { const normalized = String(value || "") .trim() .replace(/[^a-zA-Z0-9._-]+/g, "-") .replace(/-+/g, "-") .replace(/^-|-$/g, ""); return normalized || "target"; }, getSelectionRootObject(object) { let current = object; while (current?.parent && current.parent !== core.scene) { current = current.parent; } return current || object; }, getSelectionRootSlot(rootObject) { if (!rootObject || !Array.isArray(core.mainObject)) return -1; return core.mainObject.findIndex((entry) => { if (entry === rootObject) return true; return Array.isArray(entry) && entry.includes(rootObject); }); }, getObjectHierarchyPath(object, rootObject) { if (!object || !rootObject) return ""; const path = []; let current = object; while (current && current !== rootObject) { const parent = current.parent; if (!parent) break; const index = parent.children.indexOf(current); path.push(index >= 0 ? String(index) : "x"); current = parent; } return path.reverse().join(".") || "root"; }, resolveFaceTargetId(object) { if (!object) return ""; const explicitId = object.userData?.annotationTargetId || object.userData?.id; if (explicitId) return String(explicitId); const rootObject = Viewer.getSelectionRootObject(object); const rootSlot = Viewer.getSelectionRootSlot(rootObject); const path = Viewer.getObjectHierarchyPath(object, rootObject); let rootTag = rootSlot >= 0 ? `m${rootSlot}` : "m0"; if (rootSlot >= 0 && Array.isArray(core.mainObject?.[rootSlot])) { const rootIndex = core.mainObject[rootSlot].indexOf(rootObject); if (rootIndex >= 0) { rootTag = `${rootTag}.${rootIndex}`; } } const targetId = `${rootTag}:${path}`; object.userData ??= {}; object.userData.annotationTargetId = targetId; return targetId; }, resolveObjectByTargetId(targetId) { const raw = String(targetId || "").trim(); const match = raw.match(/^m(\d+)(?:\.(\d+))?:(.+)$/); if (!match) return null; const slot = Number.parseInt(match[1], 10); const rootIndex = Number.parseInt(match[2] || "0", 10); const path = match[3] || "root"; if (!Number.isInteger(slot) || slot < 0) return null; const entry = core.mainObject?.[slot]; if (!entry) return null; let rootObject = Array.isArray(entry) ? entry[rootIndex] : entry; if (!rootObject) return null; if (path === "root") return rootObject; const segments = path.split(".").filter(Boolean); for (const segment of segments) { const childIndex = Number.parseInt(segment, 10); if (!Number.isInteger(childIndex) || childIndex < 0) return null; rootObject = rootObject.children?.[childIndex]; if (!rootObject) return null; } return rootObject; }, getFaceCentroidWorld(object, faceIndex) { const geometry = object?.geometry; if (!geometry || !geometry.getAttribute) return null; const position = geometry.getAttribute("position"); if (!position) return null; const face = Number(faceIndex); if (!Number.isInteger(face) || face < 0) return null; let ia = face * 3; let ib = ia + 1; let ic = ia + 2; const index = geometry.getIndex?.() || geometry.index || null; if (index?.array) { const arr = index.array; if (ic >= arr.length) return null; ia = arr[ia]; ib = arr[ib]; ic = arr[ic]; } else if (ic >= position.count) { return null; } const va = new THREE.Vector3().fromBufferAttribute(position, ia); const vb = new THREE.Vector3().fromBufferAttribute(position, ib); const vc = new THREE.Vector3().fromBufferAttribute(position, ic); const center = va.add(vb).add(vc).multiplyScalar(1 / 3); object.updateMatrixWorld?.(true); center.applyMatrix4(object.matrixWorld); return center; }, clearSelectedFaces() { if (!Array.isArray(Viewer.selectedFaces) || Viewer.selectedFaces.length === 0) { Viewer.updateSelectedFacesCount(); return; } Viewer.selectedFaces.forEach((entry) => { Viewer.disposeFaceOverlay(entry); }); Viewer.selectedFaces.length = 0; Viewer.updateSelectedFacesCount(); }, restoreLastPickedFace() { if (!Viewer.lastPickedFace.overlay) { Viewer.lastPickedFace = { id: "", object: "", faceIndex: null, overlay: null }; return; } Viewer.disposeFaceOverlay(Viewer.lastPickedFace); Viewer.lastPickedFace = { id: "", object: "", faceIndex: null, overlay: null }; }, pickFaces(intersection) { const hoveredObjectId = intersection?.object?.id ?? ""; const hoveredFaceIndex = intersection?.faceIndex ?? null; if (!hoveredObjectId) { Viewer.restoreLastPickedFace(); return; } if ( Viewer.lastPickedFace.object === hoveredObjectId && Viewer.lastPickedFace.faceIndex === hoveredFaceIndex ) { return; } Viewer.restoreLastPickedFace(); const overlay = Viewer.createPickingFaceOverlay(intersection, { fillColor: 0xff3b30, lineColor: 0xffffff, opacity: 0.4, }); if (!overlay) return; Viewer.lastPickedFace = { id: hoveredObjectId, object: hoveredObjectId, faceIndex: hoveredFaceIndex, overlay, }; intersection.object.add(overlay); }, toggleSelectedFace(intersection, options = {}) { const targetId = Viewer.resolveFaceTargetId(intersection?.object); const runtimeObjectId = intersection?.object?.id ?? ""; const faceIndex = intersection?.faceIndex ?? null; if (!targetId || faceIndex === null) return; const multiSelect = options.multiSelect === true; const selectedFaceIndex = Viewer.findSelectedFaceIndex(targetId, faceIndex); if (!multiSelect) { const clickedFaceKey = Viewer.getFaceSelectionKey(targetId, faceIndex); const clickedFaceAlreadySelected = selectedFaceIndex >= 0 && Viewer.selectedFaces.length === 1 && Viewer.selectedFaces[0]?.key === clickedFaceKey; Viewer.clearSelectedFaces(); if (clickedFaceAlreadySelected) { return; } } if (selectedFaceIndex >= 0) { const [selectedFace] = Viewer.selectedFaces.splice(selectedFaceIndex, 1); Viewer.disposeFaceOverlay(selectedFace); Viewer.updateSelectedFacesCount(); return; } const overlay = Viewer.createPickingFaceOverlay(intersection, { fillColor: 0x00c853, lineColor: 0xe8ffe8, opacity: 0.5, }); if (!overlay) return; intersection.object.add(overlay); Viewer.selectedFaces.push({ key: Viewer.getFaceSelectionKey(targetId, faceIndex), targetId, object: targetId, runtimeObjectId, faceIndex, overlay, }); Viewer.updateSelectedFacesCount(); }, onPointerDown(e) { Viewer.disableInteractionHint(); e.stopPropagation(); if (e.button === 0) { getNormalizedPointerPosition(Viewer, e.clientX, e.clientY, Viewer.onDownPosition); } }, onPointerUp(e) { if (e.button !== 0) return; getNormalizedPointerPosition(Viewer, e.clientX, e.clientY, Viewer.onUpPosition); if ( Viewer.onUpPosition.x !== Viewer.onDownPosition.x || Viewer.onUpPosition.y !== Viewer.onDownPosition.y ) { return; } if (!Viewer.pickingMode && !Viewer.RULER_MODE) { const poiHit = getPoiHit(Viewer, Viewer.onUpPosition); if (poiHit?.object) { Viewer.openAnnotationDialogFromPOIMarker(poiHit.object); return; } Viewer.closeAnnotationPOITooltip(); } if (Viewer.pickingMode || Viewer.RULER_MODE) { const primaryIntersection = getPrimaryModelIntersection(Viewer, Viewer.onUpPosition); if (!primaryIntersection) return; if (Viewer.RULER_MODE) { Viewer.buildRuler(primaryIntersection); } else if (Viewer.pickingMode) { Viewer.toggleSelectedFace(primaryIntersection, { multiSelect: e.shiftKey, }); } } }, onPointerMove(e) { getNormalizedPointerPosition(Viewer, e.clientX, e.clientY, Viewer.pointer); if (e.buttons !== 0) { Viewer.disableInteractionHint(); Viewer.closeAnnotationPOITooltip(); } if (e.buttons == 1) { if (Viewer.pointer.x !== Viewer.onDownPosition.x && Viewer.pointer.y !== Viewer.onDownPosition.y) { Viewer.cameraLight.position.set( core.camera.position.x, core.camera.position.y, core.camera.position.z ); } return; } if (!Viewer.pickingMode && !Viewer.RULER_MODE) { if (Viewer.annotationDialog && Viewer.annotationDialog.hidden === false) { Viewer.closeAnnotationPOITooltip(); } else { const poiHit = getPoiHit(Viewer, Viewer.pointer); if (poiHit?.object) { Viewer.openAnnotationPOITooltip(poiHit.object); } else { Viewer.closeAnnotationPOITooltip(); } } } if (Viewer.pickingMode) { const primaryIntersection = getPrimaryModelIntersection(Viewer, Viewer.pointer); if (primaryIntersection) { Viewer.pickFaces(primaryIntersection); } else { Viewer.pickFaces(""); } } }, }); }