fix exame fisico2

This commit is contained in:
Rodrigo Bertoldo de Sousa Martins committed 2026-09-10 20:25:28 -03:00
1 parent 9a848416ef
commit 6b5adc3403
1 file changed
+95 -64
+95 -64
View File
@@ -2,37 +2,50 @@ import { useEffect, useState } from "react";
import { useThree } from "@react-three/fiber"; import { useThree } from "@react-three/fiber";
import { Html } from "@react-three/drei"; import { Html } from "@react-three/drei";
import * as THREE from "three"; import * as THREE from "three";
import { useAtlasStore } from "../../store/useAtlasStore.js";
import type { ExamPoint, ExamTechnique, ExamAnchor } from "../../data/physicalExam.js"; import type { ExamPoint, ExamTechnique, ExamAnchor } from "../../data/physicalExam.js";
import { TECHNIQUES } from "../../data/physicalExam.js"; import { TECHNIQUES } from "../../data/physicalExam.js";
// Antes os pontos de exame usavam frações do bounding box do CORPO INTEIRO // Os pontos de exame são ancorados no bounding box de OSSOS REAIS do
// (cabeça a pé) — mas isso supõe proporções corporais genéricas que não // esqueleto (esterno, costelas, vértebras, bacia, crânio, rádio, tíbia) em
// batem necessariamente com a escala/proporção real deste modelo específico, // vez de uma fração do corpo inteiro — assim herdam a escala/posição
// e por isso os pontos apareciam longe de onde deveriam. A correção: ancorar // corretas de graça. Só que a versão anterior exigia TODOS os ossos de uma
// cada ponto no bounding box de OSSOS REAIS específicos (esterno, costelas, // região de uma vez (se um só faltasse — nome bruto do Z-Anatomy diferente
// bacia, crânio, rádio, tíbia) — como esses ossos já são renderizados // do esperado, sufixo de lado ausente etc. — a região inteira sumia, e com
// corretamente pelo pipeline, herdamos a escala/posição certa de graça, sem // ela TODOS os pontos que dependiam dela). Esta versão é tolerante a falha:
// precisar adivinhar proporção nenhuma. // cada região usa vários ossos candidatos (só precisa achar ALGUM) e, se
// // mesmo assim nada for encontrado, cai pro bounding box do corpo inteiro
// Isso exige que o sistema esquelético esteja CARREGADO na cena (não // (contentBox, o mesmo do plano de corte) — os pontos ficam menos precisos
// necessariamente visível — ver o prop alwaysLoadSystems em Scene.tsx/ // nesse caso, mas nunca somem.
// PhysicalExamPage.tsx, que força o glb do esqueleto a entrar mesmo com a const RIB_ORDINALS = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12];
// camada desligada). function ribSlugs(): string[] {
const LANDMARK_SLUGS: Record<string, string[]> = { const out: string[] = [];
sternum: ["esqueletico-esterno"], for (const n of RIB_ORDINALS) {
ribsTop: ["esqueletico-1-costela-direito", "esqueletico-1-costela-esquerdo"], out.push(`esqueletico-${n}-costela-direito`, `esqueletico-${n}-costela-esquerdo`);
ribsBottom: ["esqueletico-12-costela-direito", "esqueletico-12-costela-esquerdo"], }
pelvis: ["esqueletico-osso-do-quadril-direito", "esqueletico-osso-do-quadril-esquerdo"], return out;
skull: ["esqueletico-osso-frontal", "esqueletico-mandibula"], }
const REGION_CANDIDATES: Record<string, string[]> = {
thorax: [
"esqueletico-esterno",
"esqueletico-vertebra-t1",
"esqueletico-vertebra-t12",
...ribSlugs(),
],
pelvis: [
"esqueletico-osso-do-quadril-direito",
"esqueletico-osso-do-quadril-esquerdo",
"esqueletico-sacro",
],
skull: ["esqueletico-osso-frontal", "esqueletico-mandibula", "esqueletico-osso-occipital"],
radiusR: ["esqueletico-radio-direito"], radiusR: ["esqueletico-radio-direito"],
radiusL: ["esqueletico-radio-esquerdo"], radiusL: ["esqueletico-radio-esquerdo"],
tibiaR: ["esqueletico-tibia-direito"], tibiaR: ["esqueletico-tibia-direito"],
tibiaL: ["esqueletico-tibia-esquerdo"], tibiaL: ["esqueletico-tibia-esquerdo"],
}; };
type AnchorBoxes = Record<ExamAnchor, THREE.Box3>; function unionFromSlugs(scene: THREE.Object3D, slugs: string[]): THREE.Box3 | null {
function boxFromSlugs(scene: THREE.Object3D, slugs: string[]): THREE.Box3 | null {
const box = new THREE.Box3(); const box = new THREE.Box3();
let found = false; let found = false;
for (const slug of slugs) { for (const slug of slugs) {
@@ -46,25 +59,23 @@ function boxFromSlugs(scene: THREE.Object3D, slugs: string[]): THREE.Box3 | null
return found ? box : null; return found ? box : null;
} }
function computeAnchorBoxes(scene: THREE.Object3D): AnchorBoxes | null { type AnchorBoxes = Partial<Record<ExamAnchor, THREE.Box3>>;
const raw: Record<string, THREE.Box3 | null> = {};
for (const [key, slugs] of Object.entries(LANDMARK_SLUGS)) {
raw[key] = boxFromSlugs(scene, slugs);
}
// Precisamos pelo menos do tórax (esterno+costelas), da bacia e do crânio
// pra derivar todas as regiões — se algum não foi encontrado ainda
// (glb ainda carregando), tenta de novo no próximo frame.
if (!raw.sternum || !raw.ribsTop || !raw.ribsBottom || !raw.pelvis || !raw.skull) return null;
const thorax = raw.sternum.clone().union(raw.ribsTop).union(raw.ribsBottom); function computeAnchorBoxes(scene: THREE.Object3D): AnchorBoxes {
const pelvis = raw.pelvis.clone(); const thorax = unionFromSlugs(scene, REGION_CANDIDATES.thorax);
const skull = raw.skull.clone(); const pelvis = unionFromSlugs(scene, REGION_CANDIDATES.pelvis);
const skull = unionFromSlugs(scene, REGION_CANDIDATES.skull);
const boxes: AnchorBoxes = {};
if (thorax) boxes.thorax = thorax;
if (pelvis) boxes.pelvis = pelvis;
if (skull) boxes.head = skull;
// Abdome = vão entre a base do tórax e o topo da bacia; pescoço = vão // Abdome = vão entre a base do tórax e o topo da bacia; pescoço = vão
// entre a base do crânio e o topo do tórax. Usamos a largura/profundidade // entre a base do crânio e o topo do tórax — só dá pra calcular se as
// do tórax como aproximação (o abdome real é um pouco mais estreito, mas // duas pontas envolvidas foram encontradas.
// é só uma referência de onde clicar, não uma malha visível). if (thorax && pelvis) {
const abdomen = new THREE.Box3( boxes.abdomen = new THREE.Box3(
new THREE.Vector3( new THREE.Vector3(
Math.min(thorax.min.x, pelvis.min.x), Math.min(thorax.min.x, pelvis.min.x),
pelvis.max.y, pelvis.max.y,
@@ -76,20 +87,33 @@ function computeAnchorBoxes(scene: THREE.Object3D): AnchorBoxes | null {
Math.max(thorax.max.z, pelvis.max.z), Math.max(thorax.max.z, pelvis.max.z),
), ),
); );
const neck = new THREE.Box3( }
if (thorax && skull) {
boxes.neck = new THREE.Box3(
new THREE.Vector3(thorax.min.x, thorax.max.y, thorax.min.z), new THREE.Vector3(thorax.min.x, thorax.max.y, thorax.min.z),
new THREE.Vector3(thorax.max.x, skull.min.y, thorax.max.z), new THREE.Vector3(thorax.max.x, skull.min.y, thorax.max.z),
); );
}
const boxes: Partial<AnchorBoxes> = { thorax, abdomen, pelvis, head: skull, neck }; for (const key of ["radiusR", "radiusL", "tibiaR", "tibiaL"] as const) {
if (raw.radiusR) boxes.radiusR = raw.radiusR; const found = unionFromSlugs(scene, REGION_CANDIDATES[key]);
if (raw.radiusL) boxes.radiusL = raw.radiusL; if (found) boxes[key] = found;
if (raw.tibiaR) boxes.tibiaR = raw.tibiaR; }
if (raw.tibiaL) boxes.tibiaL = raw.tibiaL;
return boxes as AnchorBoxes; return boxes;
} }
// Ordem de fallback quando o anchor "ideal" de um ponto não foi encontrado
// — tenta uma região vizinha razoável antes de recorrer ao corpo inteiro.
const FALLBACK_CHAIN: Partial<Record<ExamAnchor, ExamAnchor[]>> = {
neck: ["thorax", "head"],
abdomen: ["thorax", "pelvis"],
radiusR: ["thorax"],
radiusL: ["thorax"],
tibiaR: ["pelvis"],
tibiaL: ["pelvis"],
};
export function ExamMarkers({ export function ExamMarkers({
points, points,
selectedId, selectedId,
@@ -100,7 +124,8 @@ export function ExamMarkers({
onSelect: (id: string) => void; onSelect: (id: string) => void;
}) { }) {
const { scene } = useThree(); const { scene } = useThree();
const [anchors, setAnchors] = useState<AnchorBoxes | null>(null); const contentBox = useAtlasStore((s) => s.contentBox);
const [anchors, setAnchors] = useState<AnchorBoxes>({});
useEffect(() => { useEffect(() => {
let cancelled = false; let cancelled = false;
@@ -109,18 +134,21 @@ export function ExamMarkers({
function tryCompute() { function tryCompute() {
if (cancelled) return; if (cancelled) return;
const boxes = computeAnchorBoxes(scene); const boxes = computeAnchorBoxes(scene);
if (boxes) {
setAnchors(boxes); setAnchors(boxes);
return; // "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; attempts += 1;
if (attempts >= 50) { if (!boxes.thorax && attempts < 50) {
console.warn(
"ExamMarkers: não encontrou os ossos de referência (esterno/costelas/bacia/crânio) a tempo — confira se o sistema esquelético foi carregado.",
);
return;
}
timer = setTimeout(tryCompute, 200); timer = setTimeout(tryCompute, 200);
} 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:",
Object.keys(boxes),
);
} else {
console.log("ExamMarkers: anchors encontrados:", Object.keys(boxes));
}
} }
tryCompute(); tryCompute();
return () => { return () => {
@@ -129,13 +157,23 @@ export function ExamMarkers({
}; };
}, [scene]); }, [scene]);
if (!anchors) return null;
const techniqueColor = (t: ExamTechnique) => TECHNIQUES.find((x) => x.key === t)?.color ?? "#fff"; const techniqueColor = (t: ExamTechnique) => TECHNIQUES.find((x) => x.key === t)?.color ?? "#fff";
const bodyBox = contentBox
? new THREE.Box3(new THREE.Vector3(...contentBox.min), new THREE.Vector3(...contentBox.max))
: null;
function resolveBox(anchor: ExamAnchor): THREE.Box3 | null {
if (anchors[anchor]) return anchors[anchor]!;
for (const fallback of FALLBACK_CHAIN[anchor] ?? []) {
if (anchors[fallback]) return anchors[fallback]!;
}
return bodyBox;
}
return ( return (
<> <>
{points.map((p) => { {points.map((p) => {
const box = anchors[p.anchor] ?? anchors[fallbackAnchor(p.anchor)]; const box = resolveBox(p.anchor);
if (!box) return null; if (!box) return null;
const [fx, fy, fz] = p.fraction; const [fx, fy, fz] = p.fraction;
const position: [number, number, number] = [ const position: [number, number, number] = [
@@ -163,10 +201,3 @@ export function ExamMarkers({
</> </>
); );
} }
// radiusR/radiusL/tibiaR/tibiaL às vezes não existem (bone não encontrado);
// nesse caso cai pro tórax pra não sumir o ponto de vez.
function fallbackAnchor(anchor: ExamAnchor): ExamAnchor {
if (anchor.startsWith("radius") || anchor.startsWith("tibia")) return "thorax";
return anchor;
}