diff --git a/frontend/src/components/QuickRegionNav.tsx b/frontend/src/components/QuickRegionNav.tsx new file mode 100644 index 0000000..ef7badd --- /dev/null +++ b/frontend/src/components/QuickRegionNav.tsx @@ -0,0 +1,33 @@ +import { useAtlasStore, type BodyRegion } from "../store/useAtlasStore.js"; + +const REGIONS: { region: BodyRegion; label: string }[] = [ + { region: "cabeca", label: "Cabeça" }, + { region: "pescoco", label: "Pescoço" }, + { region: "torax", label: "Tórax" }, + { region: "abdome", label: "Abdome" }, + { region: "pelve", label: "Pelve" }, + { region: "membros_superiores", label: "Membros Sup." }, + { region: "membros_inferiores", label: "Membros Inf." }, + { region: "corpo_inteiro", label: "Corpo Inteiro" }, +]; + +// Navegação rápida por região — em vez de precisar acertar o clique numa +// estrutura minúscula pra girar/aproximar a câmera manualmente, o usuário +// pede direto pra "ir" numa região do corpo. +export function QuickRegionNav() { + const focusOnRegion = useAtlasStore((s) => s.focusOnRegion); + + return ( +
+ {REGIONS.map((r) => ( + + ))} +
+ ); +} diff --git a/frontend/src/components/viewer/Scene.tsx b/frontend/src/components/viewer/Scene.tsx index f6b15ea..fbc0d63 100644 --- a/frontend/src/components/viewer/Scene.tsx +++ b/frontend/src/components/viewer/Scene.tsx @@ -2,7 +2,7 @@ import { Suspense, useEffect, useMemo, useRef } from "react"; import { Canvas, useThree } from "@react-three/fiber"; import { CameraControls, Grid } from "@react-three/drei"; import * as THREE from "three"; -import { useAtlasStore, type ClipAxis } from "../../store/useAtlasStore.js"; +import { useAtlasStore, type BodyRegion, type ClipAxis } from "../../store/useAtlasStore.js"; import { SystemModel } from "./SystemModel.js"; import type { Structure } from "../../lib/api.js"; @@ -47,10 +47,36 @@ function computeVisibleBoundingBox(root: THREE.Object3D): THREE.Box3 { return union; } +// Faixas aproximadas de altura (0 = pés, 1 = topo da cabeça) usadas pela +// navegação rápida por região — proporções corporais padrão de um adulto +// em pé. Regiões dos membros usam a largura toda (os braços ficam nas +// bordas laterais dessa faixa de altura). +const BODY_REGION_Y_RANGES: Record, [number, number]> = { + cabeca: [0.85, 1.0], + pescoco: [0.8, 0.87], + torax: [0.62, 0.83], + abdome: [0.52, 0.65], + pelve: [0.42, 0.55], + membros_superiores: [0.55, 0.88], + membros_inferiores: [0.0, 0.45], +}; + +function regionSubBox(total: THREE.Box3, region: BodyRegion): THREE.Box3 { + if (region === "corpo_inteiro") return total; + const [t0, t1] = BODY_REGION_Y_RANGES[region]; + const yMin = THREE.MathUtils.lerp(total.min.y, total.max.y, t0); + const yMax = THREE.MathUtils.lerp(total.min.y, total.max.y, t1); + return new THREE.Box3( + new THREE.Vector3(total.min.x, yMin, total.min.z), + new THREE.Vector3(total.max.x, yMax, total.max.z), + ); +} + function CameraController({ modelFiles }: { modelFiles: string[] }) { const controlsRef = useRef(null); const { scene, camera } = useThree(); const focusRequest = useAtlasStore((s) => s.focusRequest); + const regionFocusRequest = useAtlasStore((s) => s.regionFocusRequest); // Malhas reais (Z-Anatomy) não seguem a mesma escala/origem que os // modelos esquemáticos antigos (podem vir em cm, ou centralizadas em @@ -93,6 +119,20 @@ function CameraController({ modelFiles }: { modelFiles: string[] }) { }); }, [focusRequest, scene]); + useEffect(() => { + if (!regionFocusRequest || !controlsRef.current) return; + const total = computeVisibleBoundingBox(scene); + if (total.isEmpty()) return; + const box = regionSubBox(total, regionFocusRequest.region); + if (box.isEmpty()) return; + controlsRef.current.fitToBox(box, true, { + paddingLeft: 0.3, + paddingRight: 0.3, + paddingTop: 0.3, + paddingBottom: 0.3, + }); + }, [regionFocusRequest, scene]); + return ( s.visibleSystems); @@ -22,6 +23,7 @@ export default function AtlasPage() {
+