diff --git a/frontend/src/components/viewer/ExamMarkers.tsx b/frontend/src/components/viewer/ExamMarkers.tsx index be723f7..7a4db76 100644 --- a/frontend/src/components/viewer/ExamMarkers.tsx +++ b/frontend/src/components/viewer/ExamMarkers.tsx @@ -45,14 +45,26 @@ const REGION_CANDIDATES: Record = { tibiaL: ["esqueletico-tibia-esquerdo"], }; -function unionFromSlugs(scene: THREE.Object3D, slugs: string[]): THREE.Box3 | null { +// Cada Box3.setFromObject percorre a geometria toda do osso — cacheamos o +// resultado por slug pra não recalcular a cada tentativa de retry (o +// esqueleto não se move, então o box de um osso já achado nunca muda). +function boxForSlug(scene: THREE.Object3D, slug: string, cache: Map): THREE.Box3 | null { + const cached = cache.get(slug); + if (cached) return cached; + const obj = scene.getObjectByName(slug); + if (!obj) return null; + const box = new THREE.Box3().setFromObject(obj); + if (box.isEmpty()) return null; + cache.set(slug, box); + return box; +} + +function unionFromSlugs(scene: THREE.Object3D, slugs: string[], cache: Map): THREE.Box3 | null { const box = new THREE.Box3(); let found = false; for (const slug of slugs) { - const obj = scene.getObjectByName(slug); - if (!obj) continue; - const objBox = new THREE.Box3().setFromObject(obj); - if (objBox.isEmpty()) continue; + const objBox = boxForSlug(scene, slug, cache); + if (!objBox) continue; box.union(objBox); found = true; } @@ -61,10 +73,10 @@ function unionFromSlugs(scene: THREE.Object3D, slugs: string[]): THREE.Box3 | nu type AnchorBoxes = Partial>; -function computeAnchorBoxes(scene: THREE.Object3D): AnchorBoxes { - const thorax = unionFromSlugs(scene, REGION_CANDIDATES.thorax); - const pelvis = unionFromSlugs(scene, REGION_CANDIDATES.pelvis); - const skull = unionFromSlugs(scene, REGION_CANDIDATES.skull); +function computeAnchorBoxes(scene: THREE.Object3D, cache: Map): AnchorBoxes { + const thorax = unionFromSlugs(scene, REGION_CANDIDATES.thorax, cache); + const pelvis = unionFromSlugs(scene, REGION_CANDIDATES.pelvis, cache); + const skull = unionFromSlugs(scene, REGION_CANDIDATES.skull, cache); const boxes: AnchorBoxes = {}; if (thorax) boxes.thorax = thorax; @@ -96,7 +108,7 @@ function computeAnchorBoxes(scene: THREE.Object3D): AnchorBoxes { } for (const key of ["radiusR", "radiusL", "tibiaR", "tibiaL"] as const) { - const found = unionFromSlugs(scene, REGION_CANDIDATES[key]); + const found = unionFromSlugs(scene, REGION_CANDIDATES[key], cache); if (found) boxes[key] = found; } @@ -131,16 +143,19 @@ export function ExamMarkers({ let cancelled = false; let attempts = 0; let timer: ReturnType; + const cache = new Map(); function tryCompute() { if (cancelled) return; - const boxes = computeAnchorBoxes(scene); + const boxes = computeAnchorBoxes(scene, cache); setAnchors(boxes); - // "thorax" é o anchor mais usado (a maioria dos pontos cardio/resp - // dependem dele) — se ainda não achou, continua tentando (o glb do - // esqueleto pode ainda estar carregando); só desiste depois de 10s. attempts += 1; - if (!boxes.thorax && attempts < 50) { - timer = setTimeout(tryCompute, 200); + // Só continua tentando o que ainda falta (thorax/pelvis/head) — uma + // vez achados, cada Box3 fica em cache e não é recalculado de novo, + // então parar assim que os 3 principais existirem evita trabalho + // repetido (percorrer geometria) rodando junto com o resto da cena. + const gotCore = boxes.thorax && boxes.pelvis && boxes.head; + if (!gotCore && attempts < 30) { + timer = setTimeout(tryCompute, 300); } else if (!boxes.thorax) { console.warn( "ExamMarkers: não encontrou o tórax (esterno/costelas/vértebras) — os pontos vão usar o corpo inteiro como referência aproximada. Anchors encontrados:", diff --git a/frontend/src/components/viewer/Scene.tsx b/frontend/src/components/viewer/Scene.tsx index 8265151..74f5324 100644 --- a/frontend/src/components/viewer/Scene.tsx +++ b/frontend/src/components/viewer/Scene.tsx @@ -104,6 +104,16 @@ function CameraController({ modelFiles }: { modelFiles: string[] }) { const regionFocusRequest = useAtlasStore((s) => s.regionFocusRequest); const setContentBox = useAtlasStore((s) => s.setContentBox); + // focusRequest/regionFocusRequest moram no store GLOBAL (compartilhado + // entre páginas) — se o usuário clicou "Cabeça" no Atlas e depois foi pro + // Exame Físico, um novo montaria aqui e replayaria esse clique + // antigo, zoomando numa região errada sem o usuário ter clicado em nada + // nesta página. Guardamos o nonce já visto NA MONTAGEM (valor inicial do + // useRef) pra só reagir a cliques que aconteçam DEPOIS que esta instância + // da câmera já existe. + const seenFocusNonce = useRef(focusRequest?.nonce); + const seenRegionNonce = useRef(regionFocusRequest?.nonce); + // Malhas reais (Z-Anatomy) não seguem a mesma escala/origem que os // modelos esquemáticos antigos (podem vir em cm, ou centralizadas em // outro ponto) — em vez de assumir uma posição de câmera fixa, sempre @@ -137,6 +147,8 @@ function CameraController({ modelFiles }: { modelFiles: string[] }) { useEffect(() => { if (!focusRequest || !controlsRef.current) return; + if (focusRequest.nonce === seenFocusNonce.current) return; + seenFocusNonce.current = focusRequest.nonce; const obj = scene.getObjectByName(focusRequest.slug); if (!obj) return; const box = computeVisibleBoundingBox(obj); @@ -151,6 +163,8 @@ function CameraController({ modelFiles }: { modelFiles: string[] }) { useEffect(() => { if (!regionFocusRequest || !controlsRef.current) return; + if (regionFocusRequest.nonce === seenRegionNonce.current) return; + seenRegionNonce.current = regionFocusRequest.nonce; const total = computeVisibleBoundingBox(scene); if (total.isEmpty()) return; const box = regionSubBox(total, regionFocusRequest.region);