diff --git a/data-pipeline/Dockerfile b/data-pipeline/Dockerfile index 3b2ac29..b3b2175 100644 --- a/data-pipeline/Dockerfile +++ b/data-pipeline/Dockerfile @@ -3,17 +3,8 @@ # Alpine essa dependência é bem mais dolorosa de resolver. FROM node:20-bookworm RUN apt-get update \ - && apt-get install -y --no-install-recommends assimp-utils ca-certificates python3 python3-venv \ + && apt-get install -y --no-install-recommends assimp-utils ca-certificates \ && rm -rf /var/lib/apt/lists/* - -# Ambiente Python isolado só pra buscar as organelas reais da célula -# (fetch-cell-organelles.py) — cloud-volume/trimesh não têm nada a ver com -# o resto do pipeline (100% Node/glTF-transform), então ficam num venv -# próprio em vez de poluir o Python do sistema. -RUN python3 -m venv /opt/venv-cell \ - && /opt/venv-cell/bin/pip install --no-cache-dir --upgrade pip \ - && /opt/venv-cell/bin/pip install --no-cache-dir cloud-volume trimesh - WORKDIR /app COPY package.json ./ RUN npm install diff --git a/data-pipeline/package.json b/data-pipeline/package.json index 971ad45..aaa7bb2 100644 --- a/data-pipeline/package.json +++ b/data-pipeline/package.json @@ -9,12 +9,10 @@ "extract:manifest": "node scripts/extract-manifest.mjs", "build:tegumentar": "node seed/build-manifest.mjs", "build:microscopy": "node seed/build-manifest-microscopy.mjs", - "cell:fetch": "/opt/venv-cell/bin/python scripts/fetch-cell-organelles.py", - "cell:model": "node scripts/build-cell-model.mjs", - "cell:manifest": "node seed/build-manifest-cell.mjs", + "build:cell": "node seed/build-manifest-cell.mjs", "merge:manifest": "node scripts/merge-manifests.mjs", "seed:db": "node scripts/seed-db.mjs", - "run:all": "npm run fetch:models && npm run convert:models && npm run extract:manifest && npm run build:tegumentar && npm run build:microscopy && npm run cell:fetch && npm run cell:model && npm run cell:manifest && npm run merge:manifest && npm run seed:db" + "run:all": "npm run fetch:models && npm run convert:models && npm run extract:manifest && npm run build:tegumentar && npm run build:microscopy && npm run build:cell && npm run merge:manifest && npm run seed:db" }, "dependencies": { "@gltf-transform/core": "^4.0.4", diff --git a/data-pipeline/scripts/build-cell-model.mjs b/data-pipeline/scripts/build-cell-model.mjs deleted file mode 100644 index 040b26e..0000000 --- a/data-pipeline/scripts/build-cell-model.mjs +++ /dev/null @@ -1,56 +0,0 @@ -// Lê citologia-raw.glb (gerado por fetch-cell-organelles.py a partir de -// dados REAIS de FIB-SEM do Janelia OpenOrganelle/CellMap — ver aquele -// script para a curadoria completa e a licença CC BY 4.0), simplifica e -// comprime com o MESMO recipe já usado pras malhas do Z-Anatomy em -// extract-manifest.mjs, e salva o resultado final em seed/models/citologia.glb. -// -// Diferente do Z-Anatomy, aqui não precisamos marcar nós excluídos nem -// resolver nomes duplicados — o .glb de entrada já tem exatamente 5 nós, -// um por organela, com o nome final (ex: "celula-nucleo") definido no -// próprio script Python. weld/simplify/dedup/meshopt só afetam a -// geometria, não o nome dos nós, então o "meshName" no manifesto -// (build-manifest-cell.mjs) pode confiar que bate exatamente com isso. -import { NodeIO } from "@gltf-transform/core"; -import { ALL_EXTENSIONS } from "@gltf-transform/extensions"; -import { prune, dedup, weld, simplify, meshopt } from "@gltf-transform/functions"; -import { MeshoptSimplifier, MeshoptEncoder } from "meshoptimizer"; -import path from "node:path"; -import { fileURLToPath } from "node:url"; -import { mkdirSync } from "node:fs"; - -await MeshoptSimplifier.ready; -await MeshoptEncoder.ready; - -const __dirname = path.dirname(fileURLToPath(import.meta.url)); -const IN_PATH = path.resolve(__dirname, "../converted/citologia/citologia-raw.glb"); -const OUT_DIR = path.resolve(__dirname, "../seed/models"); -mkdirSync(OUT_DIR, { recursive: true }); - -const io = new NodeIO() - .registerExtensions(ALL_EXTENSIONS) - .registerDependencies({ "meshopt.encoder": MeshoptEncoder }); - -console.log(`Lendo ${IN_PATH}...`); -const doc = await io.read(IN_PATH); - -const meshNames = doc - .getRoot() - .listNodes() - .map((n) => n.getName()) - .filter(Boolean); -console.log(`Organelas encontradas: ${meshNames.join(", ")}`); - -await doc.transform( - weld(), - // As organelas maiores (membrana, RE) chegam com bem mais geometria do - // que o necessário pra estudo interativo — mesmo ratio conservador já - // usado no resto do atlas. - simplify({ simplifier: MeshoptSimplifier, ratio: 0.5, error: 0.01 }), - prune(), - dedup(), - meshopt({ encoder: MeshoptEncoder, level: "medium" }), -); - -const outPath = path.join(OUT_DIR, "citologia.glb"); -await io.write(outPath, doc); -console.log(`✓ ${outPath} salvo.`); diff --git a/data-pipeline/scripts/fetch-cell-organelles.py b/data-pipeline/scripts/fetch-cell-organelles.py deleted file mode 100644 index 74c1a48..0000000 --- a/data-pipeline/scripts/fetch-cell-organelles.py +++ /dev/null @@ -1,126 +0,0 @@ -#!/usr/bin/env python3 -# Busca organelas REAIS de uma célula HeLa inteira, reconstruída em 3D a -# partir de FIB-SEM (microscopia eletrônica volumétrica) e segmentada por -# machine learning pelo projeto CellMap/COSEM do Janelia Research Campus -# (HHMI) — publicado como OpenOrganelle, CC BY 4.0. -# https://openorganelle.janelia.org/ | dataset "jrc_hela-2" -# DOI da coleção: https://doi.org/10.25378/janelia.c.7456966 -# -# POR QUE ISSO E NÃO GEOMETRIA PROCEDURAL: é o mesmo princípio já usado pro -# resto do atlas (malhas reais do Z-Anatomy) — aqui não existe um "Z-Anatomy -# da célula" pronto pra converter de FBX, mas o Janelia publica a segmentação -# 3D real como malhas no formato Neuroglancer "precomputed" (multi-resolução, -# comprimido com Draco). Este script usa a biblioteca `cloud-volume` (Seung -# Lab) pra baixar e decodificar essas malhas direto do bucket público, sem -# fabricar nenhuma forma. -# -# Escolhas de curadoria (documentadas pra poder ajustar depois): -# - Cada organela é uma "camada" de segmentação própria no dataset -# (nucleus_seg, mito_seg, er_seg, golgi_seg, pm_seg), com um ID por -# instância. Pegamos a maior instância de núcleo/membrana (a célula -# mais completa no campo de visão) e TODAS as instâncias de -# mitocôndria/RE/Golgi (são realmente múltiplas organelas de verdade, -# não fragmentos de ruído — juntar todas reproduz a distribuição real). -# - LOD (nível de detalhe) escolhido por organela: mais fino pra -# estruturas pequenas/poucas (núcleo, Golgi), mais grosso pras que têm -# MUITA geometria agregada (mitocôndrias, RE) — o pipeline Node.js -# (build-cell-model.mjs) simplifica/comprime tudo de qualquer forma. -# - Todas as peças são recentralizadas usando o MESMO deslocamento (o -# centro da membrana plasmática) — cada organela NÃO pode ser -# recentralizada individualmente, isso destruiria a posição relativa -# real dentro da célula. -# - Escala: nanômetros (unidade nativa do dataset) → micrômetros (÷1000), -# só por conveniência numérica — a câmera do app se ajusta ao tamanho -# real do conteúdo de qualquer forma (ver Scene.tsx/CameraController). -# -# Vesículas (~40 mil instâncias) e endossomos (~5.600) ficaram de fora -# desta primeira versão por volume de dados — dá pra adicionar depois como -# mais uma organela seguindo exatamente o mesmo padrão abaixo. -import os -import time - -import numpy as np -import cloudvolume as cv -import trimesh - -DATASET = "jrc_hela-2" -BASE = f"precomputed://https://janelia-cosem-datasets.s3.amazonaws.com/{DATASET}/neuroglancer/mesh" -OUT_DIR = os.path.join(os.path.dirname(__file__), "..", "converted", "citologia") - -# (nome do nó no glb, camada de segmentação, IDs de instância, LOD) -GROUPS = [ - ("celula-membrana-plasmatica", "pm_seg", [2], 4), - ("celula-nucleo", "nucleus_seg", [1], 3), - ("celula-reticulo-endoplasmatico", "er_seg", list(range(1, 25)), 4), - ("celula-complexo-de-golgi", "golgi_seg", list(range(1, 9)), 3), - ("celula-mitocondrias", "mito_seg", list(range(1, 422)), 4), -] - -SCALE_NM_TO_UM = 1.0 / 1000.0 - - -def make_volume(seg_layer): - # CloudVolume espera o "info" completo de uma camada de imagem/ - # segmentação (com "scales"), mas só precisamos da parte de malha — - # os valores de "scales" abaixo são um placeholder mínimo válido só - # pra passar da checagem interna da biblioteca; a resolução REAL usada - # pra posicionar os vértices vem do "transform" dentro do info.json de - # cada camada de malha (lido automaticamente pelo cloud-volume). - info = { - "type": "segmentation", - "data_type": "uint64", - "num_channels": 1, - "scales": [{ - "chunk_sizes": [[64, 64, 64]], - "encoding": "raw", - "key": "1_1_1", - "resolution": [4, 4, 5.24], - "size": [1, 1, 1], - "voxel_offset": [0, 0, 0], - }], - "mesh": seg_layer, - } - return cv.CloudVolume(BASE, info=info, use_https=True) - - -def fetch_group(name, seg_layer, ids, lod): - t0 = time.time() - vol = make_volume(seg_layer) - meshes = vol.mesh.get(ids, lod=lod) - parts = [] - for m in meshes.values(): - if m is None or len(m.vertices) == 0 or len(m.faces) == 0: - continue - # A decodificação Draco em algumas LODs mais grosseiras às vezes - # produz vértices NaN/Inf pra fragmentos degenerados — zera-los - # em vez de excluir a organela inteira mantém a peça utilizável. - vertices = np.nan_to_num(m.vertices, nan=0.0, posinf=0.0, neginf=0.0) - parts.append(trimesh.Trimesh(vertices=vertices, faces=m.faces, process=False)) - if not parts: - raise RuntimeError(f"{name}: nenhuma geometria válida encontrada (verifique os IDs/LOD)") - combined = trimesh.util.concatenate(parts) - print(f" {name}: {len(combined.vertices)} vértices, {len(combined.faces)} faces ({time.time() - t0:.1f}s)", flush=True) - return combined - - -def main(): - print(f"Baixando organelas reais de {DATASET} (Janelia OpenOrganelle/CellMap, CC BY 4.0)...") - results = {} - for name, seg_layer, ids, lod in GROUPS: - results[name] = fetch_group(name, seg_layer, ids, lod) - - center = results["celula-membrana-plasmatica"].bounds.mean(axis=0) - - scene = trimesh.Scene() - for name, mesh in results.items(): - mesh.vertices = (mesh.vertices - center) * SCALE_NM_TO_UM - scene.add_geometry(mesh, node_name=name, geom_name=name) - - os.makedirs(OUT_DIR, exist_ok=True) - out_path = os.path.join(OUT_DIR, "citologia-raw.glb") - scene.export(out_path) - print(f"✓ {out_path} salvo — pronto para o build-cell-model.mjs simplificar/comprimir.") - - -if __name__ == "__main__": - main() diff --git a/data-pipeline/scripts/merge-manifests.mjs b/data-pipeline/scripts/merge-manifests.mjs index 9c21306..635a6e9 100644 --- a/data-pipeline/scripts/merge-manifests.mjs +++ b/data-pipeline/scripts/merge-manifests.mjs @@ -17,4 +17,4 @@ writeFileSync( JSON.stringify({ structures }, null, 2), ); -console.log(`manifest.json final: ${structures.length} estruturas (${real.structures.length} reais + ${tegumentar.structures.length} tegumentar + ${microscopy.structures.length} histologia + ${cell.structures.length} citologia 3D).`); +console.log(`manifest.json final: ${structures.length} estruturas (${real.structures.length} reais + ${tegumentar.structures.length} tegumentar + ${microscopy.structures.length} histologia + ${cell.structures.length} citologia).`); diff --git a/data-pipeline/seed/build-manifest-cell.mjs b/data-pipeline/seed/build-manifest-cell.mjs index 5bad753..42268e7 100644 --- a/data-pipeline/seed/build-manifest-cell.mjs +++ b/data-pipeline/seed/build-manifest-cell.mjs @@ -1,27 +1,20 @@ -// Gera manifest-cell.json — as 5 organelas do sistema "citologia", com -// malha 3D REAL (ao contrário de "histologia", que continua sendo -// fotomicrografias 2D — ver build-manifest-microscopy.mjs). +// Gera manifest-cell.json — as organelas do sistema "citologia". // -// Fonte: célula HeLa (jrc_hela-2) reconstruída em 3D a partir de FIB-SEM -// real e segmentada por machine learning pelo projeto CellMap/COSEM do -// Janelia Research Campus (HHMI) — OpenOrganelle, CC BY 4.0. -// https://openorganelle.janelia.org/ — DOI da coleção: -// https://doi.org/10.25378/janelia.c.7456966 -// Extração das malhas: data-pipeline/scripts/fetch-cell-organelles.py -// Simplificação/compressão final: data-pipeline/scripts/build-cell-model.mjs -// -// O nome de cada nó do citologia.glb (definido no script Python) é usado -// aqui como "meshName" — sem simplificação de nome/exclusão como no -// esquelético, já que são só 5 peças, cada uma com nome final único. +// Chegamos a integrar uma malha 3D REAL (segmentação FIB-SEM de uma célula +// HeLa completa, Janelia OpenOrganelle/CellMap, CC BY 4.0), mas o +// resultado visual atrapalhava mais do que ajudava pra estudar: a +// membrana real de uma célula aderente em cultura é achatada e irregular +// (não a "bolinha" clássica de diagrama didático) e cobria as organelas +// de dentro. A pedido do usuário, citologia agora é um modelo 3D +// ILUSTRATIVO — sem malha (modelFile "n/a", mesmo padrão do tegumentar), +// renderizado com primitivas do Three.js em +// frontend/src/components/CellDiagram3D.tsx (mesmo princípio do boneco +// esquemático de Planos Anatômicos, ver Mannequin.tsx). import { writeFileSync } from "node:fs"; import { fileURLToPath } from "node:url"; import path from "node:path"; const __dirname = path.dirname(fileURLToPath(import.meta.url)); -const MODEL_FILE = "citologia.glb"; - -const CREDIT = - "Malha 3D real, segmentada por FIB-SEM (célula HeLa, jrc_hela-2) pelo projeto CellMap/Janelia Research Campus — OpenOrganelle, CC BY 4.0."; function structure(slug, namePt, nameEn, description) { return { @@ -30,9 +23,9 @@ function structure(slug, namePt, nameEn, description) { namePt, nameEn, synonyms: [], - description: `${description} ${CREDIT}`, - regionPath: ["Citologia", "Célula (HeLa) — FIB-SEM real"], - modelFile: MODEL_FILE, + description, + regionPath: ["Citologia", "Célula (modelo ilustrativo)"], + modelFile: "n/a", meshName: slug, }; } @@ -48,25 +41,49 @@ const structures = [ "celula-nucleo", "Núcleo", "Nucleus", - "Organela que guarda o DNA da célula e comanda a síntese proteica; a forma irregular e volumosa vista aqui é real e típica de células HeLa (uma linhagem tumoral, com núcleo frequentemente atípico).", + "Organela que guarda o DNA da célula, organizado em cromatina, e comanda a síntese proteica ao transcrever RNA mensageiro; delimitado por um envoltório com poros nucleares.", ), structure( - "celula-reticulo-endoplasmatico", - "Retículo Endoplasmático", - "Endoplasmic Reticulum", - "Rede de membranas contínua com o envoltório nuclear, espalhada por todo o citoplasma; participa da síntese de proteínas (porção rugosa) e lipídios, além do armazenamento de cálcio.", + "celula-nucleolo", + "Nucléolo", + "Nucleolus", + "Região densa dentro do núcleo, sem membrana própria, onde o RNA ribossômico é transcrito e as subunidades do ribossomo começam a ser montadas.", ), structure( - "celula-complexo-de-golgi", + "celula-reticulo-rugoso", + "Retículo Endoplasmático Rugoso", + "Rough Endoplasmic Reticulum", + "Rede de membranas contínua com o envoltório nuclear, coberta de ribossomos (daí o aspecto \"rugoso\"); sintetiza e processa proteínas destinadas à secreção, à membrana ou a outras organelas.", + ), + structure( + "celula-reticulo-liso", + "Retículo Endoplasmático Liso", + "Smooth Endoplasmic Reticulum", + "Extensão do retículo endoplasmático sem ribossomos na superfície; sintetiza lipídios e esteroides, e participa da desintoxicação de substâncias e do armazenamento de cálcio.", + ), + structure( + "celula-golgi", "Complexo de Golgi", "Golgi Apparatus", - "Conjunto de sáculos membranosos empilhados, tipicamente posicionado perto do núcleo (posição confirmada nesta reconstrução real); processa, modifica e empacota proteínas vindas do retículo endoplasmático.", + "Conjunto de sáculos membranosos empilhados, tipicamente próximo ao núcleo; processa, modifica (ex: glicosilação) e empacota proteínas vindas do retículo endoplasmático, endereçando-as ao destino final.", ), structure( "celula-mitocondrias", "Mitocôndrias", "Mitochondria", - "População completa de mitocôndrias segmentadas nesta célula — organelas responsáveis pela produção de ATP por respiração celular; note a variação real de tamanho e forma entre elas, incluindo fragmentação, comum em linhagens tumorais.", + "Organelas de dupla membrana responsáveis pela produção de ATP por respiração celular — a \"usina de energia\" da célula; têm DNA próprio e se multiplicam por divisão.", + ), + structure( + "celula-ribossomos", + "Ribossomos", + "Ribosomes", + "Complexos de RNA ribossômico e proteínas que traduzem RNA mensageiro em proteínas; podem estar livres no citosol ou aderidos ao retículo endoplasmático rugoso.", + ), + structure( + "celula-lisossomos", + "Lisossomos", + "Lysosomes", + "Vesículas com enzimas digestivas (hidrolases ácidas) que degradam material vindo de dentro ou de fora da célula — o \"sistema de reciclagem e digestão\" celular.", ), ]; diff --git a/data-pipeline/seed/systems.mjs b/data-pipeline/seed/systems.mjs index bf9de57..22c16cc 100644 --- a/data-pipeline/seed/systems.mjs +++ b/data-pipeline/seed/systems.mjs @@ -13,8 +13,10 @@ export const SYSTEMS = [ { slug: "tegumentar", namePt: "Sistema Tegumentar", nameEn: "Integumentary System", colorHex: "#e0a96d", order: 9 }, { slug: "regioes", namePt: "Regiões Anatômicas do Corpo", nameEn: "Body Regions", colorHex: "#f472b6", order: 10 }, { slug: "referencias", namePt: "Planos e Linhas de Referência", nameEn: "Reference Planes and Lines", colorHex: "#a3e635", order: 11 }, - // Citologia: malha 3D REAL (segmentação FIB-SEM de uma célula HeLa - // inteira, Janelia OpenOrganelle/CellMap) — ver build-manifest-cell.mjs. + // Citologia: sem malha (modelFile "n/a") — modelo 3D ILUSTRATIVO + // desenhado com primitivas do Three.js (não há malha 3D real e aberta + // de célula pronta pra uso didático) — ver build-manifest-cell.mjs e + // CellDiagram3D.tsx. // Histologia: sem malha 3D (não existe dataset aberto de malha 3D real // de tecido histológico), usa fotomicrografias REAIS 2D com hotspots // — ver build-manifest-microscopy.mjs e MicroscopyDiagram.tsx. diff --git a/frontend/src/components/CellDiagram3D.tsx b/frontend/src/components/CellDiagram3D.tsx new file mode 100644 index 0000000..3cf8f74 --- /dev/null +++ b/frontend/src/components/CellDiagram3D.tsx @@ -0,0 +1,237 @@ +import { useMemo, type ReactNode } from "react"; +import { Canvas } from "@react-three/fiber"; +import { CameraControls } from "@react-three/drei"; +import * as THREE from "three"; +import { useAtlasStore } from "../store/useAtlasStore.js"; +import type { Structure } from "../lib/api.js"; + +// Não existe (ver discussão com o usuário) uma malha 3D real e aberta de +// célula com todas as organelas prontas pra estudo — chegamos a integrar +// uma segmentação real de FIB-SEM (célula HeLa, Janelia OpenOrganelle), +// mas o resultado visual ficou confuso pra estudar (a membrana real de uma +// célula aderente em cultura é achatada/irregular, não a "bolinha" que se +// espera de um diagrama didático, e cobria as organelas de dentro). Por +// pedido do usuário, citologia agora é um modelo 3D ILUSTRATIVO — desenhado +// à mão com primitivas do Three.js, mesmo princípio já usado no boneco +// esquemático de Planos Anatômicos (ver Mannequin.tsx) — mais reconhecível +// pra quem está estudando, mesmo não vindo de dado real. +const HIGHLIGHT = new THREE.Color("#22d3ee"); +const BLACK = new THREE.Color(0x000000); + +// Função comum (NÃO hook) de propósito — é chamada dentro de .map() e de +// callbacks de render-prop abaixo, onde um hook (useMemo/useState) violaria +// as Regras de Hooks do React (número de chamadas por render precisa ser +// sempre igual e não pode depender de laços/condicionais). +function materialProps(baseColor: string, selected: boolean, opacity = 1) { + return { + color: baseColor, + transparent: opacity < 1, + opacity, + emissive: selected ? HIGHLIGHT : BLACK, + emissiveIntensity: selected ? 0.7 : 0, + }; +} + +function Organelle({ + slug, + bySlug, + onSelect, + children, +}: { + slug: string; + bySlug: Map; + onSelect: (id: string) => void; + children: (isSelected: boolean) => ReactNode; +}) { + const selectedStructureId = useAtlasStore((s) => s.selectedStructureId); + const structure = bySlug.get(slug); + const isSelected = !!structure && structure.id === selectedStructureId; + + return ( + { + e.stopPropagation(); + if (structure) onSelect(structure.id); + }} + > + {children(isSelected)} + + ); +} + +// Pontos de mitocôndrias/lisossomos espalhados pelo citoplasma — fixos +// (não aleatórios a cada render) pra não "pular" toda vez que o componente +// atualiza. +const MITOCHONDRIA_SPOTS: [number, number, number, number][] = [ + [0.55, 0.35, 0.2, 0.4], [-0.5, 0.2, -0.35, -0.2], [0.3, -0.45, 0.4, 1.1], + [-0.35, -0.5, -0.2, 2.0], [0.6, -0.1, -0.45, 0.8], [-0.6, 0.4, 0.3, -1.3], + [0.15, 0.55, -0.5, 2.4], [-0.15, -0.2, 0.6, -0.6], +]; +const LYSOSOME_SPOTS: [number, number, number][] = [ + [0.4, -0.55, -0.15], [-0.45, -0.35, 0.35], [0.5, 0.15, 0.45], [-0.3, 0.5, -0.4], +]; +const RIBOSOME_SPOTS: [number, number, number][] = [ + [0.2, 0.42, 0.32], [0.28, 0.5, 0.18], [0.12, 0.55, 0.3], [0.3, 0.35, 0.42], + [-0.5, -0.2, -0.5], [0.55, -0.35, 0.1], [-0.2, 0.6, -0.3], [0.4, 0.1, -0.55], + [-0.55, 0.3, 0.25], [0.1, -0.6, -0.25], +]; + +export function CellDiagram3D({ bySlug: bySlugAll }: { bySlug: Map }) { + const toggleSystem = useAtlasStore((s) => s.toggleSystem); + const selectStructure = useAtlasStore((s) => s.selectStructure); + + const bySlug = useMemo( + () => new Map([...bySlugAll.values()].filter((s) => s.system.slug === "citologia").map((s) => [s.slug, s])), + [bySlugAll], + ); + + if (bySlug.size === 0) return null; + + return ( +
+
+
+

+ Citologia (A Célula) — modelo ilustrativo, gire e clique nas organelas +

+ +
+ +
+ + + + + + + {/* Núcleo + nucléolo */} + + {(sel) => ( + + + + + )} + + + {(sel) => ( + + + + + )} + + + {/* Retículo endoplasmático rugoso — anéis achatados perto do núcleo */} + + {(sel) => ( + + {[0, 1, 2].map((i) => ( + + + + + ))} + + )} + + + {/* Retículo endoplasmático liso — tubos sem ribossomos, mais afastado */} + + {(sel) => ( + + {[0, 1].map((i) => ( + + + + + ))} + + )} + + + {/* Complexo de Golgi — pilha de discos curvos perto do núcleo */} + + {(sel) => ( + + {[0, 1, 2, 3, 4].map((i) => ( + + + + + ))} + + )} + + + {/* Mitocôndrias — várias, espalhadas */} + + {(sel) => ( + <> + {MITOCHONDRIA_SPOTS.map(([x, y, z, rot], i) => ( + + + + + ))} + + )} + + + {/* Lisossomos — pequenas esferas isoladas */} + + {(sel) => ( + <> + {LYSOSOME_SPOTS.map(([x, y, z], i) => ( + + + + + ))} + + )} + + + {/* Ribossomos — pontinhos livres pelo citoplasma e no RE rugoso */} + + {(sel) => ( + <> + {RIBOSOME_SPOTS.map(([x, y, z], i) => ( + + + + + ))} + + )} + + + {/* Membrana plasmática — envoltório translúcido por fora de tudo. + Sem raycast: sendo a casca que ENVOLVE todas as outras + organelas, ela sempre seria o primeiro acerto de qualquer + raio de clique, escondendo as organelas de dentro atrás + dela (o problema que aconteceu com a malha real de FIB-SEM). + Puramente decorativa aqui; ainda dá pra abrir a descrição + dela pela busca. */} + null}> + + + + + + +
+ +

+ Modelo ilustrativo (não derivado de dado real) — proporções e posições relativas são esquemáticas, pensadas pra reconhecer cada organela. +

+
+
+ ); +} diff --git a/frontend/src/components/viewer/Scene.tsx b/frontend/src/components/viewer/Scene.tsx index 17ff7ee..0ab2d73 100644 --- a/frontend/src/components/viewer/Scene.tsx +++ b/frontend/src/components/viewer/Scene.tsx @@ -3,14 +3,15 @@ import { Canvas, useThree } from "@react-three/fiber"; import { CameraControls, Grid } from "@react-three/drei"; import * as THREE from "three"; import { useAtlasStore, type BodyRegion, type ClipAxis } from "../../store/useAtlasStore.js"; -import { SystemModel, CELL_MEMBRANE_SLUG } from "./SystemModel.js"; +import { SystemModel } from "./SystemModel.js"; import type { Structure } from "../../lib/api.js"; -// Sistemas apresentados como diagrama 2D (fotomicrografia real com -// hotspots) em vez de malha 3D — ver SkinDiagram.tsx e -// MicroscopyDiagram.tsx. Citologia usa malha 3D real (ver -// build-manifest-cell.mjs), então não entra aqui. -const NON_3D_SYSTEMS = new Set(["tegumentar", "histologia"]); +// Sistemas sem .glb pra carregar — tegumentar/histologia são +// fotomicrografias 2D reais com hotspots (ver SkinDiagram.tsx e +// MicroscopyDiagram.tsx) e citologia é um modelo 3D ilustrativo desenhado +// à mão em Three.js (ver CellDiagram3D.tsx), não uma malha carregada de +// arquivo. +const NON_3D_SYSTEMS = new Set(["tegumentar", "histologia", "citologia"]); // Normais de referência em anatomia: sagital separa esq./dir., coronal // (= frontal) separa ant./post., transversal separa sup./inf. @@ -260,14 +261,7 @@ function ClickSelector({ ndc.set(x, y); raycaster.setFromCamera(ndc, camera); const hits = raycaster.intersectObjects(scene.children, true); - const valid = hits.filter((h) => h.object.visible && h.object.name && bySlug.has(h.object.name)); - // A membrana plasmática da célula é semitransparente de propósito - // (ver SystemModel.tsx) justamente pra deixar ver as organelas por - // dentro — clicar nela não pode "capturar" o clique antes de - // chegar na organela real por trás; só vira o resultado do clique - // se não houver mais nada na frente/atrás dela nesse raio. - const hit = - valid.find((h) => h.object.name !== CELL_MEMBRANE_SLUG) ?? valid[0]; + const hit = hits.find((h) => h.object.visible && h.object.name && bySlug.has(h.object.name)); if (hit && (!best || hit.distance < best.distance)) { best = { distance: hit.distance, slug: hit.object.name }; } diff --git a/frontend/src/components/viewer/SystemModel.tsx b/frontend/src/components/viewer/SystemModel.tsx index 9348211..bf78085 100644 --- a/frontend/src/components/viewer/SystemModel.tsx +++ b/frontend/src/components/viewer/SystemModel.tsx @@ -17,13 +17,6 @@ interface Props { const HIGHLIGHT = new THREE.Color("#22d3ee"); const BLACK = new THREE.Color(0x000000); -// A membrana plasmática é a malha REAL (FIB-SEM) de fora de uma célula -// HeLa cultivada — achatada e irregular (célula aderente em cultura, não -// uma bolinha lisa), não um erro. Mas sólida e opaca ela cobria e escondia -// as organelas de dentro (núcleo, mitocôndrias etc.), que é o oposto do -// que se quer pra estudar a célula — por isso ela é semitransparente. -export const CELL_MEMBRANE_SLUG = "celula-membrana-plasmatica"; - export function SystemModel({ modelFile, structuresInFile, @@ -127,13 +120,6 @@ export function SystemModel({ const isSelected = structure.slug === selectedSlug; mat.emissive = isSelected ? HIGHLIGHT : BLACK; mat.emissiveIntensity = isSelected ? 0.6 : 0; - - if (structure.slug === CELL_MEMBRANE_SLUG) { - mat.transparent = true; - mat.opacity = 0.18; - mat.depthWrite = false; - mat.side = THREE.DoubleSide; - } }); // eslint-disable-next-line react-hooks/exhaustive-deps }, [root, bySlugInFile, byMeshNameInFile, visibleSystems, selectedSlug, clippingPlanes]); diff --git a/frontend/src/routes/AtlasPage.tsx b/frontend/src/routes/AtlasPage.tsx index a97686c..3282351 100644 --- a/frontend/src/routes/AtlasPage.tsx +++ b/frontend/src/routes/AtlasPage.tsx @@ -9,6 +9,7 @@ import { StructureInfoPanel } from "../components/StructureInfoPanel.js"; import { QuickRegionNav } from "../components/QuickRegionNav.js"; import { SkinDiagram } from "../components/SkinDiagram.js"; import { MicroscopyDiagram } from "../components/MicroscopyDiagram.js"; +import { CellDiagram3D } from "../components/CellDiagram3D.js"; import { trackVisit } from "../lib/analytics.js"; // Em telas pequenas não há espaço para os 3 painéis lado a lado — os @@ -74,6 +75,7 @@ export default function AtlasPage() { +