planos e eixos camera
Signed-off-by: Rodrigo Bertoldo de Sousa Martins <rodrigo@martinsevieira.adv.br>
This commit is contained in:
1 parent
1d875ace2c
commit
64793922af
5 files changed
+103
-52
No files matched your search
@@ -68,6 +68,12 @@ export function Mannequin() {
|
|||||||
box.getCenter(center);
|
box.getCenter(center);
|
||||||
|
|
||||||
const group = new THREE.Group();
|
const group = new THREE.Group();
|
||||||
|
// Nome fixo pra PlanesPage.tsx conseguir achar exatamente este objeto
|
||||||
|
// via `scene.getObjectByName(...)` e calcular a caixa delimitadora só
|
||||||
|
// dele — sem isso, o bounding box da cena inteira incluiria os planos
|
||||||
|
// de corte e as setas decorativas, estragando o enquadramento por
|
||||||
|
// região (Cabeça, Tórax...).
|
||||||
|
group.name = "mannequin-body";
|
||||||
group.add(clone);
|
group.add(clone);
|
||||||
// Centraliza em X/Z e põe o pé no chão (y=0) ANTES de escalar, depois
|
// Centraliza em X/Z e põe o pé no chão (y=0) ANTES de escalar, depois
|
||||||
// aplica a escala no grupo pai — assim o pivô da escala é a origem já
|
// aplica a escala no grupo pai — assim o pivô da escala é a origem já
|
||||||
|
|||||||
@@ -2,9 +2,10 @@ import { Suspense, useEffect, useMemo, useRef, type ReactNode } from "react";
|
|||||||
import { Canvas, useThree } from "@react-three/fiber";
|
import { Canvas, useThree } from "@react-three/fiber";
|
||||||
import { CameraControls, Grid } from "@react-three/drei";
|
import { CameraControls, Grid } from "@react-three/drei";
|
||||||
import * as THREE from "three";
|
import * as THREE from "three";
|
||||||
import { useAtlasStore, type BodyRegion, type ClipAxis } from "../../store/useAtlasStore.js";
|
import { useAtlasStore, type ClipAxis } from "../../store/useAtlasStore.js";
|
||||||
import { SystemModel } from "./SystemModel.js";
|
import { SystemModel } from "./SystemModel.js";
|
||||||
import type { Structure } from "../../lib/api.js";
|
import type { Structure } from "../../lib/api.js";
|
||||||
|
import { computeVisibleBoundingBox, regionSubBox } from "../../lib/bodyRegionBox.js";
|
||||||
|
|
||||||
// Sistemas sem .glb pra carregar — tegumentar/histologia são
|
// Sistemas sem .glb pra carregar — tegumentar/histologia são
|
||||||
// fotomicrografias 2D reais com hotspots (ver SkinDiagram.tsx e
|
// fotomicrografias 2D reais com hotspots (ver SkinDiagram.tsx e
|
||||||
@@ -56,53 +57,6 @@ function useClippingPlanes(): THREE.Plane[] {
|
|||||||
}, [enabled, axis, position, tiltX, tiltZ, contentBox]);
|
}, [enabled, axis, position, tiltX, tiltZ, contentBox]);
|
||||||
}
|
}
|
||||||
|
|
||||||
function isFiniteVec3(v: THREE.Vector3): boolean {
|
|
||||||
return Number.isFinite(v.x) && Number.isFinite(v.y) && Number.isFinite(v.z);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Une o bounding box só das malhas VISÍVEIS e com geometria válida —
|
|
||||||
// alguns objetos do dataset real (anotações/curvas do Z-Anatomy) podem ter
|
|
||||||
// vértices degenerados/NaN, e Box3.setFromObject(scene) ingenuamente
|
|
||||||
// incluiria isso, distorcendo o enquadramento (ou escondendo tudo o mais
|
|
||||||
// fora do "zoom" resultante).
|
|
||||||
function computeVisibleBoundingBox(root: THREE.Object3D): THREE.Box3 {
|
|
||||||
const union = new THREE.Box3();
|
|
||||||
const tmp = new THREE.Box3();
|
|
||||||
root.traverse((obj) => {
|
|
||||||
const mesh = obj as THREE.Mesh;
|
|
||||||
if (!mesh.isMesh || !mesh.visible) return;
|
|
||||||
tmp.setFromObject(mesh);
|
|
||||||
if (tmp.isEmpty()) return;
|
|
||||||
if (!isFiniteVec3(tmp.min) || !isFiniteVec3(tmp.max)) return;
|
|
||||||
union.union(tmp);
|
|
||||||
});
|
|
||||||
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<Exclude<BodyRegion, "corpo_inteiro">, [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[] }) {
|
function CameraController({ modelFiles }: { modelFiles: string[] }) {
|
||||||
const controlsRef = useRef<CameraControls>(null);
|
const controlsRef = useRef<CameraControls>(null);
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
import * as THREE from "three";
|
||||||
|
import type { BodyRegion } from "../store/useAtlasStore.js";
|
||||||
|
|
||||||
|
// Extraído de Scene.tsx pra ser reaproveitado também em PlanesPage.tsx —
|
||||||
|
// mesma lógica de enquadrar a câmera numa região do corpo (Cabeça,
|
||||||
|
// Pescoço, Tórax...), agora usada tanto no Atlas/Exame Físico (malha real
|
||||||
|
// do Z-Anatomy) quanto em Planos e Eixos (o boneco de Regiões Anatômicas).
|
||||||
|
|
||||||
|
function isFiniteVec3(v: THREE.Vector3): boolean {
|
||||||
|
return Number.isFinite(v.x) && Number.isFinite(v.y) && Number.isFinite(v.z);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Une o bounding box só das malhas VISÍVEIS e com geometria válida —
|
||||||
|
// alguns objetos do dataset real (anotações/curvas do Z-Anatomy) podem ter
|
||||||
|
// vértices degenerados/NaN, e Box3.setFromObject(scene) ingenuamente
|
||||||
|
// incluiria isso, distorcendo o enquadramento (ou escondendo tudo o mais
|
||||||
|
// fora do "zoom" resultante).
|
||||||
|
export function computeVisibleBoundingBox(root: THREE.Object3D): THREE.Box3 {
|
||||||
|
const union = new THREE.Box3();
|
||||||
|
const tmp = new THREE.Box3();
|
||||||
|
root.traverse((obj) => {
|
||||||
|
const mesh = obj as THREE.Mesh;
|
||||||
|
if (!mesh.isMesh || !mesh.visible) return;
|
||||||
|
tmp.setFromObject(mesh);
|
||||||
|
if (tmp.isEmpty()) return;
|
||||||
|
if (!isFiniteVec3(tmp.min) || !isFiniteVec3(tmp.max)) return;
|
||||||
|
union.union(tmp);
|
||||||
|
});
|
||||||
|
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).
|
||||||
|
export const BODY_REGION_Y_RANGES: Record<Exclude<BodyRegion, "corpo_inteiro">, [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],
|
||||||
|
};
|
||||||
|
|
||||||
|
export 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),
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,8 +1,11 @@
|
|||||||
import { useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import { Canvas } from "@react-three/fiber";
|
import { Canvas, useThree } from "@react-three/fiber";
|
||||||
import { CameraControls, Html, Line } from "@react-three/drei";
|
import { CameraControls, Html, Line } from "@react-three/drei";
|
||||||
import * as THREE from "three";
|
import * as THREE from "three";
|
||||||
import { Mannequin } from "../components/viewer/Mannequin.js";
|
import { Mannequin } from "../components/viewer/Mannequin.js";
|
||||||
|
import { QuickRegionNav } from "../components/QuickRegionNav.js";
|
||||||
|
import { useAtlasStore } from "../store/useAtlasStore.js";
|
||||||
|
import { computeVisibleBoundingBox, regionSubBox } from "../lib/bodyRegionBox.js";
|
||||||
|
|
||||||
interface PlaneDef {
|
interface PlaneDef {
|
||||||
key: "sagittal" | "coronal" | "transverse";
|
key: "sagittal" | "coronal" | "transverse";
|
||||||
@@ -135,6 +138,38 @@ const GLOSSARY_GROUPS: GlossaryGroup[] = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// Mesma navegação rápida por região do Atlas/Exame Físico (QuickRegionNav
|
||||||
|
// já existente, botões "Cabeça", "Tórax" etc.) — aqui aplicada só ao
|
||||||
|
// boneco (procura o objeto pelo nome fixo `mannequin-body`, ver
|
||||||
|
// Mannequin.tsx, pra não misturar no bounding box os planos de corte e as
|
||||||
|
// setas decorativas da cena).
|
||||||
|
function RegionCameraController() {
|
||||||
|
const controlsRef = useRef<CameraControls>(null);
|
||||||
|
const { scene } = useThree();
|
||||||
|
const regionFocusRequest = useAtlasStore((s) => s.regionFocusRequest);
|
||||||
|
const seenNonce = useRef(regionFocusRequest?.nonce);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!regionFocusRequest || !controlsRef.current) return;
|
||||||
|
if (regionFocusRequest.nonce === seenNonce.current) return;
|
||||||
|
seenNonce.current = regionFocusRequest.nonce;
|
||||||
|
const body = scene.getObjectByName("mannequin-body");
|
||||||
|
if (!body) return;
|
||||||
|
const total = computeVisibleBoundingBox(body);
|
||||||
|
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 <CameraControls ref={controlsRef} makeDefault />;
|
||||||
|
}
|
||||||
|
|
||||||
export default function PlanesPage() {
|
export default function PlanesPage() {
|
||||||
const [visible, setVisible] = useState<Record<PlaneDef["key"], boolean>>({
|
const [visible, setVisible] = useState<Record<PlaneDef["key"], boolean>>({
|
||||||
sagittal: true,
|
sagittal: true,
|
||||||
@@ -259,8 +294,9 @@ export default function PlanesPage() {
|
|||||||
)),
|
)),
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<CameraControls makeDefault />
|
<RegionCameraController />
|
||||||
</Canvas>
|
</Canvas>
|
||||||
|
<QuickRegionNav />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<aside
|
<aside
|
||||||
|
|||||||
@@ -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/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/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