Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
286 changes: 27 additions & 259 deletions client/package-lock.json

Large diffs are not rendered by default.

3 changes: 1 addition & 2 deletions client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,7 @@
"@czi-sds/components": "^22.11.4",
"@emotion/react": "^11.13.0",
"@emotion/styled": "^11.13.0",
"@idetik/core": "^0.7.5",
"@idetik/react": "^0.2.5",
"@idetik/core": "0.32.2",
"@mui/icons-material": "5.16.7",
"@mui/material": "5.16.7",
"@tanstack/react-query": "^5.60.0",
Expand Down
222 changes: 55 additions & 167 deletions client/src/components/overlays/InteractivePicksOverlay.tsx
Original file line number Diff line number Diff line change
@@ -1,37 +1,34 @@
/**
* Interactive picks overlay that supports both viewing and editing modes.
* Renders both read-only picks and editable picks being modified.
*
* When editing, the picks being edited are rendered from local state
* instead of fetched data, allowing real-time visual feedback.
*/

import { useEffect, useRef } from "react";
import { useIdetik } from "@idetik/react";
import { useMemo } from "react";
import { Idetik } from "@/idetik/Idetik";
import { PointsLayer } from "@/idetik/components/PointsLayer";
import { useCopick } from "@/contexts/CopickContext";
import { usePicking, type PickingPoint } from "@/contexts/PickingContext";
import { usePickPoints } from "@/api/hooks";
import { PicksLayer } from "./layers/PicksLayer";

const DEFAULT_POINT_SIZE_PIXELS = 30;
const DEFAULT_Z_FADE_FACTOR = 64.0;
const SELECTED_POINT_SIZE_PIXELS = 40; // Larger for selected points
const SELECTED_POINT_SIZE_PIXELS = 40; // larger for selected points

type Rgba = [number, number, number, number];

interface InteractivePicksOverlayProps {
viewer: Idetik | null;
currentZIndex: number;
voxelSpacing: number;
}

export function InteractivePicksOverlay({ currentZIndex, voxelSpacing }: InteractivePicksOverlayProps) {
export function InteractivePicksOverlay({ viewer, currentZIndex, voxelSpacing }: InteractivePicksOverlayProps) {
const { state: copickState } = useCopick();
const { state: pickingState, isEditing } = usePicking();

// Get all visible picks
const worldZ = currentZIndex * voxelSpacing;
const visiblePicks = copickState.selectedPicks.filter((p) => p.visible);

if (!viewer) {
return null;
}

return (
<>
{/* Read-only picks (excluding the one being edited) */}
{visiblePicks
.filter(
(pick) =>
Expand All @@ -41,182 +38,73 @@ export function InteractivePicksOverlay({ currentZIndex, voxelSpacing }: Interac
pick.sessionId !== pickingState.editingPicks?.sessionId
)
.map((pick) => (
<ReadOnlyPickPointsLayer
<ReadOnlyPicks
key={`${pick.objectName}-${pick.userId}-${pick.sessionId}`}
viewer={viewer}
objectName={pick.objectName}
userId={pick.userId}
sessionId={pick.sessionId}
currentZIndex={currentZIndex}
voxelSpacing={voxelSpacing}
worldZ={worldZ}
/>
))}

{/* Editable picks layer */}
{isEditing && pickingState.editingPicks && (
<EditablePicksLayer
<EditablePicks
viewer={viewer}
points={pickingState.localPoints}
selectedIds={pickingState.selectedPointIds}
color={pickingState.editingPicks.color}
currentZIndex={currentZIndex}
voxelSpacing={voxelSpacing}
worldZ={worldZ}
/>
)}
</>
);
}

// Read-only layer (same as existing PickPointsLayer)
interface ReadOnlyPickPointsLayerProps {
objectName: string;
userId: string;
sessionId: string;
currentZIndex: number;
voxelSpacing: number;
}

function ReadOnlyPickPointsLayer({
function ReadOnlyPicks({
viewer,
objectName,
userId,
sessionId,
currentZIndex,
voxelSpacing,
}: ReadOnlyPickPointsLayerProps) {
worldZ,
}: {
viewer: Idetik;
objectName: string;
userId: string;
sessionId: string;
worldZ: number;
}) {
const { state } = useCopick();
const { runtime } = useIdetik();
const layerRef = useRef<PicksLayer | null>(null);

const { data: picks } = usePickPoints(state.selectedRunName, objectName, userId, sessionId);

useEffect(() => {
if (!runtime || !picks || picks.points.length === 0) {
return;
}

const layerManager = runtime.viewports[0]?.layerManager;
if (!layerManager) return;

if (layerRef.current) {
layerManager.remove(layerRef.current);
layerRef.current = null;
}

const layer = new PicksLayer({
points: picks.points.map((pt) => ({ x: pt.x, y: pt.y, z: pt.z })),
color: picks.color,
pointSizePixels: DEFAULT_POINT_SIZE_PIXELS,
zFadeRadius: DEFAULT_Z_FADE_FACTOR,
});

const currentZInAngstroms = currentZIndex * voxelSpacing;
layer.setCurrentZ(currentZInAngstroms);

layerManager.add(layer);
layerRef.current = layer;

return () => {
if (layerRef.current && layerManager.layers.includes(layerRef.current)) {
layerManager.remove(layerRef.current);
layerRef.current = null;
}
};
}, [runtime, picks]);

useEffect(() => {
if (layerRef.current) {
const currentZInAngstroms = currentZIndex * voxelSpacing;
layerRef.current.setCurrentZ(currentZInAngstroms);
}
}, [currentZIndex, voxelSpacing]);

return null;
return (
<PointsLayer viewer={viewer} points={picks?.points} color={picks?.color} pointSizePixels={DEFAULT_POINT_SIZE_PIXELS} worldZ={worldZ} />
);
}

// Editable layer with selection highlighting
interface EditablePicksLayerProps {
function EditablePicks({
viewer,
points,
selectedIds,
color,
worldZ,
}: {
viewer: Idetik;
points: PickingPoint[];
selectedIds: Set<string>;
color: [number, number, number, number];
currentZIndex: number;
voxelSpacing: number;
}

function EditablePicksLayer({ points, selectedIds, color, currentZIndex, voxelSpacing }: EditablePicksLayerProps) {
const { runtime } = useIdetik();
const normalLayerRef = useRef<PicksLayer | null>(null);
const selectedLayerRef = useRef<PicksLayer | null>(null);

useEffect(() => {
if (!runtime) return;

const layerManager = runtime.viewports[0]?.layerManager;
if (!layerManager) return;

// Clean up old layers
if (normalLayerRef.current && layerManager.layers.includes(normalLayerRef.current)) {
layerManager.remove(normalLayerRef.current);
}
if (selectedLayerRef.current && layerManager.layers.includes(selectedLayerRef.current)) {
layerManager.remove(selectedLayerRef.current);
}
normalLayerRef.current = null;
selectedLayerRef.current = null;

const normalPoints = points.filter((p) => !selectedIds.has(p.id));
const selectedPoints = points.filter((p) => selectedIds.has(p.id));
const currentZInAngstroms = currentZIndex * voxelSpacing;

// Normal points layer
if (normalPoints.length > 0) {
const normalLayer = new PicksLayer({
points: normalPoints.map((p) => ({ x: p.x, y: p.y, z: p.z })),
color,
pointSizePixels: DEFAULT_POINT_SIZE_PIXELS,
zFadeRadius: DEFAULT_Z_FADE_FACTOR,
});
normalLayer.setCurrentZ(currentZInAngstroms);
layerManager.add(normalLayer);
normalLayerRef.current = normalLayer;
}

// Selected points layer (brighter, larger)
if (selectedPoints.length > 0) {
// Use a brighter version of the color for selection
const highlightColor: [number, number, number, number] = [
Math.min(255, color[0] + 80),
Math.min(255, color[1] + 80),
Math.min(255, color[2] + 80),
255,
];

const selectedLayer = new PicksLayer({
points: selectedPoints.map((p) => ({ x: p.x, y: p.y, z: p.z })),
color: highlightColor,
pointSizePixels: SELECTED_POINT_SIZE_PIXELS,
zFadeRadius: DEFAULT_Z_FADE_FACTOR,
});
selectedLayer.setCurrentZ(currentZInAngstroms);
layerManager.add(selectedLayer);
selectedLayerRef.current = selectedLayer;
}

return () => {
if (normalLayerRef.current && layerManager.layers.includes(normalLayerRef.current)) {
layerManager.remove(normalLayerRef.current);
}
if (selectedLayerRef.current && layerManager.layers.includes(selectedLayerRef.current)) {
layerManager.remove(selectedLayerRef.current);
}
normalLayerRef.current = null;
selectedLayerRef.current = null;
};
}, [runtime, points, selectedIds, color, currentZIndex, voxelSpacing]);

// Update z-position when slice changes (without recreating layers)
useEffect(() => {
const currentZInAngstroms = currentZIndex * voxelSpacing;
normalLayerRef.current?.setCurrentZ(currentZInAngstroms);
selectedLayerRef.current?.setCurrentZ(currentZInAngstroms);
}, [currentZIndex, voxelSpacing]);
color: Rgba;
worldZ: number;
}) {
const normalPoints = useMemo(() => points.filter((p) => !selectedIds.has(p.id)), [points, selectedIds]);
const selectedPoints = useMemo(() => points.filter((p) => selectedIds.has(p.id)), [points, selectedIds]);
const highlightColor = useMemo<Rgba>(
() => [Math.min(255, color[0] + 80), Math.min(255, color[1] + 80), Math.min(255, color[2] + 80), 255],
[color]
);

return null;
return (
<>
<PointsLayer viewer={viewer} points={normalPoints} color={color} pointSizePixels={DEFAULT_POINT_SIZE_PIXELS} worldZ={worldZ} />
<PointsLayer viewer={viewer} points={selectedPoints} color={highlightColor} pointSizePixels={SELECTED_POINT_SIZE_PIXELS} worldZ={worldZ} />
</>
);
}
Loading
Loading