use corpo real exame fisico

This commit is contained in:
Rodrigo Bertoldo de Sousa Martins committed 2026-09-10 20:07:34 -03:00
1 parent 939293ba60
commit 7e8963135b
4 files changed
+166 -108

No files matched your search

@@ -0,0 +1,58 @@
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 { 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.
export function ExamMarkers({
points,
selectedId,
onSelect,
}: {
points: ExamPoint[];
selectedId: string | null;
onSelect: (id: string) => void;
}) {
const contentBox = useAtlasStore((s) => s.contentBox);
if (!contentBox) return null;
const min = new THREE.Vector3(...contentBox.min);
const max = new THREE.Vector3(...contentBox.max);
const techniqueColor = (t: ExamTechnique) => TECHNIQUES.find((x) => x.key === t)?.color ?? "#fff";
return (
<>
{points.map((p) => {
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),
];
const isSelected = p.id === selectedId;
const color = techniqueColor(p.technique);
return (
<Html key={p.id} position={position} center distanceFactor={4} zIndexRange={[10, 0]}>
<button
onClick={() => onSelect(p.id)}
title={p.name}
className="w-4 h-4 rounded-full border-2 border-white/80 transition-transform hover:scale-125"
style={{
backgroundColor: color,
boxShadow: isSelected ? `0 0 0 5px ${color}55` : undefined,
transform: isSelected ? "scale(1.4)" : undefined,
}}
/>
</Html>
);
})}
</>
);
}
+13 -2
View File
@@ -1,4 +1,4 @@
import { Suspense, useEffect, useMemo, useRef } from "react"; import { Suspense, useEffect, useMemo, useRef, type ReactNode } from "react";
import { Canvas, useThree } from "@react-three/fiber"; import { Canvas, useThree } from "@react-three/fiber";
import { CameraControls, Grid } from "@react-three/drei"; import { CameraControls, Grid } from "@react-three/drei";
import * as THREE from "three"; import * as THREE from "three";
@@ -173,7 +173,17 @@ function CameraController({ modelFiles }: { modelFiles: string[] }) {
); );
} }
export function Scene({ bySlug }: { bySlug: Map<string, Structure> }) { export function Scene({
bySlug,
children,
}: {
bySlug: Map<string, Structure>;
// Ponto de extensão pra conteúdo 3D extra dentro do MESMO Canvas (e do
// mesmo contexto do react-three-fiber) — usado pelo módulo de Exame
// 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;
}) {
const visibleSystems = useAtlasStore((s) => s.visibleSystems); const visibleSystems = useAtlasStore((s) => s.visibleSystems);
const selectedStructureId = useAtlasStore((s) => s.selectedStructureId); const selectedStructureId = useAtlasStore((s) => s.selectedStructureId);
const selectStructure = useAtlasStore((s) => s.selectStructure); const selectStructure = useAtlasStore((s) => s.selectStructure);
@@ -245,6 +255,7 @@ export function Scene({ bySlug }: { bySlug: Map<string, Structure> }) {
infiniteGrid infiniteGrid
/> />
<CameraController modelFiles={modelFiles} /> <CameraController modelFiles={modelFiles} />
{children}
</Canvas> </Canvas>
); );
} }
+50 -41
View File
@@ -1,11 +1,20 @@
// Pontos do módulo de Exame Físico — coordenadas no MESMO sistema do // Pontos do módulo de Exame Físico, posicionados sobre o corpo 3D REAL do
// boneco esquemático (ver components/viewer/Mannequin.tsx): tórax é uma // Z-Anatomy (não um boneco geométrico) — por isso a posição de cada ponto
// caixa centrada em (0, 1.2, 0), abdome em (0, 0.75, 0), braços/pernas são // é dada como FRAÇÃO (0 a 1) do bounding box do conteúdo carregado, e não
// cilindros em x = ±0.28 / ±0.11. Convenção usada aqui: x negativo = lado // como coordenada absoluta: o corpo real não tem escala/origem fixa (é
// DIREITO do paciente, x positivo = lado ESQUERDO do paciente (o boneco é // recalculado a cada carga, ver computeVisibleBoundingBox em Scene.tsx), só
// visto de frente, então a direita do paciente fica à esquerda de quem // a PROPORÇÃO do corpo é estável. As frações de altura (fy) usam as mesmas
// olha — por isso cada descrição já diz o lado por extenso, pra não // faixas por região já validadas em Scene.tsx (BODY_REGION_Y_RANGES):
// depender de decorar a convenção). // 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].
//
// 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.
export type ExamTechnique = "inspecao" | "palpacao" | "percussao" | "ausculta"; export type ExamTechnique = "inspecao" | "palpacao" | "percussao" | "ausculta";
export const TECHNIQUES: { key: ExamTechnique; label: string; color: string }[] = [ export const TECHNIQUES: { key: ExamTechnique; label: string; color: string }[] = [
@@ -20,7 +29,7 @@ export interface ExamPoint {
name: string; name: string;
system: string; system: string;
technique: ExamTechnique; technique: ExamTechnique;
position: [number, number, number]; fraction: [number, number, number];
howTo: string; howTo: string;
whatFor: string; whatFor: string;
} }
@@ -32,7 +41,7 @@ export const EXAM_POINTS: ExamPoint[] = [
name: "Ectoscopia Geral", name: "Ectoscopia Geral",
system: "Geral", system: "Geral",
technique: "inspecao", technique: "inspecao",
position: [0, 1.65, 0.1], fraction: [0.5, 0.93, 0.85],
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.", 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.", 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.",
}, },
@@ -43,7 +52,7 @@ export const EXAM_POINTS: ExamPoint[] = [
name: "Foco Aórtico", name: "Foco Aórtico",
system: "Cardiovascular", system: "Cardiovascular",
technique: "ausculta", technique: "ausculta",
position: [-0.05, 1.4, 0.12], fraction: [0.44, 0.8, 0.85],
howTo: "2º espaço intercostal, à direita do esterno. Ausculte com o diafragma do estetoscópio, paciente sentado ou deitado.", 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.", whatFor: "Melhor foco para a valva aórtica — sopros de estenose/insuficiência aórtica costumam ser mais audíveis aqui.",
}, },
@@ -52,7 +61,7 @@ export const EXAM_POINTS: ExamPoint[] = [
name: "Foco Pulmonar", name: "Foco Pulmonar",
system: "Cardiovascular", system: "Cardiovascular",
technique: "ausculta", technique: "ausculta",
position: [0.05, 1.4, 0.12], fraction: [0.56, 0.8, 0.85],
howTo: "2º espaço intercostal, à esquerda do esterno.", 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.", whatFor: "Melhor foco para a valva pulmonar — sopros de estenose/insuficiência pulmonar e desdobramento de B2.",
}, },
@@ -61,7 +70,7 @@ export const EXAM_POINTS: ExamPoint[] = [
name: "Foco Aórtico Acessório (Erb)", name: "Foco Aórtico Acessório (Erb)",
system: "Cardiovascular", system: "Cardiovascular",
technique: "ausculta", technique: "ausculta",
position: [0.05, 1.32, 0.12], fraction: [0.56, 0.76, 0.85],
howTo: "3º espaço intercostal, à esquerda do esterno.", 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.", 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.",
}, },
@@ -70,7 +79,7 @@ export const EXAM_POINTS: ExamPoint[] = [
name: "Foco Tricúspide", name: "Foco Tricúspide",
system: "Cardiovascular", system: "Cardiovascular",
technique: "ausculta", technique: "ausculta",
position: [0.05, 1.22, 0.12], fraction: [0.55, 0.71, 0.85],
howTo: "4º-5º espaço intercostal, na borda esternal esquerda baixa.", 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.", whatFor: "Melhor foco para a valva tricúspide — sopros de insuficiência/estenose tricúspide.",
}, },
@@ -79,7 +88,7 @@ export const EXAM_POINTS: ExamPoint[] = [
name: "Foco Mitral (Ápice)", name: "Foco Mitral (Ápice)",
system: "Cardiovascular", system: "Cardiovascular",
technique: "ausculta", technique: "ausculta",
position: [0.14, 1.16, 0.13], fraction: [0.62, 0.68, 0.85],
howTo: "5º espaço intercostal esquerdo, na linha hemiclavicular (região do ictus cordis).", 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).", whatFor: "Melhor foco para a valva mitral — sopro de estenose mitral (ruflar diastólico) e insuficiência mitral (sopro holossistólico).",
}, },
@@ -88,7 +97,7 @@ export const EXAM_POINTS: ExamPoint[] = [
name: "Ictus Cordis (Choque de Ponta)", name: "Ictus Cordis (Choque de Ponta)",
system: "Cardiovascular", system: "Cardiovascular",
technique: "palpacao", technique: "palpacao",
position: [0.16, 1.18, 0.135], fraction: [0.63, 0.68, 0.86],
howTo: "Palpe com a polpa dos dedos (ou a região tenar) o 5º espaço intercostal esquerdo, linha hemiclavicular.", 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.", whatFor: "Localiza o ápice do coração; desvio lateral ou impulso muito forte sugere cardiomegalia/hipertrofia ventricular.",
}, },
@@ -97,7 +106,7 @@ export const EXAM_POINTS: ExamPoint[] = [
name: "Frêmito Precordial", name: "Frêmito Precordial",
system: "Cardiovascular", system: "Cardiovascular",
technique: "palpacao", technique: "palpacao",
position: [0.08, 1.28, 0.12], fraction: [0.58, 0.74, 0.85],
howTo: "Apoie a palma da mão sobre o precórdio e sinta uma vibração (\"gato ronronando\").", 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.", whatFor: "Frêmito = sopro cardíaco intenso o bastante para ser sentido, geralmente indica lesão valvar significativa.",
}, },
@@ -108,7 +117,7 @@ export const EXAM_POINTS: ExamPoint[] = [
name: "Ápice Pulmonar Direito", name: "Ápice Pulmonar Direito",
system: "Respiratório", system: "Respiratório",
technique: "ausculta", technique: "ausculta",
position: [-0.12, 1.42, 0.11], fraction: [0.38, 0.815, 0.8],
howTo: "Ausculte logo acima da clavícula direita, pedindo ao paciente para respirar fundo pela boca.", 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.", whatFor: "Murmúrio vesicular reduzido/ausente ou ruídos adventícios (estertores, sibilos) localizados aqui sugerem acometimento do ápice pulmonar direito.",
}, },
@@ -117,7 +126,7 @@ export const EXAM_POINTS: ExamPoint[] = [
name: "Ápice Pulmonar Esquerdo", name: "Ápice Pulmonar Esquerdo",
system: "Respiratório", system: "Respiratório",
technique: "ausculta", technique: "ausculta",
position: [0.12, 1.42, 0.11], fraction: [0.62, 0.815, 0.8],
howTo: "Mesma manobra do lado esquerdo, logo acima da clavícula esquerda.", 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.", whatFor: "Comparação bilateral é essencial — assimetria entre os dois ápices é o achado mais importante.",
}, },
@@ -126,7 +135,7 @@ export const EXAM_POINTS: ExamPoint[] = [
name: "Campo Médio Direito", name: "Campo Médio Direito",
system: "Respiratório", system: "Respiratório",
technique: "ausculta", technique: "ausculta",
position: [-0.15, 1.22, 0.1], fraction: [0.35, 0.72, 0.78],
howTo: "Ausculte na linha hemiclavicular/axilar direita, altura do 4º espaço intercostal.", 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.", whatFor: "Zona onde sopros tubários e estertores de pneumonia em lobo médio costumam aparecer primeiro.",
}, },
@@ -135,7 +144,7 @@ export const EXAM_POINTS: ExamPoint[] = [
name: "Campo Médio Esquerdo", name: "Campo Médio Esquerdo",
system: "Respiratório", system: "Respiratório",
technique: "ausculta", technique: "ausculta",
position: [0.15, 1.22, 0.1], fraction: [0.65, 0.72, 0.78],
howTo: "Mesma altura, do lado esquerdo.", howTo: "Mesma altura, do lado esquerdo.",
whatFor: "Comparação bilateral do murmúrio vesicular e de ruídos adventícios.", whatFor: "Comparação bilateral do murmúrio vesicular e de ruídos adventícios.",
}, },
@@ -144,7 +153,7 @@ export const EXAM_POINTS: ExamPoint[] = [
name: "Base Pulmonar Direita", name: "Base Pulmonar Direita",
system: "Respiratório", system: "Respiratório",
technique: "ausculta", technique: "ausculta",
position: [-0.17, 1.0, 0.08], fraction: [0.33, 0.645, 0.75],
howTo: "Ausculte nas bases (região inferior do tórax, de preferência também pelas costas).", 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).", whatFor: "Local clássico de estertores crepitantes (insuficiência cardíaca, pneumonia de base) e de derrame pleural (murmúrio abolido).",
}, },
@@ -153,7 +162,7 @@ export const EXAM_POINTS: ExamPoint[] = [
name: "Base Pulmonar Esquerda", name: "Base Pulmonar Esquerda",
system: "Respiratório", system: "Respiratório",
technique: "ausculta", technique: "ausculta",
position: [0.17, 1.0, 0.08], fraction: [0.67, 0.645, 0.75],
howTo: "Mesma região, do lado esquerdo.", howTo: "Mesma região, do lado esquerdo.",
whatFor: "Comparação bilateral das bases — assimetria é o achado mais valioso.", whatFor: "Comparação bilateral das bases — assimetria é o achado mais valioso.",
}, },
@@ -162,7 +171,7 @@ export const EXAM_POINTS: ExamPoint[] = [
name: "Expansibilidade Torácica", name: "Expansibilidade Torácica",
system: "Respiratório", system: "Respiratório",
technique: "palpacao", technique: "palpacao",
position: [0, 1.05, -0.12], fraction: [0.5, 0.7, 0.15],
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.", 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.", whatFor: "Redução da expansibilidade de um lado sugere derrame pleural, atelectasia ou pneumotórax naquele hemitórax.",
}, },
@@ -171,7 +180,7 @@ export const EXAM_POINTS: ExamPoint[] = [
name: "Frêmito Toracovocal", name: "Frêmito Toracovocal",
system: "Respiratório", system: "Respiratório",
technique: "palpacao", technique: "palpacao",
position: [0.1, 1.15, 0.11], fraction: [0.6, 0.73, 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.", 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.", whatFor: "Aumentado em condensações (pneumonia); diminuído/abolido em derrame pleural, pneumotórax ou DPOC.",
}, },
@@ -180,7 +189,7 @@ export const EXAM_POINTS: ExamPoint[] = [
name: "Percussão dos Campos Pulmonares", name: "Percussão dos Campos Pulmonares",
system: "Respiratório", system: "Respiratório",
technique: "percussao", technique: "percussao",
position: [-0.1, 1.15, 0.11], fraction: [0.4, 0.73, 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.", 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.", whatFor: "Som claro pulmonar normal; hipertimpanismo sugere pneumotórax; macicez/submacicez sugere derrame pleural ou consolidação.",
}, },
@@ -191,7 +200,7 @@ export const EXAM_POINTS: ExamPoint[] = [
name: "Inspeção Abdominal", name: "Inspeção Abdominal",
system: "Abdominal", system: "Abdominal",
technique: "inspecao", technique: "inspecao",
position: [0, 0.82, 0.11], fraction: [0.5, 0.63, 0.85],
howTo: "Com o paciente deitado e relaxado, observe forma, simetria, distensão, cicatrizes, circulação colateral e peristaltismo visível.", 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.", whatFor: "Abdome distendido, assimétrico ou com peristaltismo visível pode indicar obstrução, massa ou ascite.",
}, },
@@ -200,7 +209,7 @@ export const EXAM_POINTS: ExamPoint[] = [
name: "Ausculta dos Ruídos Hidroaéreos", name: "Ausculta dos Ruídos Hidroaéreos",
system: "Abdominal", system: "Abdominal",
technique: "ausculta", technique: "ausculta",
position: [0, 0.75, 0.11], fraction: [0.5, 0.58, 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.", 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.", whatFor: "Ruídos aumentados sugerem obstrução inicial; ausência de ruídos (\"silêncio abdominal\") é sinal de íleo paralítico/abdome agudo.",
}, },
@@ -209,7 +218,7 @@ export const EXAM_POINTS: ExamPoint[] = [
name: "Percussão Abdominal (Timpanismo)", name: "Percussão Abdominal (Timpanismo)",
system: "Abdominal", system: "Abdominal",
technique: "percussao", technique: "percussao",
position: [-0.08, 0.72, 0.11], fraction: [0.44, 0.56, 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.", 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.", whatFor: "Macicez onde deveria haver timpanismo sugere massa, ascite ou visceromegalia; timpanismo excessivo sugere distensão gasosa.",
}, },
@@ -218,16 +227,16 @@ export const EXAM_POINTS: ExamPoint[] = [
name: "Delimitação da Loja Hepática", name: "Delimitação da Loja Hepática",
system: "Abdominal", system: "Abdominal",
technique: "percussao", technique: "percussao",
position: [-0.12, 0.85, 0.11], fraction: [0.35, 0.63, 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).", 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; um espaço de Traube maciço ou uma loja hepática maior que o esperado sugere hepatomegalia.", whatFor: "Estima o tamanho do fígado; uma loja hepática maior que o esperado sugere hepatomegalia.",
}, },
{ {
id: "palpacao-superficial", id: "palpacao-superficial",
name: "Palpação Superficial", name: "Palpação Superficial",
system: "Abdominal", system: "Abdominal",
technique: "palpacao", technique: "palpacao",
position: [0.08, 0.78, 0.105], fraction: [0.56, 0.6, 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.", 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.", whatFor: "Detecta defesa muscular voluntária/involuntária e localiza dor antes de aprofundar a palpação.",
}, },
@@ -236,7 +245,7 @@ export const EXAM_POINTS: ExamPoint[] = [
name: "Palpação Profunda", name: "Palpação Profunda",
system: "Abdominal", system: "Abdominal",
technique: "palpacao", technique: "palpacao",
position: [0.08, 0.7, 0.105], fraction: [0.56, 0.55, 0.78],
howTo: "Com as duas mãos sobrepostas, palpe mais profundamente para identificar massas, visceromegalias e pontos dolorosos específicos.", 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).", whatFor: "Identifica massas abdominais, contornos de vísceras e sinais de irritação peritoneal (dor à descompressão brusca).",
}, },
@@ -245,7 +254,7 @@ export const EXAM_POINTS: ExamPoint[] = [
name: "Ponto de McBurney", name: "Ponto de McBurney",
system: "Abdominal", system: "Abdominal",
technique: "palpacao", technique: "palpacao",
position: [-0.13, 0.62, 0.1], fraction: [0.32, 0.5, 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.", 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.", whatFor: "Dor à palpação (e principalmente à descompressão brusca) neste ponto é clássica de apendicite aguda.",
}, },
@@ -254,7 +263,7 @@ export const EXAM_POINTS: ExamPoint[] = [
name: "Sinal de Murphy", name: "Sinal de Murphy",
system: "Abdominal", system: "Abdominal",
technique: "palpacao", technique: "palpacao",
position: [-0.13, 0.85, 0.105], fraction: [0.34, 0.64, 0.78],
howTo: "Palpe sob o rebordo costal direito e peça ao paciente para inspirar fundo.", 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.", whatFor: "Sinal positivo = parada súbita da inspiração por dor — sugere colecistite aguda.",
}, },
@@ -263,7 +272,7 @@ export const EXAM_POINTS: ExamPoint[] = [
name: "Palpação Hepática (Borda do Fígado)", name: "Palpação Hepática (Borda do Fígado)",
system: "Abdominal", system: "Abdominal",
technique: "palpacao", technique: "palpacao",
position: [-0.15, 0.82, 0.11], fraction: [0.33, 0.63, 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.", 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.", whatFor: "Avalia tamanho, consistência e borda do fígado — borda romba/endurecida sugere hepatopatia crônica.",
}, },
@@ -272,7 +281,7 @@ export const EXAM_POINTS: ExamPoint[] = [
name: "Palpação do Baço", name: "Palpação do Baço",
system: "Abdominal", system: "Abdominal",
technique: "palpacao", technique: "palpacao",
position: [0.15, 0.85, 0.1], fraction: [0.68, 0.64, 0.75],
howTo: "Com o paciente em decúbito lateral direito, palpe sob o rebordo costal esquerdo durante a inspiração profunda.", 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.", whatFor: "O baço normal não é palpável — se for palpável, geralmente indica esplenomegalia.",
}, },
@@ -283,7 +292,7 @@ export const EXAM_POINTS: ExamPoint[] = [
name: "Pulso Carotídeo", name: "Pulso Carotídeo",
system: "Vascular", system: "Vascular",
technique: "palpacao", technique: "palpacao",
position: [-0.06, 1.5, 0.08], fraction: [0.44, 0.87, 0.7],
howTo: "Palpe suavemente (nunca os dois lados ao mesmo tempo) na borda anterior do músculo esternocleidomastóideo, no pescoço.", 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.", whatFor: "Avalia amplitude e ritmo do pulso central; útil também pra avaliar sopros carotídeos com a ausculta.",
}, },
@@ -292,7 +301,7 @@ export const EXAM_POINTS: ExamPoint[] = [
name: "Pulso Radial", name: "Pulso Radial",
system: "Vascular", system: "Vascular",
technique: "palpacao", technique: "palpacao",
position: [-0.28, 0.83, 0.03], fraction: [0.1, 0.58, 0.6],
howTo: "Palpe com 2-3 dedos na face anterior do punho, lateralmente (lado do polegar).", 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.", whatFor: "Pulso periférico mais usado na prática — avalia frequência, ritmo, amplitude e simetria entre os dois lados.",
}, },
@@ -301,7 +310,7 @@ export const EXAM_POINTS: ExamPoint[] = [
name: "Pulso Femoral", name: "Pulso Femoral",
system: "Vascular", system: "Vascular",
technique: "palpacao", technique: "palpacao",
position: [-0.1, 0.62, 0.08], fraction: [0.4, 0.44, 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.", 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.", whatFor: "Assimetria ou atraso entre pulso femoral e radial (\"pulso em atraso\") sugere coarctação de aorta.",
}, },
@@ -310,7 +319,7 @@ export const EXAM_POINTS: ExamPoint[] = [
name: "Pulso Pedioso", name: "Pulso Pedioso",
system: "Vascular", system: "Vascular",
technique: "palpacao", technique: "palpacao",
position: [-0.11, -0.02, 0.06], fraction: [0.38, 0.02, 0.65],
howTo: "Palpe no dorso do pé, lateralmente ao tendão do extensor longo do hálux.", 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.", whatFor: "Ausência sugere doença arterial periférica — compare sempre os dois pés.",
}, },
+45 -65
View File
@@ -1,16 +1,23 @@
import { useMemo, useState } from "react"; import { useEffect, useMemo, useState } from "react";
import { Canvas } from "@react-three/fiber"; import { useSystemStructures } from "../hooks/useSystemStructures.js";
import { CameraControls, Html } from "@react-three/drei"; import { useAtlasStore } from "../store/useAtlasStore.js";
import { Mannequin } from "../components/viewer/Mannequin.js"; import { Scene } from "../components/viewer/Scene.js";
import { ExamMarkers } from "../components/viewer/ExamMarkers.js";
import { LayerToggle } from "../components/LayerToggle.js";
import { QuickRegionNav } from "../components/QuickRegionNav.js";
import { EXAM_POINTS, TECHNIQUES, type ExamTechnique } from "../data/physicalExam.js"; import { EXAM_POINTS, TECHNIQUES, type ExamTechnique } from "../data/physicalExam.js";
// Módulo de estudo pro exame físico: o mesmo boneco esquemático do módulo // Módulo de estudo pro exame físico, sobre o corpo 3D REAL do Z-Anatomy
// de Planos e Eixos, agora com pontos clicáveis mostrando ONDE e COMO se // (mesmo visualizador do Atlas — camadas, navegação por região, câmera
// faz cada manobra (inspeção/palpação/percussão/ausculta) — pensado pra // tudo igual), com pontos clicáveis mostrando ONDE e COMO se faz cada
// quem está estudando propedêutica, não pra troca do atlas 3D real (que // manobra (inspeção/palpação/percussão/ausculta). Reaproveita o mesmo
// seria overkill pra um boneco de pontos de exame, que não muda por // <Scene> do Atlas (via prop "children", ver Scene.tsx) em vez de um corpo
// paciente). // geométrico à parte.
export default function PhysicalExamPage() { export default function PhysicalExamPage() {
const visibleSystems = useAtlasStore((s) => s.visibleSystems);
const { bySlug } = useSystemStructures(visibleSystems);
const selectedStructureId = useAtlasStore((s) => s.selectedStructureId);
const [activeTechniques, setActiveTechniques] = useState<Set<ExamTechnique>>( const [activeTechniques, setActiveTechniques] = useState<Set<ExamTechnique>>(
() => new Set(TECHNIQUES.map((t) => t.key)), () => new Set(TECHNIQUES.map((t) => t.key)),
); );
@@ -18,6 +25,13 @@ export default function PhysicalExamPage() {
const [leftOpen, setLeftOpen] = useState(false); const [leftOpen, setLeftOpen] = useState(false);
const [rightOpen, setRightOpen] = useState(false); const [rightOpen, setRightOpen] = useState(false);
useEffect(() => {
if (selectedStructureId) {
setRightOpen(true);
setLeftOpen(false);
}
}, [selectedStructureId]);
function toggleTechnique(key: ExamTechnique) { function toggleTechnique(key: ExamTechnique) {
setActiveTechniques((prev) => { setActiveTechniques((prev) => {
const next = new Set(prev); const next = new Set(prev);
@@ -39,18 +53,21 @@ export default function PhysicalExamPage() {
<div className="h-full relative md:grid md:grid-cols-[260px_1fr_320px]"> <div className="h-full relative md:grid md:grid-cols-[260px_1fr_320px]">
<aside <aside
className={`fixed md:static inset-y-0 left-0 z-30 w-[86vw] max-w-[280px] md:w-auto className={`fixed md:static inset-y-0 left-0 z-30 w-[86vw] max-w-[280px] md:w-auto
border-r border-white/10 bg-panel p-4 space-y-4 overflow-y-auto border-r border-white/10 bg-panel p-3 flex flex-col min-h-0 overflow-y-auto
transition-transform duration-200 md:translate-x-0 transition-transform duration-200 md:translate-x-0
${leftOpen ? "translate-x-0" : "-translate-x-full"}`} ${leftOpen ? "translate-x-0" : "-translate-x-full"}`}
> >
<button <button
onClick={() => setLeftOpen(false)} onClick={() => setLeftOpen(false)}
className="md:hidden float-right text-slate-400 hover:text-white text-sm px-2 py-1" className="md:hidden self-end text-slate-400 hover:text-white text-sm mb-2 px-2 py-1"
aria-label="Fechar painel" aria-label="Fechar painel"
> >
✕ Fechar ✕ Fechar
</button> </button>
<div>
<LayerToggle />
<div className="pt-3 mt-3 border-t border-white/10">
<h3 className="text-xs uppercase tracking-wide text-slate-400 mb-2">Técnicas de Exame</h3> <h3 className="text-xs uppercase tracking-wide text-slate-400 mb-2">Técnicas de Exame</h3>
<div className="space-y-1.5"> <div className="space-y-1.5">
{TECHNIQUES.map((t) => ( {TECHNIQUES.map((t) => (
@@ -68,24 +85,11 @@ export default function PhysicalExamPage() {
</div> </div>
</div> </div>
<div className="pt-3 border-t border-white/10"> <p className="text-[11px] text-slate-500 mt-4 leading-relaxed">
<h3 className="text-xs uppercase tracking-wide text-slate-400 mb-2">Como usar</h3> Ligue uma camada (ex: Esquelético) como referência, marque as
<p className="text-xs text-slate-400 leading-relaxed"> técnicas que quer estudar e clique nos pontos coloridos sobre o
Marque as técnicas que quer estudar e clique nos pontos coloridos corpo. Use o menu de regiões pra focar rápido numa parte do corpo.
sobre o boneco. Cada ponto explica onde encostar, como fazer a </p>
manobra e o que ela avalia — pensado como revisão rápida de
propedêutica, não substitui a prática supervisionada.
</p>
</div>
<div className="pt-3 border-t border-white/10 space-y-1">
<h3 className="text-xs uppercase tracking-wide text-slate-400 mb-2">Sistemas cobertos</h3>
{[...new Set(EXAM_POINTS.map((p) => p.system))].map((sys) => (
<p key={sys} className="text-xs text-slate-400">
{sys} ({EXAM_POINTS.filter((p) => p.system === sys).length})
</p>
))}
</div>
</aside> </aside>
{(leftOpen || rightOpen) && ( {(leftOpen || rightOpen) && (
@@ -99,6 +103,14 @@ export default function PhysicalExamPage() {
)} )}
<div className="relative h-full min-h-0"> <div className="relative h-full min-h-0">
<Scene bySlug={bySlug}>
<ExamMarkers points={visiblePoints} selectedId={selectedId} onSelect={(id) => {
setSelectedId(id);
setRightOpen(true);
}} />
</Scene>
<QuickRegionNav />
<button <button
onClick={() => setLeftOpen(true)} onClick={() => setLeftOpen(true)}
className="md:hidden absolute top-3 left-3 z-10 bg-panel/90 border border-white/10 rounded-lg px-3 py-2 text-xs shadow-lg" className="md:hidden absolute top-3 left-3 z-10 bg-panel/90 border border-white/10 rounded-lg px-3 py-2 text-xs shadow-lg"
@@ -111,38 +123,6 @@ export default function PhysicalExamPage() {
> >
ℹ️ Detalhes ℹ️ Detalhes
</button> </button>
<Canvas dpr={[1, 1.5]} camera={{ position: [1.6, 1.3, 2.0], fov: 45 }}>
<color attach="background" args={["#0b1220"]} />
<ambientLight intensity={0.65} />
<directionalLight position={[2, 3, 2]} intensity={1} />
<Mannequin />
{visiblePoints.map((p) => {
const isSelected = p.id === selectedId;
const color = techniqueColor(p.technique);
return (
<Html key={p.id} position={p.position} center distanceFactor={4} zIndexRange={[10, 0]}>
<button
onClick={() => {
setSelectedId(p.id);
setRightOpen(true);
}}
title={p.name}
className="w-4 h-4 rounded-full border-2 border-white/80 transition-transform hover:scale-125"
style={{
backgroundColor: color,
boxShadow: isSelected ? `0 0 0 5px ${color}55` : undefined,
transform: isSelected ? "scale(1.4)" : undefined,
}}
/>
</Html>
);
})}
<CameraControls makeDefault />
</Canvas>
</div> </div>
<aside <aside
@@ -153,7 +133,7 @@ export default function PhysicalExamPage() {
> >
<button <button
onClick={() => setRightOpen(false)} onClick={() => setRightOpen(false)}
className="md:hidden text-slate-400 hover:text-white text-sm px-2 py-1" className="md:hidden text-slate-400 hover:text-white text-sm mb-2 px-2 py-1"
aria-label="Fechar painel" aria-label="Fechar painel"
> >
✕ Fechar ✕ Fechar
@@ -161,7 +141,7 @@ export default function PhysicalExamPage() {
{!selected ? ( {!selected ? (
<p className="text-sm text-slate-400"> <p className="text-sm text-slate-400">
Clique em um ponto colorido sobre o boneco pra ver a técnica, o Clique em um ponto colorido sobre o corpo pra ver a técnica, o
local exato e o que ela avalia. local exato e o que ela avalia.
</p> </p>
) : ( ) : (