diff --git a/src/components/three/ModelViewer.tsx b/src/components/three/ModelViewer.tsx index 30429f1..ed7f22a 100644 --- a/src/components/three/ModelViewer.tsx +++ b/src/components/three/ModelViewer.tsx @@ -662,7 +662,131 @@ function MeasureClickHandler() { } -function GridLayer() { +/** Pointer handler for element-selection mode. Clicks an IFC element, sub-mesh + * of a non-IFC model, or any pickable mesh group. Toggles selection in store. */ +function SelectionHandler() { + const { camera, scene, gl } = useThree(); + const selectionMode = useModelStore((s) => s.selectionMode); + const raycaster = useMemo(() => new THREE.Raycaster(), []); + const mouse = useMemo(() => new THREE.Vector2(), []); + + useEffect(() => { + if (!selectionMode) return; + const prevCursor = gl.domElement.style.cursor; + gl.domElement.style.cursor = 'cell'; + const canvas = gl.domElement; + let downX = 0, downY = 0, armed = false; + + const onDown = (e: PointerEvent) => { + if (e.button !== 0) { armed = false; return; } + downX = e.clientX; downY = e.clientY; armed = true; + }; + const onUp = (e: PointerEvent) => { + if (!armed) return; + armed = false; + if (Math.hypot(e.clientX - downX, e.clientY - downY) > 4) return; + + const rect = canvas.getBoundingClientRect(); + mouse.x = ((e.clientX - rect.left) / rect.width) * 2 - 1; + mouse.y = -((e.clientY - rect.top) / rect.height) * 2 + 1; + raycaster.setFromCamera(mouse, camera); + const intersects = raycaster.intersectObjects(scene.children, true); + const hit = intersects.find((i) => { + const o = i.object; + if (!(o instanceof THREE.Mesh)) return false; + if (o.userData.__edgeLine) return false; + if (o.geometry instanceof THREE.SphereGeometry) return false; + if (o.geometry instanceof THREE.RingGeometry) return false; + if (o.geometry instanceof THREE.PlaneGeometry) return false; + return true; + }); + if (!hit) return; + + // Walk up to find ifcElement node AND the GLBModel root (carrying modelId) + let cur: THREE.Object3D | null = hit.object; + let element: THREE.Object3D | null = null; + let modelId: string | null = null; + while (cur) { + if (!element && cur.userData?.ifcElement) element = cur; + if (!modelId && cur.userData?.modelId) modelId = cur.userData.modelId as string; + cur = cur.parent; + } + // Non-IFC fallback: pick the mesh itself + if (!element) element = hit.object; + if (!modelId) modelId = useModelStore.getState().activeModelId; + if (!modelId || !element) return; + const key = elementKey(modelId, element); + useModelStore.getState().toggleElementSelection(key); + }; + + canvas.addEventListener('pointerdown', onDown); + canvas.addEventListener('pointerup', onUp); + return () => { + canvas.removeEventListener('pointerdown', onDown); + canvas.removeEventListener('pointerup', onUp); + gl.domElement.style.cursor = prevCursor; + }; + }, [selectionMode, camera, scene, gl, raycaster, mouse]); + + return null; +} + +/** Walks the scene each time visibility/selection state changes and applies + * per-element visibility + emissive highlight for selected elements. */ +function VisibilityApplier() { + const { scene } = useThree(); + const nonce = useModelStore((s) => s.visibilityNonce); + const models = useModelStore((s) => s.models); + const opacity = useModelStore((s) => s.opacity); + const renderMode = useModelStore((s) => s.renderMode); + const checklist = useModelStore((s) => s.checklist); + + useEffect(() => { + const st = useModelStore.getState(); + const { hiddenElementKeys, isolatedElementKeys, selectedElementKeys } = st; + + scene.traverse((root) => { + const modelId = root.userData?.modelId as string | undefined; + if (!modelId) return; + // For each ifcElement descendant inside this model root + root.traverse((el) => { + if (!el.userData?.ifcElement) return; + const key = elementKey(modelId, el); + const hidden = hiddenElementKeys.has(key); + const isolated = isolatedElementKeys ? !isolatedElementKeys.has(key) : false; + el.visible = !(hidden || isolated); + + const selected = selectedElementKeys.has(key); + el.traverse((m) => { + if (!(m instanceof THREE.Mesh)) return; + const mats = Array.isArray(m.material) ? m.material : [m.material]; + mats.forEach((mat) => { + if (!(mat instanceof THREE.MeshStandardMaterial)) return; + if (selected) { + if (m.userData.__origEmissive === undefined) { + m.userData.__origEmissive = mat.emissive.getHex(); + m.userData.__origEmissiveIntensity = mat.emissiveIntensity; + } + mat.emissive.set('#f59e0b'); + mat.emissiveIntensity = 0.6; + } else if (m.userData.__origEmissive !== undefined) { + mat.emissive.setHex(m.userData.__origEmissive as number); + mat.emissiveIntensity = (m.userData.__origEmissiveIntensity as number) ?? 0; + delete m.userData.__origEmissive; + delete m.userData.__origEmissiveIntensity; + } + mat.needsUpdate = true; + }); + }); + }); + }); + }, [scene, nonce, models, opacity, renderMode, checklist]); + + return null; +} + + + const showGrid = useModelStore((s) => s.showGrid); const gridY = useModelStore((s) => s.gridY); if (!showGrid) return null;