From 4542e8fda00055348956c554d2796246b3b5f90e Mon Sep 17 00:00:00 2001 From: Rodrigo Bertoldo de Sousa Martins Date: Fri, 11 Sep 2026 11:30:46 -0300 Subject: [PATCH] fix clique --- frontend/src/components/viewer/Scene.tsx | 63 +++++++++++++++++-- .../src/components/viewer/SystemModel.tsx | 15 ++--- 2 files changed, 64 insertions(+), 14 deletions(-) diff --git a/frontend/src/components/viewer/Scene.tsx b/frontend/src/components/viewer/Scene.tsx index 056e6fc..d184048 100644 --- a/frontend/src/components/viewer/Scene.tsx +++ b/frontend/src/components/viewer/Scene.tsx @@ -218,6 +218,58 @@ function CameraController({ modelFiles }: { modelFiles: string[] }) { ); } +// Ossos finos (costela vista de perfil, falange, etc.) são fáceis de +// "errar" clicando — o raio único do clique padrão do three.js precisa +// acertar exatamente um triângulo da malha. Em vez de um único raio no +// pixel exato do clique, lançamos vários raios num pequeno raio ao redor +// dele (como um "dedo gordo") e ficamos com o resultado válido mais +// próximo da câmera — bem mais tolerante sem precisar mexer na geometria. +const CLICK_SAMPLE_OFFSETS: [number, number][] = [ + [0, 0], + [5, 0], [-5, 0], [0, 5], [0, -5], + [4, 4], [-4, 4], [4, -4], [-4, -4], +]; + +function ClickSelector({ + bySlug, + onSelect, +}: { + bySlug: Map; + onSelect: (slug: string) => void; +}) { + const { camera, scene, gl, raycaster } = useThree(); + + useEffect(() => { + const canvas = gl.domElement; + const ndc = new THREE.Vector2(); + + function handleClick(event: PointerEvent) { + const rect = canvas.getBoundingClientRect(); + let best: { distance: number; slug: string } | null = null; + + for (const [dx, dy] of CLICK_SAMPLE_OFFSETS) { + const x = ((event.clientX + dx - rect.left) / rect.width) * 2 - 1; + const y = -((event.clientY + dy - rect.top) / rect.height) * 2 + 1; + if (x < -1 || x > 1 || y < -1 || y > 1) continue; + ndc.set(x, y); + raycaster.setFromCamera(ndc, camera); + const hits = raycaster.intersectObjects(scene.children, true); + const hit = hits.find((h) => h.object.visible && h.object.name && bySlug.has(h.object.name)); + if (hit && (!best || hit.distance < best.distance)) { + best = { distance: hit.distance, slug: hit.object.name }; + } + } + + if (best) onSelect(best.slug); + } + + canvas.addEventListener("click", handleClick); + return () => canvas.removeEventListener("click", handleClick); + }, [camera, scene, gl, raycaster, bySlug, onSelect]); + + return null; +} + export function Scene({ bySlug, children, @@ -296,13 +348,16 @@ export function Scene({ visibleSystems={visibleSystems} selectedSlug={selectedSlug} clippingPlanes={clippingPlanes} - onSelect={(slug) => { - const structure = bySlug.get(slug); - if (structure) selectStructure(structure.id); - }} /> ))} + { + const structure = bySlug.get(slug); + if (structure) selectStructure(structure.id); + }} + /> ; selectedSlug: string | null; - onSelect: (slug: string) => void; clippingPlanes: THREE.Plane[]; } @@ -24,7 +22,6 @@ export function SystemModel({ structuresInFile, visibleSystems, selectedSlug, - onSelect, clippingPlanes, }: Props) { const { scene } = useGLTF(modelUrl(modelFile)); @@ -127,11 +124,9 @@ export function SystemModel({ // eslint-disable-next-line react-hooks/exhaustive-deps }, [root, bySlugInFile, byMeshNameInFile, visibleSystems, selectedSlug, clippingPlanes]); - const handleClick = (e: ThreeEvent) => { - e.stopPropagation(); - const name = e.object.name; - if (name) onSelect(name); - }; - - return ; + // Seleção por clique agora é feita centralizadamente em Scene.tsx + // (ClickSelector) — um "clique gordo" com vários raios em vez de só o + // pixel exato, bem mais tolerante com ossos finos (costela de perfil, + // falange etc.) do que o onClick padrão por malha. + return ; }