diff --git a/app/components/AnatomyApp.tsx b/app/components/AnatomyApp.tsx index a133e79..80fb10b 100644 --- a/app/components/AnatomyApp.tsx +++ b/app/components/AnatomyApp.tsx @@ -7,6 +7,7 @@ import { BookOpen, Bookmark, BrainCircuit, + Check, ChevronDown, CircleHelp, Compass, @@ -139,11 +140,16 @@ export function AnatomyApp() { -
+ {/* Picking an organ is a single choice among many, so the list is a + radio group — the check mark has a programmatic equivalent rather + than being colour and an icon alone. */} +
{filteredOrgans.map((item) => ( ))}
diff --git a/app/components/OrganViewer.tsx b/app/components/OrganViewer.tsx index 35a02f4..4fac741 100644 --- a/app/components/OrganViewer.tsx +++ b/app/components/OrganViewer.tsx @@ -4,16 +4,19 @@ import { useCallback, useEffect, useRef, useState } from "react"; import { Box, CircleDashed, + Grid2x2, Layers3, Maximize2, RotateCcw, - ScanLine, Search, Sparkles, X, } from "lucide-react"; import type { Hotspot, Organ } from "../lib/anatomy-data"; -import type { AnatomyViewer } from "../lib/three/viewer"; +import type { AnatomyViewer, SectionAxis } from "../lib/three/viewer"; +import { SectionControls } from "./SectionControls"; + +const SECTION_DEFAULTS = { axis: "free" as SectionAxis, depth: 0, flipped: false, showPlane: true }; type Props = { organ: Organ; @@ -32,7 +35,10 @@ export function OrganViewer({ organ, autoRotate, onAutoRotate, compare, onCompar const [loading, setLoading] = useState(true); const [progress, setProgress] = useState(0); const [slowLoad, setSlowLoad] = useState(false); - const [activeTool, setActiveTool] = useState(null); + // The three viewer tools are independent toggles, so a single "active tool" + // could only ever describe one of them while the other two stayed on unseen. + const [active, setActive] = useState({ isolate: false, section: false, wireframe: false }); + const [section, setSection] = useState(SECTION_DEFAULTS); // A typical organ is ready well inside a second — flashing a loading panel for // that reads as jank. It only appears if the fetch is genuinely slow; the flag @@ -101,25 +107,62 @@ export function OrganViewer({ organ, autoRotate, onAutoRotate, compare, onCompar if (!viewer) return; if (tool === "rotate") onAutoRotate(!autoRotate); if (tool === "zoom") viewer.zoom(-1); - if (tool === "isolate") setActiveTool(viewer.toggleIsolate() ? tool : null); - if (tool === "section") setActiveTool(viewer.toggleCrossSection() ? tool : null); - if (tool === "layers") setActiveTool(viewer.toggleLayers() ? tool : null); + if (tool === "isolate") setActive((state) => ({ ...state, isolate: viewer.toggleIsolate() })); + if (tool === "section") { + const on = viewer.toggleCrossSection(); + setActive((state) => ({ ...state, section: on })); + // A specimen that keeps turning is no use while you are reading its cut + // face — but this goes through the normal control, so it can be turned + // straight back on rather than being locked out. + if (on && autoRotate) onAutoRotate(false); + } + if (tool === "wireframe") setActive((state) => ({ ...state, wireframe: viewer.toggleWireframe() })); if (tool === "compare") onCompare(); if (tool === "reset") { + // reset() clears every tool in the viewer too, so mirroring it here keeps + // the buttons honest. viewer.reset(); - setActiveTool(null); + setActive({ isolate: false, section: false, wireframe: false }); + setSection(SECTION_DEFAULTS); } }; + const handleAxis = (axis: SectionAxis) => { + viewerRef.current?.setSectionAxis(axis); + setSection((state) => ({ ...state, axis })); + }; + + const handleDepth = (depth: number) => { + viewerRef.current?.setSectionDepth(depth); + setSection((state) => ({ ...state, depth })); + }; + + const handleFlip = () => { + const flipped = viewerRef.current?.toggleSectionFlip() ?? false; + setSection((state) => ({ ...state, flipped })); + }; + + const handleShowPlane = (shown: boolean) => { + const showPlane = viewerRef.current?.setSectionPlaneVisible(shown) ?? true; + setSection((state) => ({ ...state, showPlane })); + }; + const tools = [ - { id: "rotate", label: "Rotate", icon: RotateCcw }, - { id: "zoom", label: "Zoom", icon: Search }, - { id: "isolate", label: "Isolate", icon: CircleDashed }, - { id: "section", label: "Cross-section", icon: ScanLine }, - { id: "layers", label: "Layers", icon: Layers3 }, - { id: "compare", label: "Compare", icon: Box }, - { id: "reset", label: "Reset", icon: RotateCcw }, - ]; + { id: "rotate", label: "Rotate", icon: RotateCcw, toggle: true }, + { id: "zoom", label: "Zoom", icon: Search, toggle: false }, + { id: "isolate", label: "Isolate", icon: CircleDashed, toggle: true }, + // The layered metaphor belongs to the tool that actually opens the organ up. + { id: "section", label: "Cross-section", icon: Layers3, toggle: true }, + { id: "wireframe", label: "Wireframe", icon: Grid2x2, toggle: true }, + { id: "compare", label: "Compare", icon: Box, toggle: true }, + { id: "reset", label: "Reset", icon: RotateCcw, toggle: false }, + ] as const; + + const isActive = (id: string) => { + if (id === "compare") return compare; + if (id === "rotate") return autoRotate; + return id in active && active[id as keyof typeof active]; + }; return (
@@ -127,13 +170,13 @@ export function OrganViewer({ organ, autoRotate, onAutoRotate, compare, onCompar
- {tools.map(({ id, label, icon: Icon }) => ( + {tools.map(({ id, label, icon: Icon, toggle }) => (
+ {active.section && ( + + )} +