first commit
This commit is contained in:
commit
0e854110d3
49 files changed
+2292
No files matched your search
@@ -0,0 +1,106 @@
|
||||
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 { SystemModel } from "./SystemModel.js";
|
||||
import type { Structure } from "../../lib/api.js";
|
||||
|
||||
const AXIS_CONFIG: Record<ClipAxis, { normal: [number, number, number]; min: number; max: number }> = {
|
||||
sagittal: { normal: [1, 0, 0], min: -0.4, max: 0.4 },
|
||||
coronal: { normal: [0, 0, 1], min: -0.3, max: 0.3 },
|
||||
transverse: { normal: [0, -1, 0], min: -1.8, max: 0 },
|
||||
};
|
||||
|
||||
function useClippingPlanes(): THREE.Plane[] {
|
||||
const { enabled, axis, position } = useAtlasStore((s) => s.clipping);
|
||||
return useMemo(() => {
|
||||
if (!enabled) return [];
|
||||
const cfg = AXIS_CONFIG[axis];
|
||||
const t = (position + 1) / 2; // normalize -1..1 -> 0..1
|
||||
const constant = cfg.min + t * (cfg.max - cfg.min);
|
||||
const normal = new THREE.Vector3(...cfg.normal);
|
||||
return [new THREE.Plane(normal, -constant)];
|
||||
}, [enabled, axis, position]);
|
||||
}
|
||||
|
||||
function FocusController() {
|
||||
const controlsRef = useRef<CameraControls>(null);
|
||||
const { scene } = useThree();
|
||||
const focusRequest = useAtlasStore((s) => s.focusRequest);
|
||||
|
||||
useEffect(() => {
|
||||
if (!focusRequest || !controlsRef.current) return;
|
||||
const obj = scene.getObjectByName(focusRequest.slug);
|
||||
if (!obj) return;
|
||||
const box = new THREE.Box3().setFromObject(obj);
|
||||
if (box.isEmpty()) return;
|
||||
controlsRef.current.fitToBox(box, true, {
|
||||
paddingLeft: 0.4,
|
||||
paddingRight: 0.4,
|
||||
paddingTop: 0.4,
|
||||
paddingBottom: 0.4,
|
||||
});
|
||||
}, [focusRequest, scene]);
|
||||
|
||||
return (
|
||||
<CameraControls
|
||||
ref={controlsRef}
|
||||
makeDefault
|
||||
minDistance={0.15}
|
||||
maxDistance={4}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function Scene({ bySlug }: { bySlug: Map<string, Structure> }) {
|
||||
const visibleSystems = useAtlasStore((s) => s.visibleSystems);
|
||||
const selectedStructureId = useAtlasStore((s) => s.selectedStructureId);
|
||||
const selectStructure = useAtlasStore((s) => s.selectStructure);
|
||||
const clippingPlanes = useClippingPlanes();
|
||||
|
||||
const selectedSlug = selectedStructureId
|
||||
? [...bySlug.values()].find((s) => s.id === selectedStructureId)?.slug ?? null
|
||||
: null;
|
||||
|
||||
const modelFiles = useMemo(
|
||||
() => [...visibleSystems].map((slug) => `${slug}.glb`),
|
||||
[visibleSystems],
|
||||
);
|
||||
|
||||
return (
|
||||
<Canvas
|
||||
shadows
|
||||
camera={{ position: [1.2, 1.4, 1.6], fov: 45, near: 0.01, far: 20 }}
|
||||
gl={{ localClippingEnabled: true }}
|
||||
>
|
||||
<color attach="background" args={["#0b1220"]} />
|
||||
<ambientLight intensity={0.55} />
|
||||
<directionalLight position={[2, 3, 2]} intensity={1.1} castShadow />
|
||||
<directionalLight position={[-2, 1, -2]} intensity={0.4} />
|
||||
<Suspense fallback={null}>
|
||||
{modelFiles.map((modelFile) => (
|
||||
<SystemModel
|
||||
key={modelFile}
|
||||
modelFile={modelFile}
|
||||
selectedSlug={selectedSlug}
|
||||
clippingPlanes={clippingPlanes}
|
||||
onSelect={(slug) => {
|
||||
const structure = bySlug.get(slug);
|
||||
if (structure) selectStructure(structure.id);
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</Suspense>
|
||||
<Grid
|
||||
position={[0, 0, 0]}
|
||||
args={[4, 4]}
|
||||
cellColor="#1e293b"
|
||||
sectionColor="#334155"
|
||||
fadeDistance={6}
|
||||
infiniteGrid
|
||||
/>
|
||||
<FocusController />
|
||||
</Canvas>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user