espaco intercostal

This commit is contained in:
Rodrigo Bertoldo de Sousa Martins committed 2026-09-22 11:05:45 -03:00
1 parent a55e8c50df
commit bd07618097
3 files changed
+116 -1

No files matched your search

@@ -0,0 +1,113 @@
import { useEffect, useMemo, 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 { Structure } from "../../lib/api.js";
import { computeVisibleBoundingBox } from "../../lib/bodyRegionBox.js";
// O pipeline nomeia cada uma das 11 instâncias reais do músculo intercostal
// externo (por lado) com o espaço que ocupa — ver INTERCOSTAL_MUSCLE_BASES
// em extract-manifest.mjs — algo como "Intercostais Externos Direito — 3º
// Espaço Intercostal". Aqui só usamos essa estrutura JÁ REAL como âncora de
// posição (pega a caixa delimitadora dela na cena carregada) pra desenhar
// uma etiqueta numerada flutuante — não inventamos posição nenhuma.
const LABEL_RE = /^Intercostais Externos (Direito|Esquerdo) — (\d+)º Espaço Intercostal$/;
interface LabelInfo {
structure: Structure;
side: "Direito" | "Esquerdo";
number: number;
}
export function IntercostalLabels({ bySlug }: { bySlug: Map<string, Structure> }) {
const { scene } = useThree();
const visibleSystems = useAtlasStore((s) => s.visibleSystems);
const selectStructure = useAtlasStore((s) => s.selectStructure);
const selectedStructureId = useAtlasStore((s) => s.selectedStructureId);
const [positions, setPositions] = useState<Map<string, THREE.Vector3>>(new Map());
const labelInfos = useMemo(() => {
const list: LabelInfo[] = [];
for (const structure of bySlug.values()) {
const m = structure.namePt.match(LABEL_RE);
if (!m) continue;
list.push({ structure, side: m[1] as "Direito" | "Esquerdo", number: Number(m[2]) });
}
return list;
}, [bySlug]);
// Só faz sentido mostrar com o Sistema Muscular ligado — é o arquivo que
// contém essas malhas; se estiver desligado, o arquivo nem é carregado
// na cena (ver Scene.tsx), então não haveria nada pra ancorar mesmo.
const active = visibleSystems.has("muscular") && labelInfos.length > 0;
// O carregamento do .glb é assíncrono — igual ao CameraController de
// Scene.tsx, tenta de novo a cada 200ms até achar todas as âncoras (ou
// desistir depois de várias tentativas).
useEffect(() => {
if (!active) {
setPositions(new Map());
return;
}
let cancelled = false;
let attempts = 0;
function tryLocate() {
if (cancelled) return;
const next = new Map<string, THREE.Vector3>();
for (const info of labelInfos) {
const obj = scene.getObjectByName(info.structure.slug);
if (!obj) continue;
const box = computeVisibleBoundingBox(obj);
if (box.isEmpty()) continue;
next.set(info.structure.slug, box.getCenter(new THREE.Vector3()));
}
attempts += 1;
if (next.size < labelInfos.length && attempts < 50) {
setTimeout(tryLocate, 200);
return;
}
if (!cancelled) setPositions(next);
}
tryLocate();
return () => {
cancelled = true;
};
}, [active, labelInfos, scene]);
if (!active) return null;
return (
<>
{labelInfos.map((info) => {
const pos = positions.get(info.structure.slug);
if (!pos) return null;
const isSelected = info.structure.id === selectedStructureId;
return (
<Html key={info.structure.slug} position={pos} center distanceFactor={2.5} zIndexRange={[10, 0]}>
<button
onClick={(e) => {
e.stopPropagation();
selectStructure(info.structure.id);
}}
title={info.structure.namePt}
className="flex items-center justify-center rounded-full text-[10px] font-semibold leading-none transition-transform hover:scale-110"
style={{
width: 18,
height: 18,
background: isSelected ? "#22d3ee" : "rgba(15,23,42,0.85)",
color: isSelected ? "#0b1220" : "#e2e8f0",
border: "1px solid rgba(226,232,240,0.5)",
cursor: "pointer",
}}
>
{info.number}
</button>
</Html>
);
})}
</>
);
}
+2
View File
@@ -4,6 +4,7 @@ import { CameraControls, Grid } from "@react-three/drei";
import * as THREE from "three";
import { useAtlasStore, type ClipAxis } from "../../store/useAtlasStore.js";
import { SystemModel } from "./SystemModel.js";
import { IntercostalLabels } from "./IntercostalLabels.js";
import type { Structure } from "../../lib/api.js";
import { computeVisibleBoundingBox, regionSubBox } from "../../lib/bodyRegionBox.js";
@@ -332,6 +333,7 @@ export function Scene({
infiniteGrid
/>
<CameraController modelFiles={modelFiles} />
<IntercostalLabels bySlug={bySlug} />
{children}
</Canvas>
);
+1 -1
View File
@@ -1 +1 @@
{"root":["./src/app.tsx","./src/main.tsx","./src/vite-env.d.ts","./src/components/celldiagram3d.tsx","./src/components/clippingcontrols.tsx","./src/components/cookieconsent.tsx","./src/components/feedbackwidget.tsx","./src/components/layertoggle.tsx","./src/components/layout.tsx","./src/components/microscopydiagram.tsx","./src/components/onlinecounter.tsx","./src/components/quickregionnav.tsx","./src/components/searchnav.tsx","./src/components/skindiagram3d.tsx","./src/components/structureinfopanel.tsx","./src/components/celldivision/celldivisionscene.tsx","./src/components/viewer/exammarkers.tsx","./src/components/viewer/mannequin.tsx","./src/components/viewer/scene.tsx","./src/components/viewer/systemmodel.tsx","./src/data/celldivisioncomponents.ts","./src/data/celldivisionstages.ts","./src/data/geneticsyndromes.ts","./src/data/microscopycredits.ts","./src/data/microscopyhotspots.ts","./src/data/musclereferenceimages.ts","./src/data/physicalexam.ts","./src/hooks/usestructures.ts","./src/hooks/usesystemstructures.ts","./src/lib/analytics.ts","./src/lib/api.ts","./src/lib/bodyregionbox.ts","./src/routes/adminstatspage.tsx","./src/routes/atlaspage.tsx","./src/routes/celldivisionpage.tsx","./src/routes/physicalexampage.tsx","./src/routes/planespage.tsx","./src/routes/quizpage.tsx","./src/store/useatlasstore.ts"],"version":"5.9.3"}
{"root":["./src/app.tsx","./src/main.tsx","./src/vite-env.d.ts","./src/components/celldiagram3d.tsx","./src/components/clippingcontrols.tsx","./src/components/cookieconsent.tsx","./src/components/feedbackwidget.tsx","./src/components/layertoggle.tsx","./src/components/layout.tsx","./src/components/microscopydiagram.tsx","./src/components/onlinecounter.tsx","./src/components/quickregionnav.tsx","./src/components/searchnav.tsx","./src/components/skindiagram3d.tsx","./src/components/structureinfopanel.tsx","./src/components/celldivision/celldivisionscene.tsx","./src/components/viewer/exammarkers.tsx","./src/components/viewer/intercostallabels.tsx","./src/components/viewer/mannequin.tsx","./src/components/viewer/scene.tsx","./src/components/viewer/systemmodel.tsx","./src/data/celldivisioncomponents.ts","./src/data/celldivisionstages.ts","./src/data/geneticsyndromes.ts","./src/data/microscopycredits.ts","./src/data/microscopyhotspots.ts","./src/data/musclereferenceimages.ts","./src/data/physicalexam.ts","./src/hooks/usestructures.ts","./src/hooks/usesystemstructures.ts","./src/lib/analytics.ts","./src/lib/api.ts","./src/lib/bodyregionbox.ts","./src/routes/adminstatspage.tsx","./src/routes/atlaspage.tsx","./src/routes/celldivisionpage.tsx","./src/routes/physicalexampage.tsx","./src/routes/planespage.tsx","./src/routes/quizpage.tsx","./src/store/useatlasstore.ts"],"version":"5.9.3"}