import { useEffect, useMemo, useRef } from "react"; import { useGLTF } from "@react-three/drei"; import * as THREE from "three"; import { modelUrl, type Structure } from "../../lib/api.js"; interface Props { modelFile: string; // Estruturas do banco cujo modelFile é este arquivo — pode conter mais de // um sistema (ex: o glb "visceral-organs" tem estruturas de "respiratorio" // e de "visceral" ao mesmo tempo). structuresInFile: Structure[]; visibleSystems: Set; selectedSlug: string | null; clippingPlanes: THREE.Plane[]; } const HIGHLIGHT = new THREE.Color("#22d3ee"); const BLACK = new THREE.Color(0x000000); export function SystemModel({ modelFile, structuresInFile, visibleSystems, selectedSlug, clippingPlanes, }: Props) { const { scene } = useGLTF(modelUrl(modelFile)); const clonedRef = useRef(); // Nome original de cada objeto (fallback), capturado uma única vez no // clone — usado apenas se o objeto não tiver o "slug" gravado em // userData (ver abaixo). const originalNames = useRef>(new WeakMap()); if (!clonedRef.current) { const clone = scene.clone(true); clone.traverse((obj) => { originalNames.current.set(obj, obj.name); const mesh = obj as THREE.Mesh; if (mesh.isMesh && mesh.material) { mesh.material = (mesh.material as THREE.MeshStandardMaterial).clone(); // Malhas finas tipo lâmina (fáscias, aponeuroses) saem da simplificação // do pipeline (weld + meshopt) com normais corrompidas — a frente e o // verso dessas "folhas" quase se tocam, então o weld mistura normais // opostas num vértice só, gerando aquele efeito de iluminação // "manchado"/ruidoso. Recalcular a partir da geometria final (já // soldada) sempre dá um resultado consistente com a malha real. mesh.geometry.computeVertexNormals(); } }); clonedRef.current = clone; } const root = clonedRef.current!; const bySlugInFile = useMemo( () => new Map(structuresInFile.map((s) => [s.slug, s])), [structuresInFile], ); const byMeshNameInFile = useMemo( () => new Map(structuresInFile.map((s) => [s.meshName, s])), [structuresInFile], ); // Sobe pelos pais até achar uma correspondência. Preferimos o "slug" // gravado em userData (extras do glTF) — sobrevive intacto por todo o // pipeline (assimp, simplificação, GLTFLoader). O nome ("name") é usado // só como fallback, pois um nó com mais de um material vira um Group // (com o nome original) + sub-Meshes com nomes gerados pelo loader. function resolveStructure(obj: THREE.Object3D): Structure | undefined { let node: THREE.Object3D | null = obj; while (node) { const slug = node.userData?.slug as string | undefined; if (slug) { const bySlug = bySlugInFile.get(slug); if (bySlug) return bySlug; } // O pipeline marca explicitamente nós que NÃO são um dos 206 ossos // (acidentes ósseos, marcadores de origem/inserção muscular, linhas // de anotação do app original) — parar aqui evita que esse nó // "herde" o slug de um osso-pai ao continuar subindo a árvore. if (node.userData?.excluded) return undefined; const name = originalNames.current.get(node); if (name) { const byName = byMeshNameInFile.get(name); if (byName) return byName; } node = node.parent; } return undefined; } useEffect(() => { root.traverse((obj) => { // O FBX original do Z-Anatomy tem "espinhos" de anotação (linhas // apontando pra um rótulo, usadas no app de origem) que não são // Mesh — são Line/LineSegments/Points. Como eles nunca batem com // nenhuma das estruturas catalogadas, sempre deveriam ficar // ocultos, mas um filtro que só olhava "isMesh" deixava passar // batido (objeto sem dono nunca tinha visible=false setado). const renderable = obj as THREE.Object3D & { isMesh?: boolean; isLine?: boolean; isLineSegments?: boolean; isPoints?: boolean; }; if (!renderable.isMesh && !renderable.isLine && !renderable.isLineSegments && !renderable.isPoints) return; const mesh = obj as THREE.Mesh; const structure = resolveStructure(mesh); if (!structure) { // Malha sem correspondência no manifesto (não catalogada) — oculta. mesh.visible = false; return; } mesh.name = structure.slug; mesh.visible = visibleSystems.has(structure.system.slug); const mat = mesh.material as THREE.MeshStandardMaterial; mat.clippingPlanes = clippingPlanes; mat.clipShadows = true; const isSelected = structure.slug === selectedSlug; mat.emissive = isSelected ? HIGHLIGHT : BLACK; mat.emissiveIntensity = isSelected ? 0.6 : 0; }); // eslint-disable-next-line react-hooks/exhaustive-deps }, [root, bySlugInFile, byMeshNameInFile, visibleSystems, selectedSlug, clippingPlanes]); // 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 ; }