espaco intercostal
This commit is contained in:
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>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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 @@
|
||||
{"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"}
|
||||
Reference in new issue
Block a user