adiciona modelo real

This commit is contained in:
Rodrigo Bertoldo de Sousa Martins committed 2026-09-07 21:30:18 -03:00
1 parent 71d13753c9
commit 3a659a52c7
18 files changed
+612 -176

No files matched your search

+8
View File
@@ -19,6 +19,14 @@ export function Layout() {
Quiz
</NavLink>
</nav>
<a
href="https://github.com/LluisV/Z-Anatomy-Sample"
target="_blank"
rel="noreferrer"
className="ml-auto text-[11px] text-slate-500 hover:text-slate-300"
>
Modelos 3D: projeto Z-Anatomy (CC BY-SA 4.0)
</a>
</header>
<main className="flex-1 min-h-0">
<Outlet />
+27 -8
View File
@@ -75,12 +75,13 @@ export function SearchNav({ structures }: { structures: Structure[] }) {
<details key={system} open={false} className="text-sm">
<summary className="cursor-pointer text-slate-300 hover:text-white">{system}</summary>
<div className="pl-3 mt-1 space-y-2">
{[...regions.entries()].map(([region, items]) => (
<details key={region}>
<summary className="cursor-pointer text-slate-400 hover:text-white text-xs uppercase tracking-wide">
{region}
</summary>
<ul className="pl-3 mt-1 space-y-0.5">
{[...regions.entries()].map(([region, items]) =>
// Estruturas reais (Z-Anatomy) não têm sub-região própria
// — regionPath tem só o nome do sistema, então a região
// aqui seria idêntica ao título acima. Nesse caso lista
// direto, sem um nível de agrupamento redundante.
region === system ? (
<ul key={region} className="space-y-0.5">
{items.map((s) => (
<li key={s.id}>
<button
@@ -92,8 +93,26 @@ export function SearchNav({ structures }: { structures: Structure[] }) {
</li>
))}
</ul>
</details>
))}
) : (
<details key={region}>
<summary className="cursor-pointer text-slate-400 hover:text-white text-xs uppercase tracking-wide">
{region}
</summary>
<ul className="pl-3 mt-1 space-y-0.5">
{items.map((s) => (
<li key={s.id}>
<button
onClick={() => goTo(s)}
className="w-full text-left text-sm rounded px-2 py-1 hover:bg-white/5"
>
{s.namePt}
</button>
</li>
))}
</ul>
</details>
),
)}
</div>
</details>
))}
+22 -2
View File
@@ -63,9 +63,27 @@ export function Scene({ bySlug }: { bySlug: Map<string, Structure> }) {
? [...bySlug.values()].find((s) => s.id === selectedStructureId)?.slug ?? null
: null;
// Um arquivo .glb pode conter estruturas de mais de um sistema (ex: o
// "visceral-organs.glb" tem estruturas de "respiratorio" e "visceral"),
// então carregamos qualquer arquivo que tenha ao menos uma estrutura de
// um sistema visível — a visibilidade por malha é resolvida dentro de
// cada SystemModel.
const structuresByFile = useMemo(() => {
const structuresList = [...bySlug.values()];
const map = new Map<string, Structure[]>();
for (const s of structuresList) {
if (!map.has(s.modelFile)) map.set(s.modelFile, []);
map.get(s.modelFile)!.push(s);
}
return map;
}, [bySlug]);
const modelFiles = useMemo(
() => [...visibleSystems].map((slug) => `${slug}.glb`),
[visibleSystems],
() =>
[...structuresByFile.entries()]
.filter(([, items]) => items.some((s) => visibleSystems.has(s.system.slug)))
.map(([file]) => file),
[structuresByFile, visibleSystems],
);
return (
@@ -83,6 +101,8 @@ export function Scene({ bySlug }: { bySlug: Map<string, Structure> }) {
<SystemModel
key={modelFile}
modelFile={modelFile}
structuresInFile={structuresByFile.get(modelFile) ?? []}
visibleSystems={visibleSystems}
selectedSlug={selectedSlug}
clippingPlanes={clippingPlanes}
onSelect={(slug) => {
+45 -8
View File
@@ -1,46 +1,83 @@
import { useEffect, useRef } from "react";
import { useEffect, useMemo, useRef } from "react";
import { useGLTF } from "@react-three/drei";
import * as THREE from "three";
import type { ThreeEvent } from "@react-three/fiber";
import { modelUrl } from "../../lib/api.js";
import { modelUrl, type Structure } from "../../lib/api.js";
interface Props {
modelFile: string;
// Estruturas do banco cujo modelFile é este arquivo — pode conter mais de
// um sistema (ex: o glb "visceral-organs" tem estruturas de "respiratorio"
// e de "visceral" ao mesmo tempo).
structuresInFile: Structure[];
visibleSystems: Set<string>;
selectedSlug: string | null;
onSelect: (slug: string) => void;
clippingPlanes: THREE.Plane[];
}
const HIGHLIGHT = new THREE.Color("#22d3ee");
const BLACK = new THREE.Color(0x000000);
export function SystemModel({ modelFile, selectedSlug, onSelect, clippingPlanes }: Props) {
export function SystemModel({
modelFile,
structuresInFile,
visibleSystems,
selectedSlug,
onSelect,
clippingPlanes,
}: Props) {
const { scene } = useGLTF(modelUrl(modelFile));
const clonedRef = useRef<THREE.Group>();
// Nome original do nó (como veio do .glb), capturado uma única vez no
// clone — necessário porque o efeito abaixo reescreve mesh.name para o
// slug da estrutura, e precisa continuar encontrando a estrutura certa
// em execuções seguintes (toggle de camadas, seleção, corte etc.).
const originalNames = useRef<WeakMap<THREE.Object3D, string>>(new WeakMap());
if (!clonedRef.current) {
const clone = scene.clone(true);
clone.traverse((obj) => {
const mesh = obj as THREE.Mesh;
if (mesh.isMesh && mesh.material) {
mesh.material = (mesh.material as THREE.MeshStandardMaterial).clone();
if (mesh.isMesh) {
originalNames.current.set(mesh, mesh.name);
if (mesh.material) {
mesh.material = (mesh.material as THREE.MeshStandardMaterial).clone();
}
}
});
clonedRef.current = clone;
}
const root = clonedRef.current!;
const byMeshName = useMemo(
() => new Map(structuresInFile.map((s) => [s.meshName, s])),
[structuresInFile],
);
useEffect(() => {
root.traverse((obj) => {
const mesh = obj as THREE.Mesh;
if (!mesh.isMesh) return;
const originalName = originalNames.current.get(mesh) ?? mesh.name;
const structure = byMeshName.get(originalName);
if (!structure) {
// Malha sem correspondência no manifesto (não catalogada) — oculta.
mesh.visible = false;
return;
}
mesh.name = structure.slug;
mesh.visible = visibleSystems.has(structure.system.slug);
const mat = mesh.material as THREE.MeshStandardMaterial;
mat.clippingPlanes = clippingPlanes;
mat.clipShadows = true;
const isSelected = mesh.name === selectedSlug;
mat.emissive = isSelected ? HIGHLIGHT : new THREE.Color(0x000000);
const isSelected = structure.slug === selectedSlug;
mat.emissive = isSelected ? HIGHLIGHT : BLACK;
mat.emissiveIntensity = isSelected ? 0.6 : 0;
});
}, [root, selectedSlug, clippingPlanes]);
}, [root, byMeshName, visibleSystems, selectedSlug, clippingPlanes]);
const handleClick = (e: ThreeEvent<MouseEvent>) => {
e.stopPropagation();