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;
|
||||
}
|
||||
@@ -176,6 +176,7 @@ function CameraController({ modelFiles }: { modelFiles: string[] }) {
|
||||
export function Scene({
|
||||
bySlug,
|
||||
children,
|
||||
alwaysLoadSystems,
|
||||
}: {
|
||||
bySlug: Map<string, Structure>;
|
||||
// Ponto de extensão pra conteúdo 3D extra dentro do MESMO Canvas (e do
|
||||
@@ -183,6 +184,13 @@ export function Scene({
|
||||
// Físico pra desenhar os pontos de exame em cima do corpo real, sem
|
||||
// duplicar toda a lógica de carregamento de malhas/câmera daqui.
|
||||
children?: ReactNode;
|
||||
// Sistemas cujo .glb deve carregar mesmo com a camada desligada na barra
|
||||
// lateral (fica invisível — mesh.visible continua obedecendo
|
||||
// visibleSystems — mas presente na cena). Usado pelo Exame Físico: os
|
||||
// ossos de referência (esterno, bacia, crânio...) usados pra posicionar
|
||||
// os pontos de exame precisam existir na cena mesmo se o usuário não
|
||||
// marcou "Esquelético".
|
||||
alwaysLoadSystems?: string[];
|
||||
}) {
|
||||
const visibleSystems = useAtlasStore((s) => s.visibleSystems);
|
||||
const selectedStructureId = useAtlasStore((s) => s.selectedStructureId);
|
||||
@@ -214,9 +222,13 @@ export function Scene({
|
||||
const modelFiles = useMemo(
|
||||
() =>
|
||||
[...structuresByFile.entries()]
|
||||
.filter(([, items]) => items.some((s) => visibleSystems.has(s.system.slug)))
|
||||
.filter(([, items]) =>
|
||||
items.some(
|
||||
(s) => visibleSystems.has(s.system.slug) || alwaysLoadSystems?.includes(s.system.slug),
|
||||
),
|
||||
)
|
||||
.map(([file]) => file),
|
||||
[structuresByFile, visibleSystems],
|
||||
[structuresByFile, visibleSystems, alwaysLoadSystems],
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,21 +1,34 @@
|
||||
// Pontos do módulo de Exame Físico, posicionados sobre o corpo 3D REAL do
|
||||
// Z-Anatomy (não um boneco geométrico) — por isso a posição de cada ponto
|
||||
// é dada como FRAÇÃO (0 a 1) do bounding box do conteúdo carregado, e não
|
||||
// como coordenada absoluta: o corpo real não tem escala/origem fixa (é
|
||||
// recalculado a cada carga, ver computeVisibleBoundingBox em Scene.tsx), só
|
||||
// a PROPORÇÃO do corpo é estável. As frações de altura (fy) usam as mesmas
|
||||
// faixas por região já validadas em Scene.tsx (BODY_REGION_Y_RANGES):
|
||||
// cabeça [0.85,1], pescoço [0.8,0.87], tórax [0.62,0.83], abdome [0.52,0.65],
|
||||
// pelve [0.42,0.55], membros superiores [0.55,0.88], membros inferiores [0,0.45].
|
||||
// Z-Anatomy. Cada ponto é ancorado num OSSO/REGIÃO REAL do esqueleto (não
|
||||
// numa fração do corpo inteiro, que não bate com a proporção real deste
|
||||
// modelo específico) — ver a derivação dos anchors em ExamMarkers.tsx:
|
||||
// thorax = esterno + 1ª e 12ª costelas (bounding box do tórax todo)
|
||||
// abdomen = vão entre a base do tórax e o topo da bacia
|
||||
// pelvis = osso do quadril (esq.+dir.)
|
||||
// head = osso frontal + mandíbula
|
||||
// neck = vão entre a base do crânio e o topo do tórax
|
||||
// radiusR/radiusL = rádio (antebraço) de cada lado — usado pro pulso radial
|
||||
// tibiaR/tibiaL = tíbia (perna) de cada lado — usado pro pulso pedioso
|
||||
//
|
||||
// fx = 0..1 esquerda↔direita da TELA (não do paciente — cada ponto já diz
|
||||
// o lado anatômico por extenso na descrição); fz = 0..1 trás↔frente
|
||||
// (assumindo o corpo de frente pra +Z, mesma convenção usada em
|
||||
// PlanesPage). Como as posições são só uma estimativa geométrica (não dá
|
||||
// pra inspecionar visualmente as coordenadas reais do modelo a partir daqui),
|
||||
// os números podem precisar de ajuste fino depois de testar no app — se um
|
||||
// ponto aparecer decolado do corpo, é só mudar a fração aqui.
|
||||
// fraction = [fx, fy, fz], cada um de 0 a 1 (ou um pouco fora, se o ponto
|
||||
// fica além do próprio osso-âncora) DENTRO do bounding box daquele anchor:
|
||||
// fy = 0 é a base do anchor, fy = 1 o topo; fx = 0/1 são as bordas laterais
|
||||
// (fx = 0.5 é o meio); fz = 0 é a face posterior, fz = 1 a anterior.
|
||||
//
|
||||
// Como não dá pra abrir o navegador e conferir visualmente as coordenadas
|
||||
// reais a partir daqui, o lado esquerdo/direito (fx) é uma estimativa —
|
||||
// se aparecer trocado no app, é só inverter o fx do ponto (1 - fx).
|
||||
export type ExamTechnique = "inspecao" | "palpacao" | "percussao" | "ausculta";
|
||||
export type ExamAnchor =
|
||||
| "thorax"
|
||||
| "abdomen"
|
||||
| "pelvis"
|
||||
| "head"
|
||||
| "neck"
|
||||
| "radiusR"
|
||||
| "radiusL"
|
||||
| "tibiaR"
|
||||
| "tibiaL";
|
||||
|
||||
export const TECHNIQUES: { key: ExamTechnique; label: string; color: string }[] = [
|
||||
{ key: "inspecao", label: "Inspeção", color: "#f2c14e" },
|
||||
@@ -29,6 +42,7 @@ export interface ExamPoint {
|
||||
name: string;
|
||||
system: string;
|
||||
technique: ExamTechnique;
|
||||
anchor: ExamAnchor;
|
||||
fraction: [number, number, number];
|
||||
howTo: string;
|
||||
whatFor: string;
|
||||
@@ -41,7 +55,8 @@ export const EXAM_POINTS: ExamPoint[] = [
|
||||
name: "Ectoscopia Geral",
|
||||
system: "Geral",
|
||||
technique: "inspecao",
|
||||
fraction: [0.5, 0.93, 0.85],
|
||||
anchor: "head",
|
||||
fraction: [0.5, 0.6, 0.8],
|
||||
howTo: "Observe o paciente por completo antes de tocar: estado geral, nível de consciência, postura, fácies, coloração da pele e mucosas, hidratação.",
|
||||
whatFor: "Primeira etapa de qualquer exame físico — muitas pistas diagnósticas (icterícia, cianose, palidez, dispneia) aparecem só de olhar, antes de qualquer manobra.",
|
||||
},
|
||||
@@ -52,7 +67,8 @@ export const EXAM_POINTS: ExamPoint[] = [
|
||||
name: "Foco Aórtico",
|
||||
system: "Cardiovascular",
|
||||
technique: "ausculta",
|
||||
fraction: [0.44, 0.8, 0.85],
|
||||
anchor: "thorax",
|
||||
fraction: [0.42, 0.88, 0.85],
|
||||
howTo: "2º espaço intercostal, à direita do esterno. Ausculte com o diafragma do estetoscópio, paciente sentado ou deitado.",
|
||||
whatFor: "Melhor foco para a valva aórtica — sopros de estenose/insuficiência aórtica costumam ser mais audíveis aqui.",
|
||||
},
|
||||
@@ -61,7 +77,8 @@ export const EXAM_POINTS: ExamPoint[] = [
|
||||
name: "Foco Pulmonar",
|
||||
system: "Cardiovascular",
|
||||
technique: "ausculta",
|
||||
fraction: [0.56, 0.8, 0.85],
|
||||
anchor: "thorax",
|
||||
fraction: [0.58, 0.88, 0.85],
|
||||
howTo: "2º espaço intercostal, à esquerda do esterno.",
|
||||
whatFor: "Melhor foco para a valva pulmonar — sopros de estenose/insuficiência pulmonar e desdobramento de B2.",
|
||||
},
|
||||
@@ -70,7 +87,8 @@ export const EXAM_POINTS: ExamPoint[] = [
|
||||
name: "Foco Aórtico Acessório (Erb)",
|
||||
system: "Cardiovascular",
|
||||
technique: "ausculta",
|
||||
fraction: [0.56, 0.76, 0.85],
|
||||
anchor: "thorax",
|
||||
fraction: [0.58, 0.78, 0.85],
|
||||
howTo: "3º espaço intercostal, à esquerda do esterno.",
|
||||
whatFor: "Bom ponto para captar sopros diastólicos de insuficiência aórtica, que às vezes irradiam melhor pra cá do que pro foco aórtico clássico.",
|
||||
},
|
||||
@@ -79,7 +97,8 @@ export const EXAM_POINTS: ExamPoint[] = [
|
||||
name: "Foco Tricúspide",
|
||||
system: "Cardiovascular",
|
||||
technique: "ausculta",
|
||||
fraction: [0.55, 0.71, 0.85],
|
||||
anchor: "thorax",
|
||||
fraction: [0.56, 0.68, 0.85],
|
||||
howTo: "4º-5º espaço intercostal, na borda esternal esquerda baixa.",
|
||||
whatFor: "Melhor foco para a valva tricúspide — sopros de insuficiência/estenose tricúspide.",
|
||||
},
|
||||
@@ -88,7 +107,8 @@ export const EXAM_POINTS: ExamPoint[] = [
|
||||
name: "Foco Mitral (Ápice)",
|
||||
system: "Cardiovascular",
|
||||
technique: "ausculta",
|
||||
fraction: [0.62, 0.68, 0.85],
|
||||
anchor: "thorax",
|
||||
fraction: [0.68, 0.58, 0.85],
|
||||
howTo: "5º espaço intercostal esquerdo, na linha hemiclavicular (região do ictus cordis).",
|
||||
whatFor: "Melhor foco para a valva mitral — sopro de estenose mitral (ruflar diastólico) e insuficiência mitral (sopro holossistólico).",
|
||||
},
|
||||
@@ -97,7 +117,8 @@ export const EXAM_POINTS: ExamPoint[] = [
|
||||
name: "Ictus Cordis (Choque de Ponta)",
|
||||
system: "Cardiovascular",
|
||||
technique: "palpacao",
|
||||
fraction: [0.63, 0.68, 0.86],
|
||||
anchor: "thorax",
|
||||
fraction: [0.69, 0.58, 0.87],
|
||||
howTo: "Palpe com a polpa dos dedos (ou a região tenar) o 5º espaço intercostal esquerdo, linha hemiclavicular.",
|
||||
whatFor: "Localiza o ápice do coração; desvio lateral ou impulso muito forte sugere cardiomegalia/hipertrofia ventricular.",
|
||||
},
|
||||
@@ -106,7 +127,8 @@ export const EXAM_POINTS: ExamPoint[] = [
|
||||
name: "Frêmito Precordial",
|
||||
system: "Cardiovascular",
|
||||
technique: "palpacao",
|
||||
fraction: [0.58, 0.74, 0.85],
|
||||
anchor: "thorax",
|
||||
fraction: [0.6, 0.73, 0.85],
|
||||
howTo: "Apoie a palma da mão sobre o precórdio e sinta uma vibração (\"gato ronronando\").",
|
||||
whatFor: "Frêmito = sopro cardíaco intenso o bastante para ser sentido, geralmente indica lesão valvar significativa.",
|
||||
},
|
||||
@@ -117,7 +139,8 @@ export const EXAM_POINTS: ExamPoint[] = [
|
||||
name: "Ápice Pulmonar Direito",
|
||||
system: "Respiratório",
|
||||
technique: "ausculta",
|
||||
fraction: [0.38, 0.815, 0.8],
|
||||
anchor: "thorax",
|
||||
fraction: [0.22, 0.92, 0.75],
|
||||
howTo: "Ausculte logo acima da clavícula direita, pedindo ao paciente para respirar fundo pela boca.",
|
||||
whatFor: "Murmúrio vesicular reduzido/ausente ou ruídos adventícios (estertores, sibilos) localizados aqui sugerem acometimento do ápice pulmonar direito.",
|
||||
},
|
||||
@@ -126,7 +149,8 @@ export const EXAM_POINTS: ExamPoint[] = [
|
||||
name: "Ápice Pulmonar Esquerdo",
|
||||
system: "Respiratório",
|
||||
technique: "ausculta",
|
||||
fraction: [0.62, 0.815, 0.8],
|
||||
anchor: "thorax",
|
||||
fraction: [0.78, 0.92, 0.75],
|
||||
howTo: "Mesma manobra do lado esquerdo, logo acima da clavícula esquerda.",
|
||||
whatFor: "Comparação bilateral é essencial — assimetria entre os dois ápices é o achado mais importante.",
|
||||
},
|
||||
@@ -135,7 +159,8 @@ export const EXAM_POINTS: ExamPoint[] = [
|
||||
name: "Campo Médio Direito",
|
||||
system: "Respiratório",
|
||||
technique: "ausculta",
|
||||
fraction: [0.35, 0.72, 0.78],
|
||||
anchor: "thorax",
|
||||
fraction: [0.18, 0.6, 0.7],
|
||||
howTo: "Ausculte na linha hemiclavicular/axilar direita, altura do 4º espaço intercostal.",
|
||||
whatFor: "Zona onde sopros tubários e estertores de pneumonia em lobo médio costumam aparecer primeiro.",
|
||||
},
|
||||
@@ -144,7 +169,8 @@ export const EXAM_POINTS: ExamPoint[] = [
|
||||
name: "Campo Médio Esquerdo",
|
||||
system: "Respiratório",
|
||||
technique: "ausculta",
|
||||
fraction: [0.65, 0.72, 0.78],
|
||||
anchor: "thorax",
|
||||
fraction: [0.82, 0.6, 0.7],
|
||||
howTo: "Mesma altura, do lado esquerdo.",
|
||||
whatFor: "Comparação bilateral do murmúrio vesicular e de ruídos adventícios.",
|
||||
},
|
||||
@@ -153,7 +179,8 @@ export const EXAM_POINTS: ExamPoint[] = [
|
||||
name: "Base Pulmonar Direita",
|
||||
system: "Respiratório",
|
||||
technique: "ausculta",
|
||||
fraction: [0.33, 0.645, 0.75],
|
||||
anchor: "thorax",
|
||||
fraction: [0.16, 0.2, 0.65],
|
||||
howTo: "Ausculte nas bases (região inferior do tórax, de preferência também pelas costas).",
|
||||
whatFor: "Local clássico de estertores crepitantes (insuficiência cardíaca, pneumonia de base) e de derrame pleural (murmúrio abolido).",
|
||||
},
|
||||
@@ -162,7 +189,8 @@ export const EXAM_POINTS: ExamPoint[] = [
|
||||
name: "Base Pulmonar Esquerda",
|
||||
system: "Respiratório",
|
||||
technique: "ausculta",
|
||||
fraction: [0.67, 0.645, 0.75],
|
||||
anchor: "thorax",
|
||||
fraction: [0.84, 0.2, 0.65],
|
||||
howTo: "Mesma região, do lado esquerdo.",
|
||||
whatFor: "Comparação bilateral das bases — assimetria é o achado mais valioso.",
|
||||
},
|
||||
@@ -171,7 +199,8 @@ export const EXAM_POINTS: ExamPoint[] = [
|
||||
name: "Expansibilidade Torácica",
|
||||
system: "Respiratório",
|
||||
technique: "palpacao",
|
||||
fraction: [0.5, 0.7, 0.15],
|
||||
anchor: "thorax",
|
||||
fraction: [0.5, 0.45, 0.05],
|
||||
howTo: "Coloque as mãos espalmadas nas costas do paciente, polegares próximos à linha média, e peça para respirar fundo — observe se os polegares se afastam simetricamente.",
|
||||
whatFor: "Redução da expansibilidade de um lado sugere derrame pleural, atelectasia ou pneumotórax naquele hemitórax.",
|
||||
},
|
||||
@@ -180,7 +209,8 @@ export const EXAM_POINTS: ExamPoint[] = [
|
||||
name: "Frêmito Toracovocal",
|
||||
system: "Respiratório",
|
||||
technique: "palpacao",
|
||||
fraction: [0.6, 0.73, 0.8],
|
||||
anchor: "thorax",
|
||||
fraction: [0.68, 0.55, 0.8],
|
||||
howTo: "Apoie a borda ulnar das mãos no tórax e peça ao paciente para dizer \"trinta e três\" repetidamente, comparando os dois lados.",
|
||||
whatFor: "Aumentado em condensações (pneumonia); diminuído/abolido em derrame pleural, pneumotórax ou DPOC.",
|
||||
},
|
||||
@@ -189,7 +219,8 @@ export const EXAM_POINTS: ExamPoint[] = [
|
||||
name: "Percussão dos Campos Pulmonares",
|
||||
system: "Respiratório",
|
||||
technique: "percussao",
|
||||
fraction: [0.4, 0.73, 0.8],
|
||||
anchor: "thorax",
|
||||
fraction: [0.25, 0.55, 0.8],
|
||||
howTo: "Percuta comparando os dois hemitóraxes, de cima para baixo, com o dedo plexímetro bem apoiado entre os espaços intercostais.",
|
||||
whatFor: "Som claro pulmonar normal; hipertimpanismo sugere pneumotórax; macicez/submacicez sugere derrame pleural ou consolidação.",
|
||||
},
|
||||
@@ -200,7 +231,8 @@ export const EXAM_POINTS: ExamPoint[] = [
|
||||
name: "Inspeção Abdominal",
|
||||
system: "Abdominal",
|
||||
technique: "inspecao",
|
||||
fraction: [0.5, 0.63, 0.85],
|
||||
anchor: "abdomen",
|
||||
fraction: [0.5, 0.7, 0.85],
|
||||
howTo: "Com o paciente deitado e relaxado, observe forma, simetria, distensão, cicatrizes, circulação colateral e peristaltismo visível.",
|
||||
whatFor: "Abdome distendido, assimétrico ou com peristaltismo visível pode indicar obstrução, massa ou ascite.",
|
||||
},
|
||||
@@ -209,7 +241,8 @@ export const EXAM_POINTS: ExamPoint[] = [
|
||||
name: "Ausculta dos Ruídos Hidroaéreos",
|
||||
system: "Abdominal",
|
||||
technique: "ausculta",
|
||||
fraction: [0.5, 0.58, 0.8],
|
||||
anchor: "abdomen",
|
||||
fraction: [0.5, 0.5, 0.8],
|
||||
howTo: "Ausculte ANTES de palpar ou percutir (essas manobras alteram o peristaltismo) — um ponto já basta, ouça por 1-2 minutos se estiver avaliando ausência de ruídos.",
|
||||
whatFor: "Ruídos aumentados sugerem obstrução inicial; ausência de ruídos (\"silêncio abdominal\") é sinal de íleo paralítico/abdome agudo.",
|
||||
},
|
||||
@@ -218,7 +251,8 @@ export const EXAM_POINTS: ExamPoint[] = [
|
||||
name: "Percussão Abdominal (Timpanismo)",
|
||||
system: "Abdominal",
|
||||
technique: "percussao",
|
||||
fraction: [0.44, 0.56, 0.8],
|
||||
anchor: "abdomen",
|
||||
fraction: [0.4, 0.4, 0.8],
|
||||
howTo: "Percuta os 4 quadrantes — o timpanismo (som mais alto, de tambor) é normal, pela presença de gás nas alças intestinais.",
|
||||
whatFor: "Macicez onde deveria haver timpanismo sugere massa, ascite ou visceromegalia; timpanismo excessivo sugere distensão gasosa.",
|
||||
},
|
||||
@@ -227,7 +261,8 @@ export const EXAM_POINTS: ExamPoint[] = [
|
||||
name: "Delimitação da Loja Hepática",
|
||||
system: "Abdominal",
|
||||
technique: "percussao",
|
||||
fraction: [0.35, 0.63, 0.8],
|
||||
anchor: "abdomen",
|
||||
fraction: [0.2, 0.75, 0.8],
|
||||
howTo: "Percuta de cima para baixo na linha hemiclavicular direita, do som claro pulmonar até encontrar macicez (borda superior do fígado), depois do timpanismo abdominal até a macicez (borda inferior).",
|
||||
whatFor: "Estima o tamanho do fígado; uma loja hepática maior que o esperado sugere hepatomegalia.",
|
||||
},
|
||||
@@ -236,7 +271,8 @@ export const EXAM_POINTS: ExamPoint[] = [
|
||||
name: "Palpação Superficial",
|
||||
system: "Abdominal",
|
||||
technique: "palpacao",
|
||||
fraction: [0.56, 0.6, 0.78],
|
||||
anchor: "abdomen",
|
||||
fraction: [0.6, 0.55, 0.78],
|
||||
howTo: "Com a mão relaxada, palpe levemente todos os quadrantes, avaliando tensão da parede e sensibilidade — sempre deixando a área dolorosa por último.",
|
||||
whatFor: "Detecta defesa muscular voluntária/involuntária e localiza dor antes de aprofundar a palpação.",
|
||||
},
|
||||
@@ -245,7 +281,8 @@ export const EXAM_POINTS: ExamPoint[] = [
|
||||
name: "Palpação Profunda",
|
||||
system: "Abdominal",
|
||||
technique: "palpacao",
|
||||
fraction: [0.56, 0.55, 0.78],
|
||||
anchor: "abdomen",
|
||||
fraction: [0.6, 0.4, 0.78],
|
||||
howTo: "Com as duas mãos sobrepostas, palpe mais profundamente para identificar massas, visceromegalias e pontos dolorosos específicos.",
|
||||
whatFor: "Identifica massas abdominais, contornos de vísceras e sinais de irritação peritoneal (dor à descompressão brusca).",
|
||||
},
|
||||
@@ -254,7 +291,8 @@ export const EXAM_POINTS: ExamPoint[] = [
|
||||
name: "Ponto de McBurney",
|
||||
system: "Abdominal",
|
||||
technique: "palpacao",
|
||||
fraction: [0.32, 0.5, 0.78],
|
||||
anchor: "abdomen",
|
||||
fraction: [0.2, 0.12, 0.78],
|
||||
howTo: "Localize o ponto entre o terço lateral e os dois terços mediais da linha que liga a espinha ilíaca ântero-superior direita ao umbigo; palpe e avalie a descompressão brusca.",
|
||||
whatFor: "Dor à palpação (e principalmente à descompressão brusca) neste ponto é clássica de apendicite aguda.",
|
||||
},
|
||||
@@ -263,7 +301,8 @@ export const EXAM_POINTS: ExamPoint[] = [
|
||||
name: "Sinal de Murphy",
|
||||
system: "Abdominal",
|
||||
technique: "palpacao",
|
||||
fraction: [0.34, 0.64, 0.78],
|
||||
anchor: "abdomen",
|
||||
fraction: [0.22, 0.78, 0.78],
|
||||
howTo: "Palpe sob o rebordo costal direito e peça ao paciente para inspirar fundo.",
|
||||
whatFor: "Sinal positivo = parada súbita da inspiração por dor — sugere colecistite aguda.",
|
||||
},
|
||||
@@ -272,7 +311,8 @@ export const EXAM_POINTS: ExamPoint[] = [
|
||||
name: "Palpação Hepática (Borda do Fígado)",
|
||||
system: "Abdominal",
|
||||
technique: "palpacao",
|
||||
fraction: [0.33, 0.63, 0.8],
|
||||
anchor: "abdomen",
|
||||
fraction: [0.2, 0.7, 0.8],
|
||||
howTo: "Com a mão em garra sob o rebordo costal direito, peça para o paciente inspirar fundo e sinta a borda hepática \"escapar\" sob os dedos.",
|
||||
whatFor: "Avalia tamanho, consistência e borda do fígado — borda romba/endurecida sugere hepatopatia crônica.",
|
||||
},
|
||||
@@ -281,7 +321,8 @@ export const EXAM_POINTS: ExamPoint[] = [
|
||||
name: "Palpação do Baço",
|
||||
system: "Abdominal",
|
||||
technique: "palpacao",
|
||||
fraction: [0.68, 0.64, 0.75],
|
||||
anchor: "abdomen",
|
||||
fraction: [0.82, 0.78, 0.75],
|
||||
howTo: "Com o paciente em decúbito lateral direito, palpe sob o rebordo costal esquerdo durante a inspiração profunda.",
|
||||
whatFor: "O baço normal não é palpável — se for palpável, geralmente indica esplenomegalia.",
|
||||
},
|
||||
@@ -292,7 +333,8 @@ export const EXAM_POINTS: ExamPoint[] = [
|
||||
name: "Pulso Carotídeo",
|
||||
system: "Vascular",
|
||||
technique: "palpacao",
|
||||
fraction: [0.44, 0.87, 0.7],
|
||||
anchor: "neck",
|
||||
fraction: [0.4, 0.5, 0.6],
|
||||
howTo: "Palpe suavemente (nunca os dois lados ao mesmo tempo) na borda anterior do músculo esternocleidomastóideo, no pescoço.",
|
||||
whatFor: "Avalia amplitude e ritmo do pulso central; útil também pra avaliar sopros carotídeos com a ausculta.",
|
||||
},
|
||||
@@ -301,7 +343,8 @@ export const EXAM_POINTS: ExamPoint[] = [
|
||||
name: "Pulso Radial",
|
||||
system: "Vascular",
|
||||
technique: "palpacao",
|
||||
fraction: [0.1, 0.58, 0.6],
|
||||
anchor: "radiusR",
|
||||
fraction: [0.5, 0.05, 0.6],
|
||||
howTo: "Palpe com 2-3 dedos na face anterior do punho, lateralmente (lado do polegar).",
|
||||
whatFor: "Pulso periférico mais usado na prática — avalia frequência, ritmo, amplitude e simetria entre os dois lados.",
|
||||
},
|
||||
@@ -310,7 +353,8 @@ export const EXAM_POINTS: ExamPoint[] = [
|
||||
name: "Pulso Femoral",
|
||||
system: "Vascular",
|
||||
technique: "palpacao",
|
||||
fraction: [0.4, 0.44, 0.75],
|
||||
anchor: "pelvis",
|
||||
fraction: [0.25, 0.15, 0.75],
|
||||
howTo: "Palpe na região inguinal, no ponto médio entre a espinha ilíaca ântero-superior e a sínfise púbica.",
|
||||
whatFor: "Assimetria ou atraso entre pulso femoral e radial (\"pulso em atraso\") sugere coarctação de aorta.",
|
||||
},
|
||||
@@ -319,7 +363,8 @@ export const EXAM_POINTS: ExamPoint[] = [
|
||||
name: "Pulso Pedioso",
|
||||
system: "Vascular",
|
||||
technique: "palpacao",
|
||||
fraction: [0.38, 0.02, 0.65],
|
||||
anchor: "tibiaR",
|
||||
fraction: [0.5, 0.02, 0.6],
|
||||
howTo: "Palpe no dorso do pé, lateralmente ao tendão do extensor longo do hálux.",
|
||||
whatFor: "Ausência sugere doença arterial periférica — compare sempre os dois pés.",
|
||||
},
|
||||
|
||||
@@ -13,9 +13,18 @@ import { EXAM_POINTS, TECHNIQUES, type ExamTechnique } from "../data/physicalExa
|
||||
// manobra (inspeção/palpação/percussão/ausculta). Reaproveita o mesmo
|
||||
// <Scene> do Atlas (via prop "children", ver Scene.tsx) em vez de um corpo
|
||||
// geométrico à parte.
|
||||
// O esqueleto serve de referência pra posicionar os pontos de exame (ver
|
||||
// ExamMarkers.tsx) mesmo que o usuário não tenha marcado essa camada —
|
||||
// por isso é sempre buscado/carregado aqui, independente do toggle.
|
||||
const ALWAYS_LOAD = ["esqueletico"];
|
||||
|
||||
export default function PhysicalExamPage() {
|
||||
const visibleSystems = useAtlasStore((s) => s.visibleSystems);
|
||||
const { bySlug } = useSystemStructures(visibleSystems);
|
||||
const fetchSystems = useMemo(
|
||||
() => new Set([...visibleSystems, ...ALWAYS_LOAD]),
|
||||
[visibleSystems],
|
||||
);
|
||||
const { bySlug } = useSystemStructures(fetchSystems);
|
||||
const selectedStructureId = useAtlasStore((s) => s.selectedStructureId);
|
||||
|
||||
const [activeTechniques, setActiveTechniques] = useState<Set<ExamTechnique>>(
|
||||
@@ -103,7 +112,7 @@ export default function PhysicalExamPage() {
|
||||
)}
|
||||
|
||||
<div className="relative h-full min-h-0">
|
||||
<Scene bySlug={bySlug}>
|
||||
<Scene bySlug={bySlug} alwaysLoadSystems={ALWAYS_LOAD}>
|
||||
<ExamMarkers points={visiblePoints} selectedId={selectedId} onSelect={(id) => {
|
||||
setSelectedId(id);
|
||||
setRightOpen(true);
|
||||
|
||||
Reference in new issue
Block a user