exame fisico
This commit is contained in:
1 parent
9f2fe75ff6
commit
939293ba60
8 files changed
+560
-37
No files matched your search
Binary file not shown.
|
Before Width: | Height: | Size: 164 KiB After Width: | Height: | Size: 55 KiB |
@@ -3,6 +3,7 @@ import { Layout } from "./components/Layout.js";
|
|||||||
import AtlasPage from "./routes/AtlasPage.js";
|
import AtlasPage from "./routes/AtlasPage.js";
|
||||||
import PlanesPage from "./routes/PlanesPage.js";
|
import PlanesPage from "./routes/PlanesPage.js";
|
||||||
import QuizPage from "./routes/QuizPage.js";
|
import QuizPage from "./routes/QuizPage.js";
|
||||||
|
import PhysicalExamPage from "./routes/PhysicalExamPage.js";
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
return (
|
return (
|
||||||
@@ -10,6 +11,7 @@ export default function App() {
|
|||||||
<Route element={<Layout />}>
|
<Route element={<Layout />}>
|
||||||
<Route path="/" element={<AtlasPage />} />
|
<Route path="/" element={<AtlasPage />} />
|
||||||
<Route path="/planos-anatomicos" element={<PlanesPage />} />
|
<Route path="/planos-anatomicos" element={<PlanesPage />} />
|
||||||
|
<Route path="/exame-fisico" element={<PhysicalExamPage />} />
|
||||||
<Route path="/quiz" element={<QuizPage />} />
|
<Route path="/quiz" element={<QuizPage />} />
|
||||||
</Route>
|
</Route>
|
||||||
</Routes>
|
</Routes>
|
||||||
|
|||||||
@@ -17,6 +17,9 @@ export function Layout() {
|
|||||||
<NavLink to="/planos-anatomicos" className={linkClass}>
|
<NavLink to="/planos-anatomicos" className={linkClass}>
|
||||||
Planos e Eixos
|
Planos e Eixos
|
||||||
</NavLink>
|
</NavLink>
|
||||||
|
<NavLink to="/exame-fisico" className={linkClass}>
|
||||||
|
Exame Físico
|
||||||
|
</NavLink>
|
||||||
<NavLink to="/quiz" className={linkClass}>
|
<NavLink to="/quiz" className={linkClass}>
|
||||||
Quiz
|
Quiz
|
||||||
</NavLink>
|
</NavLink>
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ export function SkinDiagram({ bySlug }: { bySlug: Map<string, Structure> }) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p className="text-[11px] text-slate-500 mt-2">
|
<p className="text-[11px] text-slate-500 mt-2">
|
||||||
Imagem de domínio público — National Cancer Institute / SEER Training Modules.
|
Imagem de domínio público (National Cancer Institute / SEER Training Modules), legendas traduzidas para PT-BR.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
// Boneco esquemático simples (mesmas proporções/posições usadas em
|
||||||
|
// PlanesPage) — extraído pra um componente próprio pra ser reaproveitado
|
||||||
|
// também pelo módulo de Exame Físico, que precisa das MESMAS coordenadas
|
||||||
|
// de tórax/abdome/membros pra posicionar os pontos de exame corretamente.
|
||||||
|
export function Mannequin() {
|
||||||
|
const skin = "#8fa3c8";
|
||||||
|
return (
|
||||||
|
<group>
|
||||||
|
<mesh position={[0, 1.65, 0]}>
|
||||||
|
<sphereGeometry args={[0.12]} />
|
||||||
|
<meshStandardMaterial color={skin} />
|
||||||
|
</mesh>
|
||||||
|
<mesh position={[0, 1.2, 0]}>
|
||||||
|
<boxGeometry args={[0.4, 0.55, 0.22]} />
|
||||||
|
<meshStandardMaterial color={skin} />
|
||||||
|
</mesh>
|
||||||
|
<mesh position={[0, 0.75, 0]}>
|
||||||
|
<boxGeometry args={[0.36, 0.3, 0.2]} />
|
||||||
|
<meshStandardMaterial color={skin} />
|
||||||
|
</mesh>
|
||||||
|
{[1, -1].map((side) => (
|
||||||
|
<group key={`arm-${side}`}>
|
||||||
|
<mesh position={[side * 0.28, 1.1, 0]}>
|
||||||
|
<cylinderGeometry args={[0.05, 0.045, 0.55, 10]} />
|
||||||
|
<meshStandardMaterial color={skin} />
|
||||||
|
</mesh>
|
||||||
|
</group>
|
||||||
|
))}
|
||||||
|
{[1, -1].map((side) => (
|
||||||
|
<group key={`leg-${side}`}>
|
||||||
|
<mesh position={[side * 0.11, 0.3, 0]}>
|
||||||
|
<cylinderGeometry args={[0.065, 0.05, 0.65, 10]} />
|
||||||
|
<meshStandardMaterial color={skin} />
|
||||||
|
</mesh>
|
||||||
|
</group>
|
||||||
|
))}
|
||||||
|
</group>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,317 @@
|
|||||||
|
// Pontos do módulo de Exame Físico — coordenadas no MESMO sistema do
|
||||||
|
// boneco esquemático (ver components/viewer/Mannequin.tsx): tórax é uma
|
||||||
|
// caixa centrada em (0, 1.2, 0), abdome em (0, 0.75, 0), braços/pernas são
|
||||||
|
// cilindros em x = ±0.28 / ±0.11. Convenção usada aqui: x negativo = lado
|
||||||
|
// DIREITO do paciente, x positivo = lado ESQUERDO do paciente (o boneco é
|
||||||
|
// visto de frente, então a direita do paciente fica à esquerda de quem
|
||||||
|
// olha — por isso cada descrição já diz o lado por extenso, pra não
|
||||||
|
// depender de decorar a convenção).
|
||||||
|
export type ExamTechnique = "inspecao" | "palpacao" | "percussao" | "ausculta";
|
||||||
|
|
||||||
|
export const TECHNIQUES: { key: ExamTechnique; label: string; color: string }[] = [
|
||||||
|
{ key: "inspecao", label: "Inspeção", color: "#f2c14e" },
|
||||||
|
{ key: "palpacao", label: "Palpação", color: "#4f7cff" },
|
||||||
|
{ key: "percussao", label: "Percussão", color: "#a78bfa" },
|
||||||
|
{ key: "ausculta", label: "Ausculta", color: "#34d399" },
|
||||||
|
];
|
||||||
|
|
||||||
|
export interface ExamPoint {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
system: string;
|
||||||
|
technique: ExamTechnique;
|
||||||
|
position: [number, number, number];
|
||||||
|
howTo: string;
|
||||||
|
whatFor: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const EXAM_POINTS: ExamPoint[] = [
|
||||||
|
// ---- Geral ----
|
||||||
|
{
|
||||||
|
id: "ectoscopia",
|
||||||
|
name: "Ectoscopia Geral",
|
||||||
|
system: "Geral",
|
||||||
|
technique: "inspecao",
|
||||||
|
position: [0, 1.65, 0.1],
|
||||||
|
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.",
|
||||||
|
},
|
||||||
|
|
||||||
|
// ---- Cardiovascular: ausculta (focos clássicos) ----
|
||||||
|
{
|
||||||
|
id: "foco-aortico",
|
||||||
|
name: "Foco Aórtico",
|
||||||
|
system: "Cardiovascular",
|
||||||
|
technique: "ausculta",
|
||||||
|
position: [-0.05, 1.4, 0.12],
|
||||||
|
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.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "foco-pulmonar",
|
||||||
|
name: "Foco Pulmonar",
|
||||||
|
system: "Cardiovascular",
|
||||||
|
technique: "ausculta",
|
||||||
|
position: [0.05, 1.4, 0.12],
|
||||||
|
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.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "foco-aortico-acessorio",
|
||||||
|
name: "Foco Aórtico Acessório (Erb)",
|
||||||
|
system: "Cardiovascular",
|
||||||
|
technique: "ausculta",
|
||||||
|
position: [0.05, 1.32, 0.12],
|
||||||
|
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.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "foco-tricuspide",
|
||||||
|
name: "Foco Tricúspide",
|
||||||
|
system: "Cardiovascular",
|
||||||
|
technique: "ausculta",
|
||||||
|
position: [0.05, 1.22, 0.12],
|
||||||
|
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.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "foco-mitral",
|
||||||
|
name: "Foco Mitral (Ápice)",
|
||||||
|
system: "Cardiovascular",
|
||||||
|
technique: "ausculta",
|
||||||
|
position: [0.14, 1.16, 0.13],
|
||||||
|
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).",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "ictus-cordis",
|
||||||
|
name: "Ictus Cordis (Choque de Ponta)",
|
||||||
|
system: "Cardiovascular",
|
||||||
|
technique: "palpacao",
|
||||||
|
position: [0.16, 1.18, 0.135],
|
||||||
|
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.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "fremito-precordial",
|
||||||
|
name: "Frêmito Precordial",
|
||||||
|
system: "Cardiovascular",
|
||||||
|
technique: "palpacao",
|
||||||
|
position: [0.08, 1.28, 0.12],
|
||||||
|
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.",
|
||||||
|
},
|
||||||
|
|
||||||
|
// ---- Respiratório ----
|
||||||
|
{
|
||||||
|
id: "apice-pulmonar-d",
|
||||||
|
name: "Ápice Pulmonar Direito",
|
||||||
|
system: "Respiratório",
|
||||||
|
technique: "ausculta",
|
||||||
|
position: [-0.12, 1.42, 0.11],
|
||||||
|
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.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "apice-pulmonar-e",
|
||||||
|
name: "Ápice Pulmonar Esquerdo",
|
||||||
|
system: "Respiratório",
|
||||||
|
technique: "ausculta",
|
||||||
|
position: [0.12, 1.42, 0.11],
|
||||||
|
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.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "campo-medio-d",
|
||||||
|
name: "Campo Médio Direito",
|
||||||
|
system: "Respiratório",
|
||||||
|
technique: "ausculta",
|
||||||
|
position: [-0.15, 1.22, 0.1],
|
||||||
|
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.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "campo-medio-e",
|
||||||
|
name: "Campo Médio Esquerdo",
|
||||||
|
system: "Respiratório",
|
||||||
|
technique: "ausculta",
|
||||||
|
position: [0.15, 1.22, 0.1],
|
||||||
|
howTo: "Mesma altura, do lado esquerdo.",
|
||||||
|
whatFor: "Comparação bilateral do murmúrio vesicular e de ruídos adventícios.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "base-pulmonar-d",
|
||||||
|
name: "Base Pulmonar Direita",
|
||||||
|
system: "Respiratório",
|
||||||
|
technique: "ausculta",
|
||||||
|
position: [-0.17, 1.0, 0.08],
|
||||||
|
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).",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "base-pulmonar-e",
|
||||||
|
name: "Base Pulmonar Esquerda",
|
||||||
|
system: "Respiratório",
|
||||||
|
technique: "ausculta",
|
||||||
|
position: [0.17, 1.0, 0.08],
|
||||||
|
howTo: "Mesma região, do lado esquerdo.",
|
||||||
|
whatFor: "Comparação bilateral das bases — assimetria é o achado mais valioso.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "expansibilidade-toracica",
|
||||||
|
name: "Expansibilidade Torácica",
|
||||||
|
system: "Respiratório",
|
||||||
|
technique: "palpacao",
|
||||||
|
position: [0, 1.05, -0.12],
|
||||||
|
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.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "fremito-toracovocal",
|
||||||
|
name: "Frêmito Toracovocal",
|
||||||
|
system: "Respiratório",
|
||||||
|
technique: "palpacao",
|
||||||
|
position: [0.1, 1.15, 0.11],
|
||||||
|
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.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "percussao-pulmonar",
|
||||||
|
name: "Percussão dos Campos Pulmonares",
|
||||||
|
system: "Respiratório",
|
||||||
|
technique: "percussao",
|
||||||
|
position: [-0.1, 1.15, 0.11],
|
||||||
|
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.",
|
||||||
|
},
|
||||||
|
|
||||||
|
// ---- Abdominal ----
|
||||||
|
{
|
||||||
|
id: "inspecao-abdominal",
|
||||||
|
name: "Inspeção Abdominal",
|
||||||
|
system: "Abdominal",
|
||||||
|
technique: "inspecao",
|
||||||
|
position: [0, 0.82, 0.11],
|
||||||
|
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.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "ausculta-abdominal",
|
||||||
|
name: "Ausculta dos Ruídos Hidroaéreos",
|
||||||
|
system: "Abdominal",
|
||||||
|
technique: "ausculta",
|
||||||
|
position: [0, 0.75, 0.11],
|
||||||
|
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.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "percussao-abdominal",
|
||||||
|
name: "Percussão Abdominal (Timpanismo)",
|
||||||
|
system: "Abdominal",
|
||||||
|
technique: "percussao",
|
||||||
|
position: [-0.08, 0.72, 0.11],
|
||||||
|
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.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "macicez-hepatica",
|
||||||
|
name: "Delimitação da Loja Hepática",
|
||||||
|
system: "Abdominal",
|
||||||
|
technique: "percussao",
|
||||||
|
position: [-0.12, 0.85, 0.11],
|
||||||
|
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.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "palpacao-superficial",
|
||||||
|
name: "Palpação Superficial",
|
||||||
|
system: "Abdominal",
|
||||||
|
technique: "palpacao",
|
||||||
|
position: [0.08, 0.78, 0.105],
|
||||||
|
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.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "palpacao-profunda",
|
||||||
|
name: "Palpação Profunda",
|
||||||
|
system: "Abdominal",
|
||||||
|
technique: "palpacao",
|
||||||
|
position: [0.08, 0.7, 0.105],
|
||||||
|
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).",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "ponto-mcburney",
|
||||||
|
name: "Ponto de McBurney",
|
||||||
|
system: "Abdominal",
|
||||||
|
technique: "palpacao",
|
||||||
|
position: [-0.13, 0.62, 0.1],
|
||||||
|
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.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "sinal-murphy",
|
||||||
|
name: "Sinal de Murphy",
|
||||||
|
system: "Abdominal",
|
||||||
|
technique: "palpacao",
|
||||||
|
position: [-0.13, 0.85, 0.105],
|
||||||
|
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.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "palpacao-hepatica",
|
||||||
|
name: "Palpação Hepática (Borda do Fígado)",
|
||||||
|
system: "Abdominal",
|
||||||
|
technique: "palpacao",
|
||||||
|
position: [-0.15, 0.82, 0.11],
|
||||||
|
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.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "palpacao-baco",
|
||||||
|
name: "Palpação do Baço",
|
||||||
|
system: "Abdominal",
|
||||||
|
technique: "palpacao",
|
||||||
|
position: [0.15, 0.85, 0.1],
|
||||||
|
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.",
|
||||||
|
},
|
||||||
|
|
||||||
|
// ---- Pulsos periféricos ----
|
||||||
|
{
|
||||||
|
id: "pulso-carotideo",
|
||||||
|
name: "Pulso Carotídeo",
|
||||||
|
system: "Vascular",
|
||||||
|
technique: "palpacao",
|
||||||
|
position: [-0.06, 1.5, 0.08],
|
||||||
|
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.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "pulso-radial",
|
||||||
|
name: "Pulso Radial",
|
||||||
|
system: "Vascular",
|
||||||
|
technique: "palpacao",
|
||||||
|
position: [-0.28, 0.83, 0.03],
|
||||||
|
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.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "pulso-femoral",
|
||||||
|
name: "Pulso Femoral",
|
||||||
|
system: "Vascular",
|
||||||
|
technique: "palpacao",
|
||||||
|
position: [-0.1, 0.62, 0.08],
|
||||||
|
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.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "pulso-pedioso",
|
||||||
|
name: "Pulso Pedioso",
|
||||||
|
system: "Vascular",
|
||||||
|
technique: "palpacao",
|
||||||
|
position: [-0.11, -0.02, 0.06],
|
||||||
|
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.",
|
||||||
|
},
|
||||||
|
];
|
||||||
@@ -0,0 +1,197 @@
|
|||||||
|
import { useMemo, useState } from "react";
|
||||||
|
import { Canvas } from "@react-three/fiber";
|
||||||
|
import { CameraControls, Html } from "@react-three/drei";
|
||||||
|
import { Mannequin } from "../components/viewer/Mannequin.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
|
||||||
|
// de Planos e Eixos, agora com pontos clicáveis mostrando ONDE e COMO se
|
||||||
|
// faz cada manobra (inspeção/palpação/percussão/ausculta) — pensado pra
|
||||||
|
// quem está estudando propedêutica, não pra troca do atlas 3D real (que
|
||||||
|
// seria overkill pra um boneco de pontos de exame, que não muda por
|
||||||
|
// paciente).
|
||||||
|
export default function PhysicalExamPage() {
|
||||||
|
const [activeTechniques, setActiveTechniques] = useState<Set<ExamTechnique>>(
|
||||||
|
() => new Set(TECHNIQUES.map((t) => t.key)),
|
||||||
|
);
|
||||||
|
const [selectedId, setSelectedId] = useState<string | null>(null);
|
||||||
|
const [leftOpen, setLeftOpen] = useState(false);
|
||||||
|
const [rightOpen, setRightOpen] = useState(false);
|
||||||
|
|
||||||
|
function toggleTechnique(key: ExamTechnique) {
|
||||||
|
setActiveTechniques((prev) => {
|
||||||
|
const next = new Set(prev);
|
||||||
|
if (next.has(key)) next.delete(key);
|
||||||
|
else next.add(key);
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const visiblePoints = useMemo(
|
||||||
|
() => EXAM_POINTS.filter((p) => activeTechniques.has(p.technique)),
|
||||||
|
[activeTechniques],
|
||||||
|
);
|
||||||
|
|
||||||
|
const selected = EXAM_POINTS.find((p) => p.id === selectedId) ?? null;
|
||||||
|
const techniqueColor = (t: ExamTechnique) => TECHNIQUES.find((x) => x.key === t)?.color ?? "#fff";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="h-full relative md:grid md:grid-cols-[260px_1fr_320px]">
|
||||||
|
<aside
|
||||||
|
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
|
||||||
|
transition-transform duration-200 md:translate-x-0
|
||||||
|
${leftOpen ? "translate-x-0" : "-translate-x-full"}`}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
onClick={() => setLeftOpen(false)}
|
||||||
|
className="md:hidden float-right text-slate-400 hover:text-white text-sm px-2 py-1"
|
||||||
|
aria-label="Fechar painel"
|
||||||
|
>
|
||||||
|
✕ Fechar
|
||||||
|
</button>
|
||||||
|
<div>
|
||||||
|
<h3 className="text-xs uppercase tracking-wide text-slate-400 mb-2">Técnicas de Exame</h3>
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
{TECHNIQUES.map((t) => (
|
||||||
|
<label key={t.key} className="flex items-center gap-2 text-sm cursor-pointer select-none">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={activeTechniques.has(t.key)}
|
||||||
|
onChange={() => toggleTechnique(t.key)}
|
||||||
|
className="accent-accent"
|
||||||
|
/>
|
||||||
|
<span className="inline-block w-2.5 h-2.5 rounded-full" style={{ backgroundColor: t.color }} />
|
||||||
|
{t.label}
|
||||||
|
</label>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="pt-3 border-t border-white/10">
|
||||||
|
<h3 className="text-xs uppercase tracking-wide text-slate-400 mb-2">Como usar</h3>
|
||||||
|
<p className="text-xs text-slate-400 leading-relaxed">
|
||||||
|
Marque as técnicas que quer estudar e clique nos pontos coloridos
|
||||||
|
sobre o boneco. Cada ponto explica onde encostar, como fazer a
|
||||||
|
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>
|
||||||
|
|
||||||
|
{(leftOpen || rightOpen) && (
|
||||||
|
<div
|
||||||
|
className="fixed inset-0 bg-black/50 z-20 md:hidden"
|
||||||
|
onClick={() => {
|
||||||
|
setLeftOpen(false);
|
||||||
|
setRightOpen(false);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="relative h-full min-h-0">
|
||||||
|
<button
|
||||||
|
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"
|
||||||
|
>
|
||||||
|
☰ Técnicas
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setRightOpen(true)}
|
||||||
|
className="md:hidden absolute top-3 right-3 z-10 bg-panel/90 border border-white/10 rounded-lg px-3 py-2 text-xs shadow-lg"
|
||||||
|
>
|
||||||
|
ℹ️ Detalhes
|
||||||
|
</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>
|
||||||
|
|
||||||
|
<aside
|
||||||
|
className={`fixed md:static inset-y-0 right-0 z-30 w-[86vw] max-w-[320px] md:w-auto
|
||||||
|
border-l border-white/10 bg-panel p-4 overflow-y-auto space-y-3
|
||||||
|
transition-transform duration-200 md:translate-x-0
|
||||||
|
${rightOpen ? "translate-x-0" : "translate-x-full"}`}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
onClick={() => setRightOpen(false)}
|
||||||
|
className="md:hidden text-slate-400 hover:text-white text-sm px-2 py-1"
|
||||||
|
aria-label="Fechar painel"
|
||||||
|
>
|
||||||
|
✕ Fechar
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{!selected ? (
|
||||||
|
<p className="text-sm text-slate-400">
|
||||||
|
Clique em um ponto colorido sobre o boneco pra ver a técnica, o
|
||||||
|
local exato e o que ela avalia.
|
||||||
|
</p>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div>
|
||||||
|
<span
|
||||||
|
className="inline-block text-xs px-2 py-1 rounded-full mb-2"
|
||||||
|
style={{
|
||||||
|
backgroundColor: techniqueColor(selected.technique) + "33",
|
||||||
|
color: techniqueColor(selected.technique),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{TECHNIQUES.find((t) => t.key === selected.technique)?.label}
|
||||||
|
</span>
|
||||||
|
<h2 className="text-lg font-semibold text-white">{selected.name}</h2>
|
||||||
|
<p className="text-xs text-slate-500">{selected.system}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h3 className="text-xs uppercase tracking-wide text-slate-400 mb-1">Como fazer</h3>
|
||||||
|
<p className="text-sm text-slate-300 leading-relaxed">{selected.howTo}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h3 className="text-xs uppercase tracking-wide text-slate-400 mb-1">O que avalia</h3>
|
||||||
|
<p className="text-sm text-slate-300 leading-relaxed">{selected.whatFor}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</aside>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ import { useState } from "react";
|
|||||||
import { Canvas } from "@react-three/fiber";
|
import { Canvas } from "@react-three/fiber";
|
||||||
import { CameraControls, Html, Line } from "@react-three/drei";
|
import { CameraControls, Html, Line } from "@react-three/drei";
|
||||||
import * as THREE from "three";
|
import * as THREE from "three";
|
||||||
|
import { Mannequin } from "../components/viewer/Mannequin.js";
|
||||||
|
|
||||||
interface PlaneDef {
|
interface PlaneDef {
|
||||||
key: "sagittal" | "coronal" | "transverse";
|
key: "sagittal" | "coronal" | "transverse";
|
||||||
@@ -38,42 +39,6 @@ const GLOSSARY: { term: string; def: string }[] = [
|
|||||||
{ term: "Plano Transversal (Axial)", def: "Divide o corpo em partes superior e inferior." },
|
{ term: "Plano Transversal (Axial)", def: "Divide o corpo em partes superior e inferior." },
|
||||||
];
|
];
|
||||||
|
|
||||||
function Mannequin() {
|
|
||||||
const skin = "#8fa3c8";
|
|
||||||
return (
|
|
||||||
<group>
|
|
||||||
<mesh position={[0, 1.65, 0]}>
|
|
||||||
<sphereGeometry args={[0.12]} />
|
|
||||||
<meshStandardMaterial color={skin} />
|
|
||||||
</mesh>
|
|
||||||
<mesh position={[0, 1.2, 0]}>
|
|
||||||
<boxGeometry args={[0.4, 0.55, 0.22]} />
|
|
||||||
<meshStandardMaterial color={skin} />
|
|
||||||
</mesh>
|
|
||||||
<mesh position={[0, 0.75, 0]}>
|
|
||||||
<boxGeometry args={[0.36, 0.3, 0.2]} />
|
|
||||||
<meshStandardMaterial color={skin} />
|
|
||||||
</mesh>
|
|
||||||
{[1, -1].map((side) => (
|
|
||||||
<group key={`arm-${side}`}>
|
|
||||||
<mesh position={[side * 0.28, 1.1, 0]}>
|
|
||||||
<cylinderGeometry args={[0.05, 0.045, 0.55, 10]} />
|
|
||||||
<meshStandardMaterial color={skin} />
|
|
||||||
</mesh>
|
|
||||||
</group>
|
|
||||||
))}
|
|
||||||
{[1, -1].map((side) => (
|
|
||||||
<group key={`leg-${side}`}>
|
|
||||||
<mesh position={[side * 0.11, 0.3, 0]}>
|
|
||||||
<cylinderGeometry args={[0.065, 0.05, 0.65, 10]} />
|
|
||||||
<meshStandardMaterial color={skin} />
|
|
||||||
</mesh>
|
|
||||||
</group>
|
|
||||||
))}
|
|
||||||
</group>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function PlanesPage() {
|
export default function PlanesPage() {
|
||||||
const [visible, setVisible] = useState<Record<PlaneDef["key"], boolean>>({
|
const [visible, setVisible] = useState<Record<PlaneDef["key"], boolean>>({
|
||||||
sagittal: true,
|
sagittal: true,
|
||||||
|
|||||||
Reference in new issue
Block a user