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() {
+