From b08a8de676618ad4a32f6cff648d4f3c4b9a18f2 Mon Sep 17 00:00:00 2001 From: Heberto Mayorquin Date: Wed, 24 Jun 2026 20:30:14 -0600 Subject: [PATCH 1/7] Add a manufacturer landing page and open the viewer fitted to a probe's contacts with a per-probe zoom cap --- apps/probe-viewer/public/logos/README.md | 19 ++ .../public/logos/cambridgeneurotech.png | Bin 0 -> 25165 bytes .../public/logos/diagnosticbiochips.png | Bin 0 -> 62706 bytes apps/probe-viewer/public/logos/imec.png | Bin 0 -> 10747 bytes apps/probe-viewer/public/logos/neuronexus.svg | 8 + apps/probe-viewer/public/logos/plexon.png | Bin 0 -> 12055 bytes .../public/logos/sinaps-research-platform.svg | 1 + apps/probe-viewer/src/App.css | 202 +++++++++++++++ apps/probe-viewer/src/App.tsx | 9 + .../src/components/ProbeCanvas.tsx | 60 ++++- .../src/components/ProbeIndex.tsx | 110 ++++++++ .../src/components/ProbeViewer.tsx | 241 ++++++++++++------ apps/probe-viewer/src/components/Sidebar.tsx | 17 ++ apps/probe-viewer/src/state/useAppStore.ts | 30 ++- .../src/state/useProbeRouteSync.ts | 101 +++----- .../src/state/useRestoreCameraFromUrl.ts | 52 +++- .../src/state/useSyncCameraToUrl.ts | 57 ++++- 17 files changed, 726 insertions(+), 181 deletions(-) create mode 100644 apps/probe-viewer/public/logos/README.md create mode 100644 apps/probe-viewer/public/logos/cambridgeneurotech.png create mode 100644 apps/probe-viewer/public/logos/diagnosticbiochips.png create mode 100644 apps/probe-viewer/public/logos/imec.png create mode 100644 apps/probe-viewer/public/logos/neuronexus.svg create mode 100644 apps/probe-viewer/public/logos/plexon.png create mode 100644 apps/probe-viewer/public/logos/sinaps-research-platform.svg create mode 100644 apps/probe-viewer/src/components/ProbeIndex.tsx diff --git a/apps/probe-viewer/public/logos/README.md b/apps/probe-viewer/public/logos/README.md new file mode 100644 index 0000000..61b6259 --- /dev/null +++ b/apps/probe-viewer/public/logos/README.md @@ -0,0 +1,19 @@ +# Manufacturer logos + +Drop a logo here named `.png` to replace the wordmark on that +manufacturer's landing-page card. The key is the manufacturer folder name in the +repo root, e.g.: + +- `cambridgeneurotech.png` +- `diagnosticbiochips.png` +- `imec.png` +- `neuronexus.png` +- `plexon.png` +- `sinaps-research-platform.png` + +If no file is present, the card falls back to a brand-colored wordmark +(see `ProbeIndex.tsx`). Logos are landscape-friendly (rendered at 16:9, contained +on a white background). + +Only add logos you have the right to use. Manufacturer logos are trademarks; +they are intentionally not committed here by default. diff --git a/apps/probe-viewer/public/logos/cambridgeneurotech.png b/apps/probe-viewer/public/logos/cambridgeneurotech.png new file mode 100644 index 0000000000000000000000000000000000000000..f22deaa775fccef34fbbfe1f36469304de63c8c5 GIT binary patch literal 25165 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 QO~fAU>$f!W)$cv|9}##~HUIzs literal 0 HcmV?d00001 diff --git a/apps/probe-viewer/public/logos/diagnosticbiochips.png b/apps/probe-viewer/public/logos/diagnosticbiochips.png new file mode 100644 index 0000000000000000000000000000000000000000..ece72278418eff2f158b89de8c76e7b0a140d959 GIT binary patch literal 62706 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

vFtFA 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 NK}8c+q-=iw{{U|){)qqp literal 0 HcmV?d00001 diff --git a/apps/probe-viewer/public/logos/imec.png b/apps/probe-viewer/public/logos/imec.png new file mode 100644 index 0000000000000000000000000000000000000000..84084d63452ca613cf9f5bd9d2d5054d9c0e6b2e GIT binary patch literal 10747 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 k5ijpNI$?XndUOQ2aKF8>5?j{-Ug`ui!Q7 + + + + + + + diff --git a/apps/probe-viewer/public/logos/plexon.png b/apps/probe-viewer/public/logos/plexon.png new file mode 100644 index 0000000000000000000000000000000000000000..e0390647ef2155fb01d33b4747098c668fe6d8f3 GIT binary patch literal 12055 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 J`pEM2{{j5txa9x< literal 0 HcmV?d00001 diff --git a/apps/probe-viewer/public/logos/sinaps-research-platform.svg b/apps/probe-viewer/public/logos/sinaps-research-platform.svg new file mode 100644 index 0000000..46797f5 --- /dev/null +++ b/apps/probe-viewer/public/logos/sinaps-research-platform.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/probe-viewer/src/App.css b/apps/probe-viewer/src/App.css index ff5e061..d68b40b 100644 --- a/apps/probe-viewer/src/App.css +++ b/apps/probe-viewer/src/App.css @@ -463,3 +463,205 @@ padding: 1rem; } } + +/* ===== Catalog landing page ===== */ +.index { + height: 100%; + overflow-y: auto; + padding: 2rem 2.5rem 3rem; +} + +.index-header { + max-width: 1200px; + margin: 0 auto 1.5rem; +} + +.index-title { + margin: 0 0 0.25rem; + font-size: 1.6rem; + color: #0f172a; +} + +.index-subtitle { + margin: 0 0 1rem; + color: #475569; +} + +.index-controls { + display: flex; + flex-wrap: wrap; + gap: 0.75rem; +} + +.index-search, +.index-manufacturer { + font: inherit; + padding: 0.5rem 0.75rem; + border-radius: 0.6rem; + border: 1px solid rgba(148, 163, 184, 0.6); + background: #fff; +} + +.index-search { + flex: 1 1 16rem; + min-width: 12rem; +} + +.index-hint, +.index-error { + max-width: 1200px; + margin: 1rem auto; + color: #64748b; +} + +.index-error { + color: #b91c1c; +} + +.index-grid { + max-width: 1200px; + margin: 0 auto; + display: grid; + grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); + gap: 1rem; +} + +/* Manufacturer landing: fewer, larger cards. */ +.index-grid--manufacturers { + grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); + gap: 1.5rem; +} + +.probe-card { + display: flex; + flex-direction: column; + text-align: left; + padding: 0; + border: 1px solid rgba(148, 163, 184, 0.45); + border-radius: 0.9rem; + background: #fff; + cursor: pointer; + overflow: hidden; + font: inherit; + transition: transform 0.12s ease, box-shadow 0.2s ease, border-color 0.2s ease; +} + +.probe-card:hover { + transform: translateY(-2px); + border-color: rgba(37, 99, 235, 0.6); + box-shadow: 0 12px 22px rgba(15, 23, 42, 0.12); +} + +.probe-card-image { + width: 100%; + aspect-ratio: 4 / 3; + object-fit: contain; + background: #f8fafc; + border-bottom: 1px solid rgba(148, 163, 184, 0.3); +} + +.probe-card-image--empty { + display: flex; + align-items: center; + justify-content: center; + color: #94a3b8; + font-size: 0.85rem; +} + +/* Manufacturer logo: centered in a uniform tile, sized by height (with a width + cap) so logos of different aspect ratios read at a consistent scale. */ +.probe-card-logo-tile { + aspect-ratio: 5 / 2; + background: #fff; + display: flex; + align-items: center; + justify-content: center; + padding: 0.5rem 0.75rem; +} + +.probe-card-logo-img { + max-height: 5rem; + max-width: 97%; + width: auto; + height: auto; + object-fit: contain; +} + +/* Manufacturer wordmark cards (fallback when no logo file is present). */ +.probe-card-logo { + width: 100%; + aspect-ratio: 16 / 9; + display: flex; + align-items: center; + justify-content: center; + padding: 1.25rem; + text-align: center; + background: #334155; +} + +.probe-card-logo-text { + color: #fff; + font-size: 1.4rem; + font-weight: 700; + letter-spacing: 0.01em; + line-height: 1.2; + text-shadow: 0 1px 2px rgba(0, 0, 0, 0.25); +} + +.probe-card-logo--cambridgeneurotech { + background: linear-gradient(135deg, #0e7490, #06b6d4); +} +.probe-card-logo--imec { + background: linear-gradient(135deg, #9f1239, #e11d48); +} +.probe-card-logo--neuronexus { + background: linear-gradient(135deg, #1e40af, #3b82f6); +} +.probe-card-logo--diagnosticbiochips { + background: linear-gradient(135deg, #6d28d9, #8b5cf6); +} +.probe-card-logo--plexon { + background: linear-gradient(135deg, #9a3412, #f97316); +} +.probe-card-logo--sinaps-research-platform { + background: linear-gradient(135deg, #115e59, #14b8a6); +} + +.probe-card-body { + display: flex; + flex-direction: column; + gap: 0.2rem; + padding: 0.7rem 0.85rem 0.9rem; +} + +.probe-card-title { + font-weight: 600; + color: #0f172a; +} + +.probe-card-manufacturer { + font-size: 0.85rem; + color: #2563eb; +} + +.probe-card-meta { + font-size: 0.8rem; + color: #64748b; +} + +.sidebar-home { + align-self: flex-start; + background: none; + border: none; + padding: 0; + margin-bottom: 0.6rem; + font: inherit; + font-size: 0.85rem; + font-weight: 600; + color: #2563eb; + cursor: pointer; +} + +.sidebar-home:hover { + text-decoration: underline; +} diff --git a/apps/probe-viewer/src/App.tsx b/apps/probe-viewer/src/App.tsx index ed9e109..84c57e8 100644 --- a/apps/probe-viewer/src/App.tsx +++ b/apps/probe-viewer/src/App.tsx @@ -1,5 +1,7 @@ import { useEffect } from "react"; +import { useParams } from "react-router-dom"; +import { ProbeIndex } from "./components/ProbeIndex"; import { ProbeViewer } from "./components/ProbeViewer"; import { Sidebar } from "./components/Sidebar"; import { useAppStore } from "./state/useAppStore"; @@ -10,6 +12,8 @@ import "./App.css"; function App() { const loadManifest = useAppStore((state) => state.loadManifest); + // Present on /probes/:manufacturer/:model, absent on the bare "/" landing. + const { model } = useParams(); useEffect(() => { void loadManifest(); @@ -24,6 +28,11 @@ function App() { useRestoreCameraFromUrl(); useSyncCameraToUrl(); + // No probe in the route: show the catalog landing instead of a probe view. + if (!model) { + return ; + } + return (

@@ -279,38 +366,23 @@ export function ProbeViewer() {
- -
@@ -355,6 +427,7 @@ export function ProbeViewer() { entry={entry} probeData={probeData} camera={view.camera} + maxZoom={view.maxZoom} showContactIds={view.showContactIds} showScaleBar={view.showScaleBar} onViewCenterChange={(x, y) => setViewCenter(x, y)} diff --git a/apps/probe-viewer/src/components/Sidebar.tsx b/apps/probe-viewer/src/components/Sidebar.tsx index 44486cb..035b9a8 100644 --- a/apps/probe-viewer/src/components/Sidebar.tsx +++ b/apps/probe-viewer/src/components/Sidebar.tsx @@ -1,4 +1,5 @@ import { useEffect, useMemo, useState } from "react"; +import { useNavigate } from "react-router-dom"; import { useAppStore } from "../state/useAppStore"; import type { ManifestEntry } from "../types/probe"; @@ -22,6 +23,7 @@ export function Sidebar() { const selectManufacturer = useAppStore((state) => state.selectManufacturer); const selectedProbeId = useAppStore((state) => state.selectedProbeId); const selectProbe = useAppStore((state) => state.selectProbe); + const navigate = useNavigate(); const searchQuery = useAppStore((state) => state.searchQuery); const setSearchQuery = useAppStore((state) => state.setSearchQuery); @@ -56,7 +58,11 @@ export function Sidebar() { }, [manifest, selectedManufacturer, searchQuery]); useEffect(() => { + // Re-pick a probe only when a stale one is selected (e.g. after switching + // manufacturer). Do NOT auto-pick when nothing is selected, or the Home + // button (which clears the selection) would immediately bounce back here. if ( + selectedProbeId && filteredEntries.length > 0 && !filteredEntries.some((entry) => entry.id === selectedProbeId) ) { @@ -157,6 +163,17 @@ export function Sidebar() { return (
+

Probe Catalog

Browse available probe layouts and inspect their geometry. diff --git a/apps/probe-viewer/src/state/useAppStore.ts b/apps/probe-viewer/src/state/useAppStore.ts index f4b93a9..a6bb6f2 100644 --- a/apps/probe-viewer/src/state/useAppStore.ts +++ b/apps/probe-viewer/src/state/useAppStore.ts @@ -16,6 +16,9 @@ interface ViewState { showContactIds: boolean; showScaleBar: boolean; showOverview: boolean; + // Per-probe zoom ceiling, computed from geometry so the smallest contact can + // fill the viewport regardless of probe length (see setMaxZoom callers). + maxZoom: number; } interface AppState { @@ -39,6 +42,7 @@ interface AppState { selectProbe: (probeId?: string) => void; ensureProbeLoaded: (probeId: string) => Promise; setZoom: (zoom: number) => void; + setMaxZoom: (value: number) => void; setViewCenter: (x: number | null, y: number | null) => void; markCameraInitialized: () => void; resetView: () => void; @@ -48,7 +52,10 @@ interface AppState { } export const VIEW_ZOOM_MIN = 0.1; -export const VIEW_ZOOM_MAX = 100; // High max for long probes like Neuropixels +export const VIEW_ZOOM_MAX = 100; // Default ceiling until a per-probe cap is computed +// Hard ceiling purely against floating-point wobble at extreme scales; the real +// per-probe cap (view.maxZoom) is almost always well below this. +export const VIEW_ZOOM_ABSOLUTE_MAX = 1e5; const INITIAL_CAMERA: ProbeViewerCamera = { zoom: 1, @@ -61,6 +68,7 @@ const INITIAL_VIEW_STATE: ViewState = { showContactIds: false, showScaleBar: true, showOverview: true, + maxZoom: VIEW_ZOOM_MAX, }; function clamp(value: number, min: number, max: number) { @@ -181,11 +189,27 @@ export const useAppStore = create((set, get) => ({ ...state.view, camera: { ...state.view.camera, - zoom: clamp(zoom, VIEW_ZOOM_MIN, VIEW_ZOOM_MAX), + zoom: clamp(zoom, VIEW_ZOOM_MIN, state.view.maxZoom), }, }, })), + setMaxZoom: (value) => + set((state) => { + const maxZoom = clamp(value, VIEW_ZOOM_MIN, VIEW_ZOOM_ABSOLUTE_MAX); + return { + view: { + ...state.view, + maxZoom, + // Re-clamp the current zoom so a tighter cap pulls the view back in. + camera: { + ...state.view.camera, + zoom: Math.min(state.view.camera.zoom, maxZoom), + }, + }, + }; + }), + setViewCenter: (x, y) => set((state) => ({ view: { @@ -201,6 +225,8 @@ export const useAppStore = create((set, get) => ({ view: { ...INITIAL_VIEW_STATE, showContactIds: state.view.showContactIds, + // The cap is a property of the probe, not the camera; keep it across a reset. + maxZoom: state.view.maxZoom, }, })), diff --git a/apps/probe-viewer/src/state/useProbeRouteSync.ts b/apps/probe-viewer/src/state/useProbeRouteSync.ts index fd46537..454642a 100644 --- a/apps/probe-viewer/src/state/useProbeRouteSync.ts +++ b/apps/probe-viewer/src/state/useProbeRouteSync.ts @@ -1,20 +1,17 @@ -import { useEffect, useMemo } from "react"; +import { useEffect, useMemo, useRef } from "react"; import { useLocation, useNavigate, useParams } from "react-router-dom"; import { useAppStore } from "./useAppStore"; -const DEFAULT_PROBE_ID = "plexon:8S1024"; - // Keeps the selected probe and the URL path (/probes/:manufacturer/:model) in // agreement: // -// select (path -> store) on load / manifest change, picks the probe named in -// the URL, falling back to a default -// sync (store -> path) navigates to match the selection when it changes -// -// Unlike the camera sync there is no shared flag: each effect carries its own -// loop-breaker (select no-ops when the selection is already valid; sync skips -// navigation when the path already matches), so the two cannot ping-pong. +// select (path -> store) acts only when the route's probe actually changes, +// so navigating away cannot re-add the probe we are +// leaving; the bare "/" landing clears the selection +// sync (store -> path) navigates to match the selection when it changes, +// reading the live selection so a same-commit clear +// (e.g. the Home button) is respected export function useProbeRouteSync() { const { manufacturer, model } = useParams(); const location = useLocation(); @@ -31,82 +28,60 @@ export function useProbeRouteSync() { return map; }, [manifest]); - // select: URL path -> store + // select: URL path -> store. Only react when the route's probe id actually + // changes (tracked via a ref). This is what prevents a bounce: during a + // navigation transient the route can briefly still name the old probe while + // the selection has been cleared, and without this guard the effect would + // re-select it. `null` is the "not yet run" sentinel; `undefined` is the + // landing route. + const lastRouteIdRef = useRef(null); useEffect(() => { - if (manifestStatus !== "success" || manifest.length === 0) { - return; - } + if (manifestStatus !== "success" || manifest.length === 0) return; const routeId = manufacturer && model ? `${manufacturer}:${model}` : undefined; - const routeEntry = routeId ? manifestById.get(routeId) : undefined; - const currentSelected = selectedProbeId - ? manifestById.get(selectedProbeId) - : undefined; - - const getDefaultProbe = () => - manifestById.get(DEFAULT_PROBE_ID) ?? manifest[0]; + if (routeId === lastRouteIdRef.current) return; + lastRouteIdRef.current = routeId; - if (selectedProbeId && !currentSelected) { - const fallback = routeEntry ?? getDefaultProbe(); - if (fallback && fallback.id !== selectedProbeId) { - selectProbe(fallback.id); - } + if (!routeId) { + // Landed on the catalog: clear any selection so the sync effect does not + // pull us back into a probe view. + if (useAppStore.getState().selectedProbeId) selectProbe(undefined); return; } - if (!selectedProbeId) { - if (routeEntry) { - selectProbe(routeEntry.id); - } else { - const fallback = getDefaultProbe(); - if (fallback) { - selectProbe(fallback.id); - } - } + const routeEntry = manifestById.get(routeId); + if (routeEntry && routeEntry.id !== useAppStore.getState().selectedProbeId) { + selectProbe(routeEntry.id); } - }, [ - manifestStatus, - manifest, - manifestById, - manufacturer, - model, - selectedProbeId, - selectProbe, - ]); + }, [manifestStatus, manifest, manifestById, manufacturer, model, selectProbe]); - // sync: store -> URL path + // sync: store -> URL path. useEffect(() => { - if ( - manifestStatus !== "success" || - !selectedProbeId || - manifest.length === 0 - ) { - return; - } + if (manifestStatus !== "success" || manifest.length === 0) return; - const selectedEntry = manifestById.get(selectedProbeId); - if (!selectedEntry) { - return; - } + // Read the live selection: if the select effect cleared it in this same + // commit (landing), we must see that and not navigate back into a probe. + const liveSelected = useAppStore.getState().selectedProbeId; + if (!liveSelected) return; + + const selectedEntry = manifestById.get(liveSelected); + if (!selectedEntry) return; const routeId = manufacturer && model ? `${manufacturer}:${model}` : undefined; - if (routeId === selectedEntry.id) { - return; - } + if (routeId === selectedEntry.id) return; const targetPath = `/probes/${selectedEntry.manufacturer}/${selectedEntry.model}`; - const replace = location.pathname === "/"; - navigate(targetPath, { replace }); + navigate(targetPath, { replace: location.pathname === "/" }); }, [ manifestStatus, - selectedProbeId, + manifest, manifestById, manufacturer, model, + selectedProbeId, navigate, location.pathname, - manifest.length, ]); } diff --git a/apps/probe-viewer/src/state/useRestoreCameraFromUrl.ts b/apps/probe-viewer/src/state/useRestoreCameraFromUrl.ts index 3113ed8..4d830dc 100644 --- a/apps/probe-viewer/src/state/useRestoreCameraFromUrl.ts +++ b/apps/probe-viewer/src/state/useRestoreCameraFromUrl.ts @@ -3,44 +3,70 @@ import { useSearchParams } from "react-router-dom"; import { useAppStore } from "./useAppStore"; -// Reads the camera params (zoom/cx/cy) from a shared link into the store. -function restoreCameraFromParams( +// Reads the camera (zoom/cx/cy) and view toggles (ids/scale/overview) from a +// shared link into the store. +function restoreViewFromParams( searchParams: URLSearchParams, setZoom: (zoom: number) => void, setViewCenter: (x: number | null, y: number | null) => void, + toggleContactIds: (value?: boolean) => void, + toggleScaleBar: (value?: boolean) => void, + toggleOverview: (value?: boolean) => void, ) { const zoomParam = searchParams.get("zoom"); - const cxParam = searchParams.get("cx"); - const cyParam = searchParams.get("cy"); - if (zoomParam) { const zoom = parseFloat(zoomParam); if (!isNaN(zoom)) setZoom(zoom); } // A center needs both coordinates. If either is missing from the URL // (e.g. a link that only set zoom), there is no center to restore. + const cxParam = searchParams.get("cx"); + const cyParam = searchParams.get("cy"); const cx = cxParam ? parseFloat(cxParam) : NaN; const cy = cyParam ? parseFloat(cyParam) : NaN; - const hasCenter = !isNaN(cx) && !isNaN(cy); - if (hasCenter) { + if (!isNaN(cx) && !isNaN(cy)) { setViewCenter(cx, cy); } + + // Toggles are applied only when present, so a link that omits a flag leaves + // that toggle at its default. + if (searchParams.has("ids")) toggleContactIds(searchParams.get("ids") === "1"); + if (searchParams.has("scale")) toggleScaleBar(searchParams.get("scale") === "1"); + if (searchParams.has("overview")) toggleOverview(searchParams.get("overview") === "1"); } -// restore: URL -> store, once per page load. Applies a shared link's camera on -// mount, then flips `cameraInitialized` so the URL writer is allowed to start. -// The restore-before-write ordering this guarantees is documented on -// `cameraInitialized` in the store; useSyncCameraToUrl is the other half. +// restore: URL -> store, once per page load. Applies a shared link's camera and +// view toggles on mount, then flips `cameraInitialized` so the URL writer is +// allowed to start. useSyncCameraToUrl is the other half. export function useRestoreCameraFromUrl() { const [searchParams] = useSearchParams(); const cameraInitialized = useAppStore((state) => state.cameraInitialized); const setZoom = useAppStore((state) => state.setZoom); const setViewCenter = useAppStore((state) => state.setViewCenter); + const toggleContactIds = useAppStore((state) => state.toggleContactIds); + const toggleScaleBar = useAppStore((state) => state.toggleScaleBar); + const toggleOverview = useAppStore((state) => state.toggleOverview); const markCameraInitialized = useAppStore((state) => state.markCameraInitialized); useEffect(() => { if (cameraInitialized) return; - restoreCameraFromParams(searchParams, setZoom, setViewCenter); + restoreViewFromParams( + searchParams, + setZoom, + setViewCenter, + toggleContactIds, + toggleScaleBar, + toggleOverview, + ); markCameraInitialized(); - }, [cameraInitialized, searchParams, setZoom, setViewCenter, markCameraInitialized]); + }, [ + cameraInitialized, + searchParams, + setZoom, + setViewCenter, + toggleContactIds, + toggleScaleBar, + toggleOverview, + markCameraInitialized, + ]); } diff --git a/apps/probe-viewer/src/state/useSyncCameraToUrl.ts b/apps/probe-viewer/src/state/useSyncCameraToUrl.ts index c513997..2e7637e 100644 --- a/apps/probe-viewer/src/state/useSyncCameraToUrl.ts +++ b/apps/probe-viewer/src/state/useSyncCameraToUrl.ts @@ -12,19 +12,44 @@ function roundForUrl(value: number, decimals = 1): number { return Math.round(value * factor) / factor; } -// Writes the current camera into the query string, dropping the params entirely -// when the camera is back at its default (zoom 1, no center). -function writeCameraToParams( +interface ViewFlags { + showContactIds: boolean; + showScaleBar: boolean; + showOverview: boolean; +} + +// A flag is written to the URL only when it differs from its default, so a +// default view carries no flag params at all. +const FLAG_DEFAULTS: ViewFlags = { + showContactIds: false, + showScaleBar: true, + showOverview: true, +}; + +function setOrDeleteFlag( + params: URLSearchParams, + key: string, + value: boolean, + defaultValue: boolean, +) { + if (value === defaultValue) params.delete(key); + else params.set(key, value ? "1" : "0"); +} + +// Writes the camera and the view toggles into the query string, dropping each +// back out when it returns to its default so a default view has a clean URL. +function writeViewToParams( camera: ProbeViewerCamera, + flags: ViewFlags, setSearchParams: SetURLSearchParams, ) { const { zoom, centerX, centerY } = camera; - const isDefault = zoom === 1 && centerX === null && centerY === null; + const isDefaultCamera = zoom === 1 && centerX === null && centerY === null; setSearchParams( (prev) => { const next = new URLSearchParams(prev); - if (isDefault) { + if (isDefaultCamera) { next.delete("zoom"); next.delete("cx"); next.delete("cy"); @@ -38,19 +63,25 @@ function writeCameraToParams( next.delete("cy"); } } + setOrDeleteFlag(next, "ids", flags.showContactIds, FLAG_DEFAULTS.showContactIds); + setOrDeleteFlag(next, "scale", flags.showScaleBar, FLAG_DEFAULTS.showScaleBar); + setOrDeleteFlag(next, "overview", flags.showOverview, FLAG_DEFAULTS.showOverview); return next; }, { replace: true }, ); } -// sync: store -> URL, debounced. Writes the current camera into the query string -// on every zoom/pan, but only once `cameraInitialized` is set, so it can't wipe -// a shared link's params before useRestoreCameraFromUrl has read them. The -// restore-before-write ordering is documented on `cameraInitialized` in the store. +// sync: store -> URL, debounced. Writes the camera and view toggles into the +// query string on every change, but only once `cameraInitialized` is set, so it +// can't wipe a shared link's params before useRestoreCameraFromUrl has read them. +// The restore-before-write ordering is documented on `cameraInitialized`. export function useSyncCameraToUrl() { const [, setSearchParams] = useSearchParams(); const camera = useAppStore((state) => state.view.camera); + const showContactIds = useAppStore((state) => state.view.showContactIds); + const showScaleBar = useAppStore((state) => state.view.showScaleBar); + const showOverview = useAppStore((state) => state.view.showOverview); const cameraInitialized = useAppStore((state) => state.cameraInitialized); const writeTimeout = useRef | undefined>(undefined); @@ -59,9 +90,13 @@ export function useSyncCameraToUrl() { clearTimeout(writeTimeout.current); writeTimeout.current = setTimeout(() => { - writeCameraToParams(camera, setSearchParams); + writeViewToParams( + camera, + { showContactIds, showScaleBar, showOverview }, + setSearchParams, + ); }, 300); return () => clearTimeout(writeTimeout.current); - }, [cameraInitialized, camera, setSearchParams]); + }, [cameraInitialized, camera, showContactIds, showScaleBar, showOverview, setSearchParams]); } From acc9da9b6adb7a2426cde07b300202492d2dcc5d Mon Sep 17 00:00:00 2001 From: Heberto Mayorquin Date: Wed, 24 Jun 2026 22:48:56 -0600 Subject: [PATCH 2/7] graphical improvements --- apps/probe-viewer/src/App.css | 247 +++++++++++++----- .../src/components/ProbeViewer.tsx | 98 +++---- apps/probe-viewer/src/components/Sidebar.tsx | 16 +- 3 files changed, 242 insertions(+), 119 deletions(-) diff --git a/apps/probe-viewer/src/App.css b/apps/probe-viewer/src/App.css index d68b40b..5bcf6c2 100644 --- a/apps/probe-viewer/src/App.css +++ b/apps/probe-viewer/src/App.css @@ -1,13 +1,36 @@ +:root { + /* "Instrument" palette: deep ink + cool neutrals, one restrained accent. The + warm gold/bronze is reserved for the probe rendering (the hero), so it is + intentionally absent from the chrome. */ + --ink: #0b1220; + --ink-2: #334155; + --muted: #64748b; + --line: #dbe2ea; + --line-strong: #c3ccd8; + --surface: #ffffff; + --bg: #eceff3; + --bg-2: #e1e6ed; + /* Monochrome graphite accent: the chrome carries no hue, so the warm gold of + the probe is the only color in the app. */ + --accent: #334155; + --accent-strong: #0f172a; + --accent-soft: rgba(15, 23, 42, 0.06); + --radius: 0.6rem; + --radius-lg: 0.9rem; + --shadow-sm: 0 1px 2px rgba(15, 23, 42, 0.05); + --shadow: 0 10px 30px rgba(15, 23, 42, 0.08); +} + .app-shell { display: flex; height: 100%; overflow: hidden; - background: linear-gradient(180deg, #f8fafc 0%, #e2e8f0 100%); + background: linear-gradient(180deg, var(--bg) 0%, var(--bg-2) 100%); } .app-sidebar { width: 320px; - border-right: 1px solid rgba(15, 23, 42, 0.08); + border-right: 1px solid var(--line); background: rgba(255, 255, 255, 0.9); backdrop-filter: blur(6px); } @@ -28,7 +51,7 @@ gap: 1.25rem; height: 100%; padding: 2rem 1.5rem; - color: #0f172a; + color: var(--ink); } .sidebar-header { @@ -38,13 +61,16 @@ } .sidebar-title { - font-size: 1.5rem; + font-size: 1.4rem; margin: 0; + font-weight: 700; + letter-spacing: -0.02em; + color: var(--ink); } .sidebar-subtitle { margin: 0; - color: #475569; + color: var(--ink-2); font-size: 0.95rem; } @@ -57,23 +83,23 @@ .sidebar-label { font-size: 0.85rem; font-weight: 600; - color: #475569; + color: var(--ink-2); } .sidebar select, .sidebar input { font: inherit; padding: 0.5rem 0.75rem; - border-radius: 0.6rem; - border: 1px solid rgba(100, 116, 139, 0.3); - background-color: #f8fafc; + border-radius: var(--radius); + border: 1px solid var(--line-strong); + background-color: var(--bg); transition: border-color 0.2s ease, box-shadow 0.2s ease; } .sidebar select:focus, .sidebar input:focus { - border-color: #2563eb; - box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.15); + border-color: var(--accent); + box-shadow: 0 0 0 3px var(--accent-soft); outline: none; } @@ -100,32 +126,34 @@ gap: 0.25rem; width: 100%; padding: 0.8rem 0.9rem; - border-radius: 0.9rem; - border: 1px solid rgba(148, 163, 184, 0.4); - background: rgba(248, 250, 252, 0.8); + border-radius: var(--radius-lg); + border: 1px solid var(--line); + background: var(--surface); cursor: pointer; - transition: background 0.2s ease, border-color 0.2s ease, transform 0.1s ease; + transition: background 0.2s ease, border-color 0.2s ease, box-shadow 0.2s ease; } .sidebar-item:hover { - border-color: rgba(37, 99, 235, 0.4); - background: rgba(191, 219, 254, 0.35); + border-color: var(--line-strong); + background: var(--accent-soft); } +/* Active probe: a graphite left bar (inset shadow, so no layout shift) plus a + darker border. Monochrome, distinct from the lighter hover fill. */ .sidebar-item--active { - border-color: rgba(37, 99, 235, 0.8); - background: rgba(191, 219, 254, 0.6); - box-shadow: 0 8px 18px rgba(37, 99, 235, 0.15); + border-color: var(--accent-strong); + background: var(--accent-soft); + box-shadow: inset 3px 0 0 var(--accent-strong), var(--shadow-sm); } .sidebar-item-name { font-weight: 600; - color: #0f172a; + color: var(--ink); } .sidebar-item-meta { font-size: 0.8rem; - color: #475569; + color: var(--ink-2); } /* Neuropixels hierarchy grouping */ @@ -144,19 +172,19 @@ border: none; background: transparent; cursor: pointer; - color: #0f172a; + color: var(--ink); font-weight: 700; font-size: 0.95rem; - border-bottom: 1px solid rgba(148, 163, 184, 0.35); + border-bottom: 1px solid var(--line); } .sidebar-group-header:hover { - color: #2563eb; + color: var(--accent-strong); } .sidebar-group-caret { width: 1rem; - color: #64748b; + color: var(--muted); } .sidebar-group-title { @@ -167,8 +195,8 @@ .sidebar-group-count { font-size: 0.75rem; font-weight: 600; - color: #475569; - background: rgba(148, 163, 184, 0.25); + color: var(--ink-2); + background: var(--accent-soft); border-radius: 999px; padding: 0.05rem 0.5rem; } @@ -192,7 +220,7 @@ } .sidebar-subgroup-header:hover .sidebar-subgroup-title { - color: #2563eb; + color: var(--accent-strong); } .sidebar-subgroup-title { @@ -202,7 +230,7 @@ font-weight: 700; text-transform: uppercase; letter-spacing: 0.03em; - color: #64748b; + color: var(--muted); } .sidebar-subdivision { @@ -218,12 +246,12 @@ font-weight: 600; text-transform: uppercase; letter-spacing: 0.04em; - color: #94a3b8; + color: var(--muted); } .sidebar-hint { font-size: 0.9rem; - color: #64748b; + color: var(--muted); margin: 0.5rem 0; } @@ -241,10 +269,11 @@ gap: 1rem; max-width: 960px; width: 100%; - background: rgba(255, 255, 255, 0.95); - border-radius: 1.25rem; + background: var(--surface); + border: 1px solid var(--line); + border-radius: var(--radius-lg); padding: 2rem; - box-shadow: 0 20px 45px rgba(15, 23, 42, 0.08); + box-shadow: var(--shadow); } .viewer-header { @@ -256,7 +285,10 @@ .viewer-title { margin: 0; - font-size: 1.75rem; + font-size: 1.7rem; + font-weight: 700; + letter-spacing: -0.02em; + color: var(--ink); } /* "{ } JSON" link inline in the subtitle metadata line. It uses the blue link @@ -268,7 +300,7 @@ gap: 0.25rem; vertical-align: middle; /* align the icon + text with the surrounding subtitle text */ font-weight: 600; - color: #2563eb; + color: var(--accent); text-decoration: none; transition: color 0.2s ease; } @@ -279,7 +311,7 @@ } .viewer-json-link:hover { - color: #1d4ed8; + color: var(--accent-strong); } .viewer-subtitle { @@ -288,46 +320,108 @@ font-size: 0.95rem; } -.viewer-controls { +/* Map-style overlay clusters floating in the canvas corners, so the probe owns + the frame and the chrome sits out of the way until you reach for it. */ +.canvas-controls { + position: absolute; + top: 0.75rem; + z-index: 4; display: flex; flex-wrap: wrap; + gap: 0.4rem; +} + +.canvas-controls--nav { + left: 0.75rem; +} + +.canvas-controls button { + display: inline-flex; align-items: center; - justify-content: space-between; - gap: 0.75rem; + justify-content: center; + gap: 0.35rem; + font: inherit; + font-size: 0.9rem; + padding: 0.4rem 0.75rem; + border-radius: var(--radius); + border: 1px solid var(--line-strong); + background: var(--surface); + color: var(--ink-2); + cursor: pointer; + box-shadow: var(--shadow-sm); + transition: color 0.15s ease, background 0.15s ease, border-color 0.15s ease; } -.viewer-controls-group { +.canvas-controls button:hover { + border-color: var(--accent); + color: var(--accent-strong); + background: var(--accent-soft); +} + +/* Control bands above (framing) and below (display preferences) the canvas, so + the canvas itself carries only the zoom overlay and nothing occludes the + probe. */ +.viewer-toolbar { display: flex; + flex-wrap: wrap; + align-items: center; gap: 0.5rem; } -.viewer-controls button { +.viewer-toolbar button { display: inline-flex; align-items: center; justify-content: center; gap: 0.35rem; font: inherit; + font-size: 0.9rem; padding: 0.45rem 0.9rem; - border-radius: 0.75rem; - border: 1px solid rgba(37, 99, 235, 0.4); - background: rgba(191, 219, 254, 0.45); - color: #1d4ed8; + border-radius: var(--radius); + border: 1px solid var(--line-strong); + background: var(--surface); + color: var(--ink-2); cursor: pointer; - transition: transform 0.1s ease, box-shadow 0.2s ease, background 0.2s ease; + transition: color 0.15s ease, background 0.15s ease, border-color 0.15s ease; } -.viewer-controls button:hover { - transform: translateY(-1px); - box-shadow: 0 10px 18px rgba(37, 99, 235, 0.2); - background: rgba(191, 219, 254, 0.7); +.viewer-toolbar button:hover { + border-color: var(--accent); + color: var(--accent-strong); + background: var(--accent-soft); } +/* Toggles styled as part of the button family: an outline chip that fills with + the accent when on, so they sit alongside the action buttons consistently. */ .viewer-toggle { display: inline-flex; align-items: center; - gap: 0.4rem; + gap: 0.45rem; font-size: 0.9rem; - color: #1e293b; + padding: 0.45rem 0.8rem; + border-radius: var(--radius); + border: 1px solid var(--line-strong); + background: var(--surface); + color: var(--ink-2); + cursor: pointer; + user-select: none; + transition: color 0.15s ease, background 0.15s ease, border-color 0.15s ease; +} + +.viewer-toggle:hover { + border-color: var(--accent); + color: var(--accent-strong); +} + +.viewer-toggle:has(input:checked) { + border-color: var(--accent); + background: var(--accent-soft); + color: var(--accent-strong); +} + +.viewer-toggle input { + accent-color: var(--accent); + margin: 0; + cursor: pointer; } .viewer-canvas { @@ -377,12 +471,12 @@ gap: 0.35rem; font-size: 0.9rem; padding: 0.5rem 0.75rem; - border-radius: 0.75rem; - border: 1px solid rgba(37, 99, 235, 0.5); - background: transparent; - color: #2563eb; + border-radius: var(--radius); + border: 1px solid var(--line-strong); + background: var(--surface); + color: var(--ink-2); text-decoration: none; - transition: all 0.2s ease; + transition: color 0.15s ease, background 0.15s ease, border-color 0.15s ease; cursor: pointer; } @@ -393,7 +487,9 @@ } .viewer-download:hover { - background: rgba(37, 99, 235, 0.1); + border-color: var(--accent); + color: var(--accent-strong); + background: var(--accent-soft); } .viewer-canvas-placeholder { @@ -429,7 +525,7 @@ } .viewer-issue-link a:hover { - color: #2563eb; + color: var(--accent-strong); text-decoration: underline; } @@ -477,14 +573,16 @@ } .index-title { - margin: 0 0 0.25rem; - font-size: 1.6rem; - color: #0f172a; + margin: 0 0 0.3rem; + font-size: 1.8rem; + font-weight: 700; + letter-spacing: -0.02em; + color: var(--ink); } .index-subtitle { margin: 0 0 1rem; - color: #475569; + color: var(--muted); } .index-controls { @@ -537,9 +635,10 @@ flex-direction: column; text-align: left; padding: 0; - border: 1px solid rgba(148, 163, 184, 0.45); - border-radius: 0.9rem; - background: #fff; + border: 1px solid var(--line); + border-radius: var(--radius-lg); + background: var(--surface); + box-shadow: var(--shadow-sm); cursor: pointer; overflow: hidden; font: inherit; @@ -548,8 +647,8 @@ .probe-card:hover { transform: translateY(-2px); - border-color: rgba(37, 99, 235, 0.6); - box-shadow: 0 12px 22px rgba(15, 23, 42, 0.12); + border-color: var(--accent); + box-shadow: var(--shadow); } .probe-card-image { @@ -650,6 +749,9 @@ } .sidebar-home { + display: inline-flex; + align-items: center; + gap: 0.35rem; align-self: flex-start; background: none; border: none; @@ -658,10 +760,11 @@ font: inherit; font-size: 0.85rem; font-weight: 600; - color: #2563eb; + color: var(--muted); cursor: pointer; + transition: color 0.15s ease; } .sidebar-home:hover { - text-decoration: underline; + color: var(--ink); } diff --git a/apps/probe-viewer/src/components/ProbeViewer.tsx b/apps/probe-viewer/src/components/ProbeViewer.tsx index ef64ce8..499d49f 100644 --- a/apps/probe-viewer/src/components/ProbeViewer.tsx +++ b/apps/probe-viewer/src/components/ProbeViewer.tsx @@ -362,58 +362,16 @@ export function ProbeViewer() {

-
-
- - + {status !== "error" && probeData && ( +
-
-
- {hasContactIds && ( - - )} - - -
-
+ + )}
{status === "error" && ( @@ -442,6 +400,23 @@ export function ProbeViewer() { onViewCenterChange={(x, y) => setViewCenter(x, y)} /> )} + +
+ + +
)} {status === "loading" && ( @@ -451,6 +426,37 @@ export function ProbeViewer() { )}
+ {status !== "error" && probeData && ( +
+ {hasContactIds && ( + + )} + + +
+ )} +
- ← Home + + Home

Probe Catalog

From 3713f0c7113db2be3c7cecd93c8cec5258ead582 Mon Sep 17 00:00:00 2001 From: Heberto Mayorquin Date: Wed, 24 Jun 2026 23:19:13 -0600 Subject: [PATCH 3/7] improvements --- apps/probe-viewer/src/App.css | 20 +++++++++++--- .../src/components/ProbeViewer.tsx | 27 +++++++++++++++++++ 2 files changed, 44 insertions(+), 3 deletions(-) diff --git a/apps/probe-viewer/src/App.css b/apps/probe-viewer/src/App.css index 5bcf6c2..d7c0d14 100644 --- a/apps/probe-viewer/src/App.css +++ b/apps/probe-viewer/src/App.css @@ -418,10 +418,24 @@ color: var(--accent-strong); } +/* Keyboard focus ring on the chip, since the real checkbox is visually hidden. */ +.viewer-toggle:has(input:focus-visible) { + outline: 2px solid var(--accent); + outline-offset: 2px; +} + +/* The checkbox stays in the DOM (and keyboard-focusable) for accessibility, but + is visually hidden: the chip's pressed fill is what conveys on/off. */ .viewer-toggle input { - accent-color: var(--accent); - margin: 0; - cursor: pointer; + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0 0 0 0); + white-space: nowrap; + border: 0; } .viewer-canvas { diff --git a/apps/probe-viewer/src/components/ProbeViewer.tsx b/apps/probe-viewer/src/components/ProbeViewer.tsx index 499d49f..5b5ffdb 100644 --- a/apps/probe-viewer/src/components/ProbeViewer.tsx +++ b/apps/probe-viewer/src/components/ProbeViewer.tsx @@ -136,6 +136,30 @@ const JsonIcon = ( ); +// Leading icons for the view-toggle chips: an eye (show/hide contact IDs), an +// I-beam matching the on-canvas scale bar, and a minimap frame for the overview. +const EyeIcon = ( + +); + +const ScaleBarIcon = ( + +); + +const MinimapIcon = ( + +); + export function ProbeViewer() { const manifest = useAppStore((state) => state.manifest); const manifestStatus = useAppStore((state) => state.manifestStatus); @@ -435,6 +459,7 @@ export function ProbeViewer() { checked={view.showContactIds} onChange={(event) => toggleContactIds(event.target.checked)} /> + {EyeIcon} Show contact IDs )} @@ -444,6 +469,7 @@ export function ProbeViewer() { checked={view.showScaleBar} onChange={(event) => toggleScaleBar(event.target.checked)} /> + {ScaleBarIcon} Scale bar From a5ac7cc778d32627d8bfb3a5ab8d8bf278488d94 Mon Sep 17 00:00:00 2001 From: Heberto Mayorquin Date: Wed, 24 Jun 2026 23:27:04 -0600 Subject: [PATCH 4/7] probe looks better --- .../src/components/ProbeCanvas.tsx | 122 ++++++++++++------ 1 file changed, 81 insertions(+), 41 deletions(-) diff --git a/apps/probe-viewer/src/components/ProbeCanvas.tsx b/apps/probe-viewer/src/components/ProbeCanvas.tsx index 78d3bd2..9a669e8 100644 --- a/apps/probe-viewer/src/components/ProbeCanvas.tsx +++ b/apps/probe-viewer/src/components/ProbeCanvas.tsx @@ -226,73 +226,113 @@ export const ProbeCanvas = forwardRef( const contactShapes = probe.contact_shapes ?? []; const contactShapeParams = probe.contact_shape_params ?? []; - // Helper to draw a contact shape - const drawContactShape = ( - x: number, - y: number, - shape: string, - params: ContactShapeParams, - ) => { - ctx.beginPath(); + // A contact's pixel dimensions, used both to size the metallic gradient and + // to skip the sheen on pads too small for it to register. + const contactDims = (shape: string, params: ContactShapeParams) => { switch (shape) { case "circle": { - const radius = (params.radius ?? 5) * scale; - ctx.arc(x, y, radius, 0, Math.PI * 2); - break; + const d = (params.radius ?? 5) * 2 * scale; + return { w: d, h: d, minPx: d, gradient: true }; } case "square": { - const side = (params.width ?? 10) * scale; - ctx.rect(x - side / 2, y - side / 2, side, side); - break; + const s = (params.width ?? 10) * scale; + return { w: s, h: s, minPx: s, gradient: true }; } case "rect": { const w = (params.width ?? 10) * scale; const h = (params.height ?? 15) * scale; - ctx.rect(x - w / 2, y - h / 2, w, h); + return { w, h, minPx: Math.min(w, h), gradient: true }; + } + default: + return { w: 0, h: 0, minPx: 0, gradient: false }; + } + }; + + // Draws one contact path centered on the current origin (callers translate + // the context to the pad position first). Rectangular pads get lightly + // rounded corners so they read as real electrode pads, not hard tiles. + const drawContactShape = (shape: string, params: ContactShapeParams) => { + ctx.beginPath(); + switch (shape) { + case "circle": { + const radius = (params.radius ?? 5) * scale; + ctx.arc(0, 0, radius, 0, Math.PI * 2); + break; + } + case "square": + case "rect": { + const w = (params.width ?? 10) * scale; + const h = (shape === "square" ? (params.width ?? 10) : (params.height ?? 15)) * scale; + const r = Math.min(w, h) * 0.18; + if (typeof ctx.roundRect === "function") { + ctx.roundRect(-w / 2, -h / 2, w, h, r); + } else { + ctx.rect(-w / 2, -h / 2, w, h); + } break; } default: { - // Unknown/missing shape: draw a dot with X to indicate missing data + // Unknown/missing shape: a dot with an X to flag missing data. const markerSize = Math.max(3, Math.min(10, 7 * (scale / 100))); - // Draw small circle - ctx.arc(x, y, markerSize * 0.4, 0, Math.PI * 2); - ctx.closePath(); - // Draw X through the center - ctx.moveTo(x - markerSize, y - markerSize); - ctx.lineTo(x + markerSize, y + markerSize); - ctx.moveTo(x + markerSize, y - markerSize); - ctx.lineTo(x - markerSize, y + markerSize); + ctx.arc(0, 0, markerSize * 0.4, 0, Math.PI * 2); + ctx.moveTo(-markerSize, -markerSize); + ctx.lineTo(markerSize, markerSize); + ctx.moveTo(markerSize, -markerSize); + ctx.lineTo(-markerSize, markerSize); } } }; - // Shadow offset for depth effect - subtle, proportional to scale - const shadowOffset = 0.4 * scale; // 0.4 micrometer offset for subtle depth + // One metallic gold gradient per distinct pad size per frame. Contacts are + // usually uniform, so this is built once and reused across all of them. + const gradientCache = new Map(); + const goldGradient = (w: number, h: number) => { + const key = `${Math.round(w)}x${Math.round(h)}`; + let g = gradientCache.get(key); + if (!g) { + g = ctx.createLinearGradient(-w / 2, -h / 2, w / 2, h / 2); + g.addColorStop(0, "rgba(248, 228, 156, 1)"); // warm highlight (top-left) + g.addColorStop(0.45, "rgba(212, 175, 55, 1)"); // gold body + g.addColorStop(1, "rgba(146, 108, 28, 1)"); // deep bronze (bottom-right) + gradientCache.set(key, g); + } + return g; + }; - // First pass: draw shadows (offset dark shapes) contactPositions.forEach((position, index) => { const [x, y] = projectPoint(position); const shape = contactShapes[index] ?? ""; const params = contactShapeParams[index] ?? {}; + const dims = contactDims(shape, params); - drawContactShape(x + shadowOffset, y + shadowOffset, shape, params); - ctx.fillStyle = "rgba(30, 20, 5, 0.7)"; // Even darker and more opaque + ctx.save(); + ctx.translate(x, y); + drawContactShape(shape, params); + + // Metallic sheen when the pad is big enough to show it; below that a flat + // gold that looks identical at that size but is cheaper. + ctx.fillStyle = + dims.gradient && dims.minPx >= 5 + ? goldGradient(dims.w, dims.h) + : "rgba(212, 175, 55, 1)"; + + // A soft, capped shadow lifts the pad off the silver shank without the + // hard offset double-image the previous two-pass approach produced. + ctx.shadowColor = "rgba(15, 12, 4, 0.35)"; + ctx.shadowBlur = Math.min(5, Math.max(1.5, dims.minPx * 0.12)); + ctx.shadowOffsetX = 0; + ctx.shadowOffsetY = Math.min(2.5, Math.max(0.4, dims.minPx * 0.06)); ctx.fill(); - }); - // Second pass: draw gold contacts on top - contactPositions.forEach((position, index) => { - const [x, y] = projectPoint(position); - const shape = contactShapes[index] ?? ""; - const params = contactShapeParams[index] ?? {}; + // Clear the shadow before the rim so the outline stays crisp. + ctx.shadowColor = "transparent"; + ctx.shadowBlur = 0; + ctx.shadowOffsetY = 0; - drawContactShape(x, y, shape, params); - - ctx.fillStyle = "rgba(212, 175, 55, 1.0)"; // Gold contacts - fully opaque to cover shadow - ctx.strokeStyle = "rgba(80, 60, 15, 0.9)"; // Dark bronze outline - ctx.lineWidth = Math.max(1.2, 2.5 * (scale / 150)); - ctx.fill(); + ctx.lineWidth = Math.min(2, Math.max(0.8, dims.minPx * 0.03)); + ctx.strokeStyle = "rgba(110, 80, 25, 0.85)"; ctx.stroke(); + ctx.restore(); }); if (showContactIds && probe.contact_ids && idLabelInfo) { From 0c56e6d4159a53c1b28e3acfe1023112b2a0b042 Mon Sep 17 00:00:00 2001 From: Heberto Mayorquin Date: Thu, 25 Jun 2026 00:45:41 -0600 Subject: [PATCH 5/7] simple style --- .../src/components/ProbeCanvas.tsx | 84 ++++--------------- .../src/components/ProbeOverview.tsx | 14 ++-- apps/probe-viewer/src/utils/exportUtils.ts | 49 ++++------- 3 files changed, 42 insertions(+), 105 deletions(-) diff --git a/apps/probe-viewer/src/components/ProbeCanvas.tsx b/apps/probe-viewer/src/components/ProbeCanvas.tsx index 9a669e8..bd6be1b 100644 --- a/apps/probe-viewer/src/components/ProbeCanvas.tsx +++ b/apps/probe-viewer/src/components/ProbeCanvas.tsx @@ -207,18 +207,21 @@ export const ProbeCanvas = forwardRef( if (probe.probe_planar_contour && probe.probe_planar_contour.length > 1) { ctx.beginPath(); probe.probe_planar_contour.forEach((point, index) => { - const [x, y] = projectPoint(point); + const [px, py] = projectPoint(point); if (index === 0) { - ctx.moveTo(x, y); + ctx.moveTo(px, py); } else { - ctx.lineTo(x, y); + ctx.lineTo(px, py); } }); ctx.closePath(); - ctx.fillStyle = "rgba(180, 185, 195, 0.7)"; // Metallic silver - ctx.strokeStyle = "rgba(100, 105, 115, 0.95)"; - ctx.lineWidth = Math.max(1.2, 2.5 * (scale / 100)); + + // Technical line-art: a faint cool wash so the shank reads as a region, + // with a thin precise outline. No fill gradient or shadow. + ctx.fillStyle = "rgba(51, 65, 85, 0.05)"; ctx.fill(); + ctx.strokeStyle = "rgba(51, 65, 85, 0.9)"; + ctx.lineWidth = Math.max(1, Math.min(1.6, 2 * (scale / 120))); ctx.stroke(); } @@ -226,28 +229,6 @@ export const ProbeCanvas = forwardRef( const contactShapes = probe.contact_shapes ?? []; const contactShapeParams = probe.contact_shape_params ?? []; - // A contact's pixel dimensions, used both to size the metallic gradient and - // to skip the sheen on pads too small for it to register. - const contactDims = (shape: string, params: ContactShapeParams) => { - switch (shape) { - case "circle": { - const d = (params.radius ?? 5) * 2 * scale; - return { w: d, h: d, minPx: d, gradient: true }; - } - case "square": { - const s = (params.width ?? 10) * scale; - return { w: s, h: s, minPx: s, gradient: true }; - } - case "rect": { - const w = (params.width ?? 10) * scale; - const h = (params.height ?? 15) * scale; - return { w, h, minPx: Math.min(w, h), gradient: true }; - } - default: - return { w: 0, h: 0, minPx: 0, gradient: false }; - } - }; - // Draws one contact path centered on the current origin (callers translate // the context to the pad position first). Rectangular pads get lightly // rounded corners so they read as real electrode pads, not hard tiles. @@ -263,7 +244,7 @@ export const ProbeCanvas = forwardRef( case "rect": { const w = (params.width ?? 10) * scale; const h = (shape === "square" ? (params.width ?? 10) : (params.height ?? 15)) * scale; - const r = Math.min(w, h) * 0.18; + const r = Math.min(w, h) * 0.12; if (typeof ctx.roundRect === "function") { ctx.roundRect(-w / 2, -h / 2, w, h, r); } else { @@ -283,54 +264,21 @@ export const ProbeCanvas = forwardRef( } }; - // One metallic gold gradient per distinct pad size per frame. Contacts are - // usually uniform, so this is built once and reused across all of them. - const gradientCache = new Map(); - const goldGradient = (w: number, h: number) => { - const key = `${Math.round(w)}x${Math.round(h)}`; - let g = gradientCache.get(key); - if (!g) { - g = ctx.createLinearGradient(-w / 2, -h / 2, w / 2, h / 2); - g.addColorStop(0, "rgba(248, 228, 156, 1)"); // warm highlight (top-left) - g.addColorStop(0.45, "rgba(212, 175, 55, 1)"); // gold body - g.addColorStop(1, "rgba(146, 108, 28, 1)"); // deep bronze (bottom-right) - gradientCache.set(key, g); - } - return g; - }; - + // Flat gold contacts (the recognizable electrode convention), with a defined + // bronze outline and no gradient or shadow — focal without the metallic + // shine that was pulling focus. contactPositions.forEach((position, index) => { const [x, y] = projectPoint(position); const shape = contactShapes[index] ?? ""; const params = contactShapeParams[index] ?? {}; - const dims = contactDims(shape, params); ctx.save(); ctx.translate(x, y); drawContactShape(shape, params); - - // Metallic sheen when the pad is big enough to show it; below that a flat - // gold that looks identical at that size but is cheaper. - ctx.fillStyle = - dims.gradient && dims.minPx >= 5 - ? goldGradient(dims.w, dims.h) - : "rgba(212, 175, 55, 1)"; - - // A soft, capped shadow lifts the pad off the silver shank without the - // hard offset double-image the previous two-pass approach produced. - ctx.shadowColor = "rgba(15, 12, 4, 0.35)"; - ctx.shadowBlur = Math.min(5, Math.max(1.5, dims.minPx * 0.12)); - ctx.shadowOffsetX = 0; - ctx.shadowOffsetY = Math.min(2.5, Math.max(0.4, dims.minPx * 0.06)); + ctx.fillStyle = "rgba(212, 175, 55, 1)"; ctx.fill(); - - // Clear the shadow before the rim so the outline stays crisp. - ctx.shadowColor = "transparent"; - ctx.shadowBlur = 0; - ctx.shadowOffsetY = 0; - - ctx.lineWidth = Math.min(2, Math.max(0.8, dims.minPx * 0.03)); - ctx.strokeStyle = "rgba(110, 80, 25, 0.85)"; + ctx.lineWidth = Math.max(1, Math.min(1.8, 2.5 * (scale / 150))); + ctx.strokeStyle = "rgba(110, 80, 25, 0.9)"; ctx.stroke(); ctx.restore(); }); diff --git a/apps/probe-viewer/src/components/ProbeOverview.tsx b/apps/probe-viewer/src/components/ProbeOverview.tsx index 8ee3da5..d83a86b 100644 --- a/apps/probe-viewer/src/components/ProbeOverview.tsx +++ b/apps/probe-viewer/src/components/ProbeOverview.tsx @@ -121,8 +121,10 @@ export function ProbeOverview({ else ctx.lineTo(x, y); }); ctx.closePath(); - ctx.fillStyle = "rgba(180, 185, 195, 0.8)"; - ctx.strokeStyle = "rgba(100, 105, 115, 0.95)"; + // Technical line-art: a faint cool wash with a thin outline, matching the + // main canvas. + ctx.fillStyle = "rgba(51, 65, 85, 0.06)"; + ctx.strokeStyle = "rgba(51, 65, 85, 0.85)"; ctx.lineWidth = 1; ctx.fill(); ctx.stroke(); @@ -146,9 +148,9 @@ export function ProbeOverview({ const viewRectX = (effectiveViewCenterX - geometry.centerX) * minimapScale + offsetX - viewRectWidth / 2; const viewRectY = -(effectiveViewCenterY - geometry.centerY) * minimapScale + offsetY - viewRectHeight / 2; - // Draw viewport rectangle - ctx.strokeStyle = "rgba(59, 130, 246, 0.9)"; // Blue - ctx.fillStyle = "rgba(59, 130, 246, 0.15)"; + // Draw viewport rectangle (graphite accent, matching the monochrome chrome) + ctx.strokeStyle = "rgba(15, 23, 42, 0.85)"; + ctx.fillStyle = "rgba(15, 23, 42, 0.08)"; ctx.lineWidth = 2; ctx.fillRect(viewRectX, viewRectY, viewRectWidth, viewRectHeight); ctx.strokeRect(viewRectX, viewRectY, viewRectWidth, viewRectHeight); @@ -192,7 +194,7 @@ export function ProbeOverview({ ctx.fillText(label, barX + scaleBarPixels / 2, barY - 4); // Border around minimap - ctx.strokeStyle = "rgba(100, 105, 115, 0.5)"; + ctx.strokeStyle = "rgba(51, 65, 85, 0.3)"; ctx.lineWidth = 1; ctx.strokeRect(0.5, 0.5, MINIMAP_WIDTH - 1, MINIMAP_HEIGHT - 1); diff --git a/apps/probe-viewer/src/utils/exportUtils.ts b/apps/probe-viewer/src/utils/exportUtils.ts index d5f94fe..2f89da8 100644 --- a/apps/probe-viewer/src/utils/exportUtils.ts +++ b/apps/probe-viewer/src/utils/exportUtils.ts @@ -347,53 +347,52 @@ function generateProbeSvgString( const elements: string[] = []; - // Probe contour + // Probe contour: technical line-art — a faint cool wash so the shank reads as + // a region, with a thin precise outline. No fill gradient or shadow. if (probe.probe_planar_contour && probe.probe_planar_contour.length > 1) { const points = probe.probe_planar_contour .map((p) => projectPoint(p).join(",")) .join(" "); - const strokeWidth = Math.max(1.2, 2.5 * (scale / 100)); + const strokeWidth = Math.max(1, Math.min(1.6, 2 * (scale / 120))); elements.push( - `` + `` ); } const contactPositions = probe.contact_positions ?? []; const contactShapes = probe.contact_shapes ?? []; const contactShapeParams = probe.contact_shape_params ?? []; - const shadowOffset = 0.4 * scale; // 0.4 micrometer offset for subtle depth - const contactStrokeWidth = Math.max(1.2, 2.5 * (scale / 150)); - // Helper to generate contact SVG element + // Helper to generate one contact SVG element: flat gold (the electrode + // convention) with a defined bronze outline, no gradient or shadow. + const contactStrokeWidth = Math.max(1, Math.min(1.8, 2.5 * (scale / 150))); const generateContactSvg = ( x: number, y: number, shape: string, - params: ContactShapeParams, - isShadow: boolean + params: ContactShapeParams ): string => { - const fill = isShadow ? "rgba(30, 20, 5, 0.7)" : "rgba(212, 175, 55, 1.0)"; - const stroke = isShadow ? "none" : "rgba(80, 60, 15, 0.9)"; - const sw = isShadow ? 0 : contactStrokeWidth; - + const common = `fill="rgb(212, 175, 55)" stroke="rgb(110, 80, 25)" stroke-opacity="0.9" stroke-width="${contactStrokeWidth}"`; switch (shape) { case "circle": { const radius = (params.radius ?? 5) * scale; - return ``; + return ``; } case "square": { const side = (params.width ?? 10) * scale; - return ``; + const r = side * 0.12; + return ``; } case "rect": { const w = (params.width ?? 10) * scale; const h = (params.height ?? 15) * scale; - return ``; + const r = Math.min(w, h) * 0.12; + return ``; } default: { - // Unknown shape: small circle + // Unknown shape: a small plain gold dot. const markerSize = Math.max(3, Math.min(10, 7 * (scale / 100))); - return ``; + return ``; } } }; @@ -404,31 +403,19 @@ function generateProbeSvgString( const size = Math.max((p.radius ?? 0) * 2, p.width ?? 0, p.height ?? 0); return Math.max(max, size); }, 10); - const frameMargin = maxContactSizeUm * scale + shadowOffset; + const frameMargin = maxContactSizeUm * scale + 4; const isContactInFrame = (x: number, y: number) => x >= -frameMargin && x <= widthPx + frameMargin && y >= -frameMargin && y <= heightPx + frameMargin; - // First pass: shadows - contactPositions.forEach((position, index) => { - const [x, y] = projectPoint(position); - if (!isContactInFrame(x, y)) return; - const shape = contactShapes[index] ?? ""; - const params = contactShapeParams[index] ?? {}; - elements.push( - generateContactSvg(x + shadowOffset, y + shadowOffset, shape, params, true) - ); - }); - - // Second pass: gold contacts contactPositions.forEach((position, index) => { const [x, y] = projectPoint(position); if (!isContactInFrame(x, y)) return; const shape = contactShapes[index] ?? ""; const params = contactShapeParams[index] ?? {}; - elements.push(generateContactSvg(x, y, shape, params, false)); + elements.push(generateContactSvg(x, y, shape, params)); }); // Scale bar (L-shaped, bottom-left corner) From 989567bda286bf1d4db4d8bfbcfaea8a407424e0 Mon Sep 17 00:00:00 2001 From: Heberto Mayorquin Date: Thu, 25 Jun 2026 00:50:38 -0600 Subject: [PATCH 6/7] improve svg quality --- apps/probe-viewer/src/utils/exportUtils.ts | 72 +++++++++++++--------- 1 file changed, 43 insertions(+), 29 deletions(-) diff --git a/apps/probe-viewer/src/utils/exportUtils.ts b/apps/probe-viewer/src/utils/exportUtils.ts index 2f89da8..74ea67e 100644 --- a/apps/probe-viewer/src/utils/exportUtils.ts +++ b/apps/probe-viewer/src/utils/exportUtils.ts @@ -347,13 +347,20 @@ function generateProbeSvgString( const elements: string[] = []; + // Round emitted coordinates to 2 decimals: sub-pixel precision is invisible + // but keeps the markup compact and readable. + const r2 = (n: number) => Math.round(n * 100) / 100; + // Probe contour: technical line-art — a faint cool wash so the shank reads as // a region, with a thin precise outline. No fill gradient or shadow. if (probe.probe_planar_contour && probe.probe_planar_contour.length > 1) { const points = probe.probe_planar_contour - .map((p) => projectPoint(p).join(",")) + .map((p) => { + const [px, py] = projectPoint(p); + return `${r2(px)},${r2(py)}`; + }) .join(" "); - const strokeWidth = Math.max(1, Math.min(1.6, 2 * (scale / 120))); + const strokeWidth = r2(Math.max(1, Math.min(1.6, 2 * (scale / 120)))); elements.push( `` ); @@ -363,36 +370,35 @@ function generateProbeSvgString( const contactShapes = probe.contact_shapes ?? []; const contactShapeParams = probe.contact_shape_params ?? []; - // Helper to generate one contact SVG element: flat gold (the electrode - // convention) with a defined bronze outline, no gradient or shadow. - const contactStrokeWidth = Math.max(1, Math.min(1.8, 2.5 * (scale / 150))); + // Helper to generate one contact's geometry. The flat-gold style (fill, + // bronze outline) is applied once on the wrapping , not per element. + // Rectangular pads get lightly rounded corners. const generateContactSvg = ( x: number, y: number, shape: string, params: ContactShapeParams ): string => { - const common = `fill="rgb(212, 175, 55)" stroke="rgb(110, 80, 25)" stroke-opacity="0.9" stroke-width="${contactStrokeWidth}"`; switch (shape) { case "circle": { const radius = (params.radius ?? 5) * scale; - return ``; + return ``; } case "square": { const side = (params.width ?? 10) * scale; - const r = side * 0.12; - return ``; + const rr = r2(side * 0.12); + return ``; } case "rect": { const w = (params.width ?? 10) * scale; const h = (params.height ?? 15) * scale; - const r = Math.min(w, h) * 0.12; - return ``; + const rr = r2(Math.min(w, h) * 0.12); + return ``; } default: { - // Unknown shape: a small plain gold dot. + // Unknown shape: a small plain dot. const markerSize = Math.max(3, Math.min(10, 7 * (scale / 100))); - return ``; + return ``; } } }; @@ -410,13 +416,21 @@ function generateProbeSvgString( y >= -frameMargin && y <= heightPx + frameMargin; + // All contacts share one flat-gold style, set once on a group wrapper. + const contactEls: string[] = []; contactPositions.forEach((position, index) => { const [x, y] = projectPoint(position); if (!isContactInFrame(x, y)) return; const shape = contactShapes[index] ?? ""; const params = contactShapeParams[index] ?? {}; - elements.push(generateContactSvg(x, y, shape, params)); + contactEls.push(generateContactSvg(x, y, shape, params)); }); + if (contactEls.length > 0) { + const contactStrokeWidth = r2(Math.max(1, Math.min(1.8, 2.5 * (scale / 150)))); + elements.push( + `\n${contactEls.join("\n")}\n` + ); + } // Scale bar (L-shaped, bottom-left corner) if (showScaleBar) { @@ -434,26 +448,26 @@ function generateProbeSvgString( const tickSize = 4; const label = scaleBarUm >= 1000 ? `${scaleBarUm / 1000} mm` : `${scaleBarUm} μm`; - const strokeStyle = "rgba(15, 23, 42, 0.9)"; - - // L shape path - elements.push( - `` - ); - - // End ticks - elements.push( - `` - ); + const col = "rgba(15, 23, 42, 0.9)"; + const x0 = r2(cornerX); + const y0 = r2(cornerY); + const xEnd = r2(cornerX + scaleBarPixels); + const yTop = r2(cornerY - scaleBarPixels); - // X label (below horizontal arm) + // L shape + end ticks, sharing one stroke style on a group. elements.push( - `${label}` + `` + + `` + + `` + + `` ); - // Y label (rotated, to the left of vertical arm) + // Both labels share one text style on a group. elements.push( - `${label}` + `` + + `${label}` + + `${label}` + + `` ); } From 1ce3d3412ccdf1c028fbdf8de6de94b95aaea7d8 Mon Sep 17 00:00:00 2001 From: Alessio Buccino Date: Fri, 26 Jun 2026 10:41:13 +0200 Subject: [PATCH 7/7] docs: add instructions to preview webapp locally --- README.md | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 3352359..5d3300f 100644 --- a/README.md +++ b/README.md @@ -12,4 +12,15 @@ See: The format of probes is JSON-based. See [documentation](https://probeinterface.readthedocs.io/en/main/format_spec.html) for full specifications. - +### Preview `probe-viewer` app locally + +To build and preview the `probe-viewer` web-app locally: + +```bash +cd apps/probe-viewer +uv run build.py +# build +npm run build +# run +npx vite preview +```