From bd0761809755e41d00804f80515db8e6b48ecc03 Mon Sep 17 00:00:00 2001 From: Rodrigo Bertoldo de Sousa Martins Date: Tue, 22 Sep 2026 11:05:45 -0300 Subject: [PATCH] espaco intercostal --- .../components/viewer/IntercostalLabels.tsx | 113 ++++++++++++++++++ frontend/src/components/viewer/Scene.tsx | 2 + frontend/tsconfig.tsbuildinfo | 2 +- 3 files changed, 116 insertions(+), 1 deletion(-) create mode 100644 frontend/src/components/viewer/IntercostalLabels.tsx diff --git a/frontend/src/components/viewer/IntercostalLabels.tsx b/frontend/src/components/viewer/IntercostalLabels.tsx new file mode 100644 index 0000000..a535bb5 --- /dev/null +++ b/frontend/src/components/viewer/IntercostalLabels.tsx @@ -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 }) { + 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>(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(); + 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 ( + + + + ); + })} + + ); +} diff --git a/frontend/src/components/viewer/Scene.tsx b/frontend/src/components/viewer/Scene.tsx index 55617e5..38cfc9b 100644 --- a/frontend/src/components/viewer/Scene.tsx +++ b/frontend/src/components/viewer/Scene.tsx @@ -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 /> + {children} ); diff --git a/frontend/tsconfig.tsbuildinfo b/frontend/tsconfig.tsbuildinfo index 366a96b..04039d7 100644 --- a/frontend/tsconfig.tsbuildinfo +++ b/frontend/tsconfig.tsbuildinfo @@ -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"} \ No newline at end of file +{"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"} \ No newline at end of file