Skip to content

Commit 821f7aa

Browse files
authored
Merge pull request #3 from copick/uermel/particle_picking
fix: add missing files for particle picking.
2 parents 26113f8 + bc8dfa1 commit 821f7aa

8 files changed

Lines changed: 1342 additions & 0 deletions

File tree

Lines changed: 296 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,296 @@
1+
/**
2+
* Enhanced picks panel with editing capabilities.
3+
* Extends PicksTable with create, edit, and delete functionality.
4+
*/
5+
6+
import { useState } from "react";
7+
import {
8+
Box,
9+
Table,
10+
TableBody,
11+
TableCell,
12+
TableContainer,
13+
TableHead,
14+
TableRow,
15+
IconButton,
16+
Typography,
17+
CircularProgress,
18+
Button,
19+
Tooltip,
20+
} from "@mui/material";
21+
import {
22+
Visibility,
23+
VisibilityOff,
24+
Add as AddIcon,
25+
Delete as DeleteIcon,
26+
Edit as EditIcon,
27+
Lock as LockIcon,
28+
} from "@mui/icons-material";
29+
import { usePicks, usePickPoints, useCreatePicks, useDeletePicks } from "@/api/hooks";
30+
import { useCopick } from "@/contexts/CopickContext";
31+
import { usePicking, type PickingPoint } from "@/contexts/PickingContext";
32+
import { rgbaToHex } from "@/utils/colorUtils";
33+
import { NewPickDialog } from "@/components/picking/NewPickDialog";
34+
import type { PicksSummaryResponse } from "@/api/types";
35+
36+
interface PicksPanelProps {
37+
runName: string;
38+
}
39+
40+
export function PicksPanel({ runName }: PicksPanelProps) {
41+
const { data: picks, isLoading, error } = usePicks(runName);
42+
const { state, togglePickVisibility, addPick } = useCopick();
43+
const { state: pickingState, isEditing } = usePicking();
44+
const createPicks = useCreatePicks();
45+
const deletePicks = useDeletePicks();
46+
47+
const [dialogOpen, setDialogOpen] = useState(false);
48+
49+
if (isLoading) {
50+
return (
51+
<Box sx={{ p: 2, display: "flex", justifyContent: "center" }}>
52+
<CircularProgress size={24} />
53+
</Box>
54+
);
55+
}
56+
57+
if (error) {
58+
return (
59+
<Typography color="error" sx={{ p: 2 }}>
60+
Failed to load picks
61+
</Typography>
62+
);
63+
}
64+
65+
const handleCreatePicks = async (objectName: string, userId: string, sessionId: string) => {
66+
await createPicks.mutateAsync({
67+
runName,
68+
data: { object_name: objectName, user_id: userId, session_id: sessionId },
69+
});
70+
};
71+
72+
const handleDeletePicks = async (pick: PicksSummaryResponse) => {
73+
if (window.confirm(`Delete picks for ${pick.object_name} by ${pick.user_id}?`)) {
74+
await deletePicks.mutateAsync({
75+
runName,
76+
objectName: pick.object_name,
77+
userId: pick.user_id,
78+
sessionId: pick.session_id,
79+
});
80+
}
81+
};
82+
83+
const handleToggle = (pick: PicksSummaryResponse) => {
84+
const existing = state.selectedPicks.find(
85+
(p) => p.objectName === pick.object_name && p.userId === pick.user_id && p.sessionId === pick.session_id
86+
);
87+
88+
if (existing) {
89+
togglePickVisibility(pick.object_name, pick.user_id, pick.session_id);
90+
} else {
91+
addPick({
92+
objectName: pick.object_name,
93+
userId: pick.user_id,
94+
sessionId: pick.session_id,
95+
});
96+
}
97+
};
98+
99+
const isVisible = (pick: PicksSummaryResponse) => {
100+
const existing = state.selectedPicks.find(
101+
(p) => p.objectName === pick.object_name && p.userId === pick.user_id && p.sessionId === pick.session_id
102+
);
103+
return existing?.visible ?? false;
104+
};
105+
106+
const isToolPick = (pick: PicksSummaryResponse) => pick.session_id === "0";
107+
108+
const isCurrentlyEditing = (pick: PicksSummaryResponse) =>
109+
pickingState.editingPicks?.objectName === pick.object_name &&
110+
pickingState.editingPicks?.userId === pick.user_id &&
111+
pickingState.editingPicks?.sessionId === pick.session_id;
112+
113+
return (
114+
<Box sx={{ display: "flex", flexDirection: "column", height: "100%" }}>
115+
{/* Toolbar */}
116+
<Box sx={{ p: 1, display: "flex", gap: 1, borderBottom: 1, borderColor: "divider" }}>
117+
<Button startIcon={<AddIcon />} size="small" onClick={() => setDialogOpen(true)} disabled={isEditing}>
118+
New
119+
</Button>
120+
</Box>
121+
122+
{/* Table */}
123+
{!picks || picks.length === 0 ? (
124+
<Typography color="text.secondary" sx={{ p: 2 }}>
125+
No picks found
126+
</Typography>
127+
) : (
128+
<TableContainer sx={{ flexGrow: 1 }}>
129+
<Table size="small" stickyHeader>
130+
<TableHead>
131+
<TableRow>
132+
<TableCell padding="checkbox" sx={{ py: 0.5 }} />
133+
<TableCell sx={{ py: 0.5, px: 1 }}>Object</TableCell>
134+
<TableCell sx={{ py: 0.5, px: 1 }}>User</TableCell>
135+
<TableCell sx={{ py: 0.5, px: 1 }}>Session</TableCell>
136+
<TableCell align="right" sx={{ py: 0.5, px: 1 }}>
137+
Count
138+
</TableCell>
139+
<TableCell padding="checkbox" sx={{ py: 0.5 }}>
140+
Actions
141+
</TableCell>
142+
</TableRow>
143+
</TableHead>
144+
<TableBody>
145+
{picks.map((pick) => (
146+
<PickRow
147+
key={`${pick.object_name}-${pick.user_id}-${pick.session_id}`}
148+
pick={pick}
149+
runName={runName}
150+
isVisible={isVisible(pick)}
151+
isToolPick={isToolPick(pick)}
152+
isCurrentlyEditing={isCurrentlyEditing(pick)}
153+
isAnyEditing={isEditing}
154+
onToggle={() => handleToggle(pick)}
155+
onDelete={() => handleDeletePicks(pick)}
156+
/>
157+
))}
158+
</TableBody>
159+
</Table>
160+
</TableContainer>
161+
)}
162+
163+
{/* New Pick Dialog */}
164+
<NewPickDialog open={dialogOpen} onClose={() => setDialogOpen(false)} onSubmit={handleCreatePicks} runName={runName} />
165+
</Box>
166+
);
167+
}
168+
169+
// Separate row component for editing functionality
170+
interface PickRowProps {
171+
pick: PicksSummaryResponse;
172+
runName: string;
173+
isVisible: boolean;
174+
isToolPick: boolean;
175+
isCurrentlyEditing: boolean;
176+
isAnyEditing: boolean;
177+
onToggle: () => void;
178+
onDelete: () => void;
179+
}
180+
181+
function PickRow({
182+
pick,
183+
runName,
184+
isVisible,
185+
isToolPick,
186+
isCurrentlyEditing,
187+
isAnyEditing,
188+
onToggle,
189+
onDelete,
190+
}: PickRowProps) {
191+
const { startEditing } = usePicking();
192+
const { data: pickDetail } = usePickPoints(runName, pick.object_name, pick.user_id, pick.session_id);
193+
194+
const handleEdit = () => {
195+
if (!pickDetail) return;
196+
197+
// Convert points to PickingPoint format
198+
const points: PickingPoint[] = pickDetail.points.map((p, index) => ({
199+
id: `${p.x.toFixed(2)}-${p.y.toFixed(2)}-${p.z.toFixed(2)}-${index}`,
200+
x: p.x,
201+
y: p.y,
202+
z: p.z,
203+
instance_id: p.instance_id,
204+
score: p.score,
205+
}));
206+
207+
startEditing(
208+
{
209+
runName,
210+
objectName: pick.object_name,
211+
userId: pick.user_id,
212+
sessionId: pick.session_id,
213+
color: pick.color,
214+
},
215+
points
216+
);
217+
};
218+
219+
return (
220+
<TableRow
221+
hover
222+
sx={{
223+
backgroundColor: isCurrentlyEditing ? `${rgbaToHex(pick.color)}40` : `${rgbaToHex(pick.color)}20`,
224+
}}
225+
>
226+
<TableCell padding="checkbox" sx={{ py: 0.5 }}>
227+
<IconButton size="small" onClick={onToggle}>
228+
{isVisible ? <Visibility fontSize="small" /> : <VisibilityOff fontSize="small" />}
229+
</IconButton>
230+
</TableCell>
231+
<TableCell sx={{ py: 0.5, px: 1, maxWidth: 100 }}>
232+
<Box sx={{ display: "flex", alignItems: "center", gap: 0.5 }}>
233+
<Box
234+
sx={{
235+
width: 12,
236+
height: 12,
237+
borderRadius: "50%",
238+
backgroundColor: rgbaToHex(pick.color),
239+
flexShrink: 0,
240+
}}
241+
/>
242+
<Typography variant="body2" noWrap sx={{ overflow: "hidden", textOverflow: "ellipsis" }}>
243+
{pick.object_name}
244+
</Typography>
245+
</Box>
246+
</TableCell>
247+
<TableCell sx={{ py: 0.5, px: 1 }}>
248+
<Typography variant="body2" noWrap>
249+
{pick.user_id}
250+
</Typography>
251+
</TableCell>
252+
<TableCell sx={{ py: 0.5, px: 1 }}>
253+
<Box sx={{ display: "flex", alignItems: "center", gap: 0.5 }}>
254+
<Typography variant="body2" noWrap>
255+
{pick.session_id}
256+
</Typography>
257+
{isToolPick && (
258+
<Tooltip title="Tool picks are read-only">
259+
<LockIcon fontSize="inherit" sx={{ color: "text.secondary" }} />
260+
</Tooltip>
261+
)}
262+
</Box>
263+
</TableCell>
264+
<TableCell align="right" sx={{ py: 0.5, px: 1 }}>
265+
<Typography variant="body2">{pick.point_count}</Typography>
266+
</TableCell>
267+
<TableCell padding="checkbox" sx={{ py: 0.5 }}>
268+
<Box sx={{ display: "flex" }}>
269+
{!isToolPick && (
270+
<>
271+
<Tooltip title={isAnyEditing ? "Finish current edit first" : "Edit picks"}>
272+
<span>
273+
<IconButton
274+
size="small"
275+
onClick={handleEdit}
276+
disabled={isAnyEditing && !isCurrentlyEditing}
277+
color={isCurrentlyEditing ? "primary" : "default"}
278+
>
279+
<EditIcon fontSize="small" />
280+
</IconButton>
281+
</span>
282+
</Tooltip>
283+
<Tooltip title="Delete picks">
284+
<span>
285+
<IconButton size="small" onClick={onDelete} disabled={isAnyEditing} color="error">
286+
<DeleteIcon fontSize="small" />
287+
</IconButton>
288+
</span>
289+
</Tooltip>
290+
</>
291+
)}
292+
</Box>
293+
</TableCell>
294+
</TableRow>
295+
);
296+
}

0 commit comments

Comments
 (0)