fix exame fisico
This commit is contained in:
1 parent
7e8963135b
commit
9a848416ef
4 files changed
+246
-66
No files matched your search
@@ -1,16 +1,95 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useThree } from "@react-three/fiber";
|
||||
import { Html } from "@react-three/drei";
|
||||
import * as THREE from "three";
|
||||
import { useAtlasStore } from "../../store/useAtlasStore.js";
|
||||
import type { ExamPoint, ExamTechnique } from "../../data/physicalExam.js";
|
||||
import type { ExamPoint, ExamTechnique, ExamAnchor } from "../../data/physicalExam.js";
|
||||
import { TECHNIQUES } from "../../data/physicalExam.js";
|
||||
|
||||
// Roda DENTRO do mesmo <Canvas> do Scene (recebido via prop "children" —
|
||||
// ver Scene.tsx) pra poder usar <Html> do drei e o bounding box real do
|
||||
// corpo carregado (contentBox no store, o mesmo usado pelo plano de corte e
|
||||
// pela navegação rápida por região). Cada ponto de exame é dado como
|
||||
// FRAÇÃO do corpo (ver data/physicalExam.ts), convertida aqui pra
|
||||
// coordenada absoluta — assim os pontos acompanham o corpo real não
|
||||
// importa a escala/origem carregada.
|
||||
// Antes os pontos de exame usavam frações do bounding box do CORPO INTEIRO
|
||||
// (cabeça a pé) — mas isso supõe proporções corporais genéricas que não
|
||||
// batem necessariamente com a escala/proporção real deste modelo específico,
|
||||
// e por isso os pontos apareciam longe de onde deveriam. A correção: ancorar
|
||||
// cada ponto no bounding box de OSSOS REAIS específicos (esterno, costelas,
|
||||
// bacia, crânio, rádio, tíbia) — como esses ossos já são renderizados
|
||||
// corretamente pelo pipeline, herdamos a escala/posição certa de graça, sem
|
||||
// precisar adivinhar proporção nenhuma.
|
||||
//
|
||||
// Isso exige que o sistema esquelético esteja CARREGADO na cena (não
|
||||
// necessariamente visível — ver o prop alwaysLoadSystems em Scene.tsx/
|
||||
// PhysicalExamPage.tsx, que força o glb do esqueleto a entrar mesmo com a
|
||||
// camada desligada).
|
||||
const LANDMARK_SLUGS: Record<string, string[]> = {
|
||||
sternum: ["esqueletico-esterno"],
|
||||
ribsTop: ["esqueletico-1-costela-direito", "esqueletico-1-costela-esquerdo"],
|
||||
ribsBottom: ["esqueletico-12-costela-direito", "esqueletico-12-costela-esquerdo"],
|
||||
pelvis: ["esqueletico-osso-do-quadril-direito", "esqueletico-osso-do-quadril-esquerdo"],
|
||||
skull: ["esqueletico-osso-frontal", "esqueletico-mandibula"],
|
||||
radiusR: ["esqueletico-radio-direito"],
|
||||
radiusL: ["esqueletico-radio-esquerdo"],
|
||||
tibiaR: ["esqueletico-tibia-direito"],
|
||||
tibiaL: ["esqueletico-tibia-esquerdo"],
|
||||
};
|
||||
|
||||
type AnchorBoxes = Record<ExamAnchor, THREE.Box3>;
|
||||
|
||||
function boxFromSlugs(scene: THREE.Object3D, slugs: string[]): 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;
|
||||
box.union(objBox);
|
||||
found = true;
|
||||
}
|
||||
return found ? box : null;
|
||||
}
|
||||
|
||||
function computeAnchorBoxes(scene: THREE.Object3D): AnchorBoxes | null {
|
||||
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);
|
||||
const pelvis = raw.pelvis.clone();
|
||||
const skull = raw.skull.clone();
|
||||
|
||||
// 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
|
||||
// do tórax como aproximação (o abdome real é um pouco mais estreito, mas
|
||||
// é só uma referência de onde clicar, não uma malha visível).
|
||||
const abdomen = new THREE.Box3(
|
||||
new THREE.Vector3(
|
||||
Math.min(thorax.min.x, pelvis.min.x),
|
||||
pelvis.max.y,
|
||||
Math.min(thorax.min.z, pelvis.min.z),
|
||||
),
|
||||
new THREE.Vector3(
|
||||
Math.max(thorax.max.x, pelvis.max.x),
|
||||
thorax.min.y,
|
||||
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),
|
||||
new THREE.Vector3(thorax.max.x, skull.min.y, thorax.max.z),
|
||||
);
|
||||
|
||||
const boxes: Partial<AnchorBoxes> = { thorax, abdomen, pelvis, head: skull, neck };
|
||||
if (raw.radiusR) boxes.radiusR = raw.radiusR;
|
||||
if (raw.radiusL) boxes.radiusL = raw.radiusL;
|
||||
if (raw.tibiaR) boxes.tibiaR = raw.tibiaR;
|
||||
if (raw.tibiaL) boxes.tibiaL = raw.tibiaL;
|
||||
|
||||
return boxes as AnchorBoxes;
|
||||
}
|
||||
|
||||
export function ExamMarkers({
|
||||
points,
|
||||
selectedId,
|
||||
@@ -20,21 +99,49 @@ export function ExamMarkers({
|
||||
selectedId: string | null;
|
||||
onSelect: (id: string) => void;
|
||||
}) {
|
||||
const contentBox = useAtlasStore((s) => s.contentBox);
|
||||
if (!contentBox) return null;
|
||||
const { scene } = useThree();
|
||||
const [anchors, setAnchors] = useState<AnchorBoxes | null>(null);
|
||||
|
||||
const min = new THREE.Vector3(...contentBox.min);
|
||||
const max = new THREE.Vector3(...contentBox.max);
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
let attempts = 0;
|
||||
let timer: ReturnType<typeof setTimeout>;
|
||||
function tryCompute() {
|
||||
if (cancelled) return;
|
||||
const boxes = computeAnchorBoxes(scene);
|
||||
if (boxes) {
|
||||
setAnchors(boxes);
|
||||
return;
|
||||
}
|
||||
attempts += 1;
|
||||
if (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);
|
||||
}
|
||||
tryCompute();
|
||||
return () => {
|
||||
cancelled = true;
|
||||
clearTimeout(timer);
|
||||
};
|
||||
}, [scene]);
|
||||
|
||||
if (!anchors) return null;
|
||||
const techniqueColor = (t: ExamTechnique) => TECHNIQUES.find((x) => x.key === t)?.color ?? "#fff";
|
||||
|
||||
return (
|
||||
<>
|
||||
{points.map((p) => {
|
||||
const box = anchors[p.anchor] ?? anchors[fallbackAnchor(p.anchor)];
|
||||
if (!box) return null;
|
||||
const [fx, fy, fz] = p.fraction;
|
||||
const position: [number, number, number] = [
|
||||
THREE.MathUtils.lerp(min.x, max.x, fx),
|
||||
THREE.MathUtils.lerp(min.y, max.y, fy),
|
||||
THREE.MathUtils.lerp(min.z, max.z, fz),
|
||||
THREE.MathUtils.lerp(box.min.x, box.max.x, fx),
|
||||
THREE.MathUtils.lerp(box.min.y, box.max.y, fy),
|
||||
THREE.MathUtils.lerp(box.min.z, box.max.z, fz),
|
||||
];
|
||||
const isSelected = p.id === selectedId;
|
||||
const color = techniqueColor(p.technique);
|
||||
@@ -56,3 +163,10 @@ 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