adiciona modelo real
This commit is contained in:
1 parent
71d13753c9
commit
3a659a52c7
18 files changed
+612
-176
No files matched your search
@@ -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 />
|
||||
|
||||
@@ -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>
|
||||
))}
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in new issue
Block a user