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
+108 -77
+108 -77
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,50 +59,61 @@ 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,
Math.min(thorax.min.z, pelvis.min.z), Math.min(thorax.min.z, pelvis.min.z),
), ),
new THREE.Vector3( new THREE.Vector3(
Math.max(thorax.max.x, pelvis.max.x), Math.max(thorax.max.x, pelvis.max.x),
thorax.min.y, thorax.min.y,
Math.max(thorax.max.z, pelvis.max.z), Math.max(thorax.max.z, pelvis.max.z),
), ),
); );
const neck = new THREE.Box3( }
new THREE.Vector3(thorax.min.x, thorax.max.y, thorax.min.z), if (thorax && skull) {
new THREE.Vector3(thorax.max.x, skull.min.y, thorax.max.z), boxes.neck = new THREE.Box3(
); new THREE.Vector3(thorax.min.x, thorax.max.y, thorax.min.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); // "thorax" é o anchor mais usado (a maioria dos pontos cardio/resp
return; // 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) {
timer = setTimeout(tryCompute, 200);
} else if (!boxes.thorax) {
console.warn( 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.", "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),
); );
return; } else {
console.log("ExamMarkers: anchors encontrados:", Object.keys(boxes));
} }
timer = setTimeout(tryCompute, 200);
} }
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;
}