fix exame fisico2
This commit is contained in:
1 parent
9a848416ef
commit
6b5adc3403
1 file changed
+108
-77
@@ -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;
|
|
||||||
}
|
|
||||||
Reference in new issue
Block a user