Per-shape hide + dimmed shape list in review editor (REQ-185, amended REQ-183)
- ReviewPage: hiddenShapeIds/overriddenShapeIds session Sets, H toggles selected/marked shapes, purge effect removed (dimmed rows selectable from list), auto-override on draw/assist/copy into hidden class, class-eye toggle clears that class's overrides - ReviewSidebar: all rows listed, dimmed rows get restore eye, header count = total shapes - ShortcutsPanel: H entry - docs: REQ-183 amended + REQ-185 added; design/ui-spec/tasks synced
This commit is contained in:
1 parent
96a00267d9
commit
6a36f102c9
7 files changed
+175
-57
No files matched your search
+13
-7
@@ -355,13 +355,19 @@ sequentially and merges, without examples it is the unchanged full-set request.
|
||||
per-class overrides table also carries a **Container** checkbox (REQ-184) that toggles
|
||||
`project_classes.container` through `PATCH /api/projects/{id} { containers: … }`.
|
||||
|
||||
**Review sidebar class rows (REQ-180, REQ-183).** Each class row's frame-clear `×` (REQ-180)
|
||||
gains an eye toggle in front of it: session-only `Set` state in `ReviewPage`, so it survives
|
||||
frame changes, resets when the review page is left, and never touches stored data. Hiding a
|
||||
class filters it out of the `annotations` array handed to `AnnotationCanvas` and to the
|
||||
"Shapes on this frame" list, and a purge effect drops hidden shapes from the marked and
|
||||
selected ids — so hidden shapes are not selectable, not marquee-selectable, not deletable —
|
||||
while the row keeps its real per-frame count.
|
||||
**Review sidebar class rows (REQ-180, REQ-183, REQ-185).** Each class row's frame-clear `×`
|
||||
(REQ-180) gains an eye toggle in front of it: session-only `Set` state in `ReviewPage`, so it
|
||||
survives frame changes, resets when the review page is left, and never touches stored data.
|
||||
Hidden shapes are never handed to `AnnotationCanvas` — not drawn, not clickable, not
|
||||
marquee-selectable — but they stay listed, dimmed, in "Shapes on this frame". Two session
|
||||
Sets drive the hide half: `hiddenShapeIds` (the `H` key, REQ-185, which toggles the single
|
||||
selection or all marked shapes) and `overriddenShapeIds` (the row-eye restore and the
|
||||
auto-override below); a shape is visible when it is not `H`-hidden and (its class is visible
|
||||
or it is overridden). Dimmed rows stay selectable, reclassable and deletable from the list —
|
||||
the old "not deletable while hidden" purge invariant is superseded — while the class row
|
||||
keeps its real per-frame count. Creating a shape while its class is hidden (draw, assist,
|
||||
copy) auto-overrides it so the user sees what they just made; reclassing into a hidden class
|
||||
dims it unless it already carries an override; toggling a class eye clears that class's overrides.
|
||||
|
||||
**Exemplar-driven labeling in review (REQ-173, REQ-174, REQ-175).** In `draw` mode a drag on
|
||||
`AnnotationCanvas` is an exemplar, not a rectangle: `onExemplar(box, positive)` where
|
||||
|
||||
+16
-5
@@ -186,11 +186,12 @@ changes.
|
||||
the **current frame only** (REQ-046 stays batch-wide). One click, no confirmation dialog;
|
||||
the next frame is untouched.
|
||||
- **REQ-183** — In the manual review editor, each class row has a **hide toggle**. A hidden
|
||||
class's shapes disappear from both the annotation canvas and the "Shapes on this frame"
|
||||
list — not selectable, not marquee-selectable, not deletable while hidden — while the class
|
||||
row keeps its real per-frame count and its eye state, so what is hidden stays visible as
|
||||
state. The choice is session state: it survives frame changes, resets when the review page
|
||||
is left, and never touches stored data.
|
||||
class's shapes are never drawn on the annotation canvas — not clickable there, not
|
||||
marquee-selectable — but they stay listed in "Shapes on this frame", dimmed, each dimmed
|
||||
row carrying an eye that restores it individually (REQ-185). The class row keeps its real
|
||||
per-frame count and its eye state, so what is hidden stays visible as state. The choice is
|
||||
session state: it survives frame changes, resets when the review page is left, and never
|
||||
touches stored data.
|
||||
- **REQ-184** — A class can be marked as **container** (persisted on
|
||||
`project_classes.container`, edited next to the per-class overrides in the auto-annotate
|
||||
modals). When the cross-class NMS of REQ-031 compares another class's box against a
|
||||
@@ -198,6 +199,16 @@ changes.
|
||||
the smaller box's area lies inside the container's box, both survive. The flag changes
|
||||
nothing else — unmarked classes are governed by IoU alone — and preview and batch job
|
||||
read the same stored flag.
|
||||
- **REQ-185** — In the manual review editor, the `H` key toggles hide on every currently
|
||||
selected shape (the single selection, or all marked shapes in select mode). Hiding is per
|
||||
shape: a shape hidden by `H`, and a shape hidden only because its class is hidden, both
|
||||
stay listed in "Shapes on this frame" **dimmed**, where an eye button restores it — the
|
||||
eye first un-hides an `H`-hidden shape, otherwise it overrides the class hide. Dimmed rows
|
||||
remain selectable, reclassable and deletable from the list; they are simply never drawn
|
||||
on the canvas. Shapes created while their class is hidden (draw, assist, copy) are
|
||||
auto-overridden so the user sees what they just made. Like the class eye, this is session
|
||||
state: it survives frame changes, resets when the review page is left, and never touches
|
||||
stored data.
|
||||
|
||||
## E4. Live counting preview
|
||||
|
||||
|
||||
+21
-5
@@ -1285,14 +1285,16 @@ failure modes to the counting path. The saving was always on the browser side.
|
||||
|
||||
## Task — Per-class hide toggle in review editor (REQ-183) `[DONE]`
|
||||
|
||||
1. Eye button on each sidebar class row — a hidden class's shapes leave the canvas, the
|
||||
"Shapes on this frame" list and every selection path while the row keeps its real
|
||||
per-frame count and eye state; the choice is session-only (survives frame changes, resets
|
||||
1. Eye button on each sidebar class row — a hidden class's shapes leave the canvas and every
|
||||
canvas selection path but stay in the "Shapes on this frame" list, dimmed, each with its
|
||||
own restore eye (amended by REQ-185; the old leave-the-list and "not deletable" semantics
|
||||
are superseded — see the REQ-185 entry below), while the row keeps its real per-frame count
|
||||
and eye state; the choice is session-only (survives frame changes, resets
|
||||
when the review page is left, stored data untouched) → verify: **[DONE]** `cd frontend &&
|
||||
npm run build` passes (✓ 535 ms, 69 modules); `wc -l` caps hold —
|
||||
`frontend/src/components/Icons.jsx` (172) and
|
||||
`frontend/src/components/ReviewSidebar.jsx` (129) ≤ 400,
|
||||
`frontend/src/pages/ReviewPage.jsx` (666) exempt (pre-existing over the 400 cap, not
|
||||
`frontend/src/components/ReviewSidebar.jsx` (145) ≤ 400,
|
||||
`frontend/src/pages/ReviewPage.jsx` (725) exempt (pre-existing over the 400 cap, not
|
||||
split by this task); rebuilt Docker frontend serves the new bundle (HTTP 200 on :9000,
|
||||
`aria-pressed` present in the shipped JS); browser click-test of the eye toggle is NOT
|
||||
automated — manual click-test pending.
|
||||
@@ -1321,6 +1323,20 @@ The REQ-031 amendment rides on this entry: cross-class greedy NMS with the per-c
|
||||
override and the containment carve-out is only real once the flag exists and both live sites
|
||||
read it.
|
||||
|
||||
## Task — Per-shape hide + dimmed shape list (REQ-183 amended, REQ-185) `[DONE]`
|
||||
|
||||
1. `H` hides/shows the selected or marked shapes; hidden shapes (by `H` or by their class)
|
||||
stay listed dimmed in "Shapes on this frame", each dimmed row with a restore eye that
|
||||
un-hides or overrides; shapes created into a hidden class (draw, assist, copy)
|
||||
auto-override; the class-eye toggle clears that class's overrides; the old purge effect
|
||||
and its "not deletable" invariant are gone → verify: **[DONE]** `cd frontend && npm run
|
||||
build` passes (✓ 545 ms, 69 modules); `wc -l` — `frontend/src/components/ReviewSidebar.jsx`
|
||||
≤ 400 (145), `frontend/src/components/ShortcutsPanel.jsx` ≤ 400 (69),
|
||||
`frontend/src/pages/ReviewPage.jsx` exempt (725, pre-existing over the 400 cap, not
|
||||
split); `grep` proofs that `'h', 'H'` is in `isShortcutKey` (`ReviewPage.jsx:407`) and
|
||||
that the removed purge effect (`while a class is hidden`) has zero hits left in
|
||||
`ReviewPage.jsx`; browser `H`/eye click-test is NOT automated — manual click-test pending.
|
||||
|
||||
## Known open points
|
||||
|
||||
- *Not closed by any task, by choice:* **any rebuild kills the running job.** Task 14's resume
|
||||
|
||||
+18
-12
@@ -1099,6 +1099,7 @@ Handlers are attached in the **capture** phase on `document`, and:
|
||||
| `T` | track selected shape forward 5 frames | same |
|
||||
| `1`–`9` | set active class; reclass the selected shape if any | reclass **all marked** shapes |
|
||||
| `Del` / `Backspace` | delete the selected shape | delete **all marked** shapes (confirmed) |
|
||||
| `H` | hide/show the selected shape(s); no-op with nothing selected | same |
|
||||
| `Esc` | cancel the gesture in flight | clear the marked set |
|
||||
| hold `S` | SAM3 assist mode while held | — |
|
||||
| `Enter` | apply the exemplar preview (panel open) | — |
|
||||
@@ -1125,18 +1126,23 @@ network.
|
||||
- **Eye button (REQ-183)** — first control in the class row, before the frame-clear `×`: a
|
||||
ghost icon button (`cursor: pointer`, `aria-pressed` bound to the hidden state, title
|
||||
`Hide "name"` / `Show "name"`), drawing the eye icon while the class is visible and the
|
||||
slashed eye at 55 % opacity while it is hidden. Hiding drops that class's shapes from the
|
||||
annotation canvas **and** from *Shapes on this frame* — not selectable, not
|
||||
marquee-selectable, not deletable while hidden, and any selection or mark on them is
|
||||
cleared — while the row keeps its real per-frame count (the `×` and the trash still count
|
||||
every shape). The choice is **session state**: it survives frame changes, resets when the
|
||||
review page is left, and never touches stored data.
|
||||
slashed eye at 55 % opacity while it is hidden. Hiding stops drawing that class's shapes on
|
||||
the annotation canvas — not clickable there, not marquee-selectable — but the shapes stay
|
||||
listed in *Shapes on this frame*, dimmed (≈45 % opacity), each dimmed row carrying its own
|
||||
show control (REQ-185), while the class row keeps its real per-frame count (the `×` and
|
||||
the trash still count every shape). The choice is **session state**: it survives frame
|
||||
changes, resets when the review page is left, and never touches stored data.
|
||||
- **Shapes on this frame (N)** — one row per annotation: swatch, class name, and either the
|
||||
score to 2 dp (auto) or the word `manual`; click selects, trash deletes. When empty:
|
||||
*"None on this frame — drag to draw a shape."* plus, if the batch has shapes elsewhere, a
|
||||
*Jump to Frame with Shapes [N]* button.
|
||||
- **Shortcuts** — a static `<dl>` of the keyboard map. Keep it visible; this is a keyboard-first
|
||||
screen and the panel is the discovery mechanism.
|
||||
score to 2 dp (auto) or the word `manual`; click selects, trash deletes. The header count
|
||||
and the rows cover **every** annotation on the frame, visible or not. A row is dimmed
|
||||
(≈45 % opacity) when its shape is `H`-hidden or hidden by its class without an override;
|
||||
a dimmed row keeps its swatch, name and score and gains an eye button (*Show shape*, the
|
||||
REQ-185 eye) that first un-hides an `H`-hidden shape and otherwise overrides the class
|
||||
hide; click still selects it and the trash still deletes it. Empty means the frame has
|
||||
**zero shapes**, not zero visible ones: *"None on this frame — drag to draw a shape."*
|
||||
plus, if the batch has shapes elsewhere, a *Jump to Frame with Shapes [N]* button.
|
||||
- **Shortcuts** — a static `<dl>` of the keyboard map (it also lists `H`). Keep it visible;
|
||||
this is a keyboard-first screen and the panel is the discovery mechanism.
|
||||
|
||||
---
|
||||
|
||||
@@ -1516,7 +1522,7 @@ Shortcuts).
|
||||
Overlap/NMS, Min box size, Max shapes), lalu baru **Apply** yang menulis. Shift-drag = contoh
|
||||
negatif ("bukan ini"). Panel filter duduk di **sidebar**, bukan menutupi frame.
|
||||
- Tahan `S` = SAM3 click-assist (satu box → satu shape).
|
||||
- Keyboard-first: `V ← → A X U N C T 1–9 Del Esc Enter`. Semua aksi punya shortcut; panel
|
||||
- Keyboard-first: `V ← → A X U N C T 1–9 Del H Esc Enter`. Semua aksi punya shortcut; panel
|
||||
Shortcuts selalu terlihat. Ctrl/Cmd/Alt **tidak pernah** di-intercept; mengetik di input tidak
|
||||
memicu shortcut.
|
||||
- Approve/Reject langsung pindah frame dan meng-update status lokal sebelum request selesai.
|
||||
|
||||
@@ -12,6 +12,9 @@ export default function ReviewSidebar({
|
||||
clearClassInFrame,
|
||||
hiddenClasses,
|
||||
onToggleHide,
|
||||
hiddenShapeIds,
|
||||
overriddenShapeIds,
|
||||
onRestoreShape,
|
||||
annotations,
|
||||
selectedId,
|
||||
setSelectedId,
|
||||
@@ -20,9 +23,6 @@ export default function ReviewSidebar({
|
||||
jumpToNextAnnotated,
|
||||
batchAnnotationCount,
|
||||
}) {
|
||||
// REQ-183: the shapes panel lists visible shapes only; class rows keep
|
||||
// counting every shape of the class (data truth).
|
||||
const visibleAnnotations = annotations.filter((row) => !hiddenClasses.has(row.class_id))
|
||||
return (
|
||||
<aside className="review-side stack">
|
||||
{exemplarPanel}
|
||||
@@ -76,8 +76,8 @@ export default function ReviewSidebar({
|
||||
</div>
|
||||
|
||||
<div className="panel side-panel">
|
||||
<h2>Shapes on this frame ({visibleAnnotations.length})</h2>
|
||||
{visibleAnnotations.length === 0 ? (
|
||||
<h2>Shapes on this frame ({annotations.length})</h2>
|
||||
{annotations.length === 0 ? (
|
||||
<div>
|
||||
<p className="hint" style={{ marginBottom: 8 }}>
|
||||
None on this frame — drag to draw a shape.
|
||||
@@ -101,8 +101,12 @@ export default function ReviewSidebar({
|
||||
</div>
|
||||
) : (
|
||||
<ul className="shape-list">
|
||||
{visibleAnnotations.map((item) => (
|
||||
<li key={item.id} className={item.id === selectedId ? 'selected' : ''}>
|
||||
{annotations.map((item) => {
|
||||
const dimmed =
|
||||
hiddenShapeIds.has(item.id) ||
|
||||
(hiddenClasses.has(item.class_id) && !overriddenShapeIds.has(item.id))
|
||||
return (
|
||||
<li key={item.id} className={item.id === selectedId ? 'selected' : ''} style={dimmed ? { opacity: 0.45 } : undefined}>
|
||||
<button className="shape-pick" onClick={() => setSelectedId(item.id)}>
|
||||
<span className="swatch" style={{ background: classColor(item.class_id) }} />
|
||||
{project?.classes?.find?.((c) => c.class_id === item.class_id)?.name ?? project?.classes?.[item.class_id]?.name ?? `class ${item.class_id}`}
|
||||
@@ -110,6 +114,17 @@ export default function ReviewSidebar({
|
||||
{item.source === 'auto' ? item.score.toFixed(2) : 'manual'}
|
||||
</span>
|
||||
</button>
|
||||
{dimmed && (
|
||||
<button
|
||||
className="btn btn-ghost"
|
||||
style={{ padding: '4px 6px', cursor: 'pointer' }}
|
||||
title="Show shape"
|
||||
aria-label="Show shape"
|
||||
onClick={() => onRestoreShape(item.id)}
|
||||
>
|
||||
<EyeIcon size={12} />
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
className="btn btn-danger"
|
||||
aria-label="Delete shape"
|
||||
@@ -118,7 +133,8 @@ export default function ReviewSidebar({
|
||||
<TrashIcon size={13} />
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -43,6 +43,10 @@ export default function ShortcutsPanel() {
|
||||
<dt><kbd>Del</kbd></dt>
|
||||
<dd>Remove selected</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt><kbd>H</kbd></dt>
|
||||
<dd>Hide / show selected shapes</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt><kbd>←</kbd> <kbd>→</kbd></dt>
|
||||
<dd>Prev / next frame</dd>
|
||||
|
||||
@@ -24,6 +24,8 @@ export default function ReviewPage({ batchId: rawBatchId, projectId, onProject }
|
||||
const [error, setError] = useState('')
|
||||
const [activeJob, setActiveJob] = useState(null)
|
||||
const [hiddenClasses, setHiddenClasses] = useState(() => new Set())
|
||||
const [hiddenShapeIds, setHiddenShapeIds] = useState(() => new Set())
|
||||
const [overriddenShapeIds, setOverriddenShapeIds] = useState(() => new Set())
|
||||
|
||||
const batchId = batch?.id || rawBatchId
|
||||
const stripRef = useRef(null)
|
||||
@@ -111,6 +113,7 @@ export default function ReviewPage({ batchId: rawBatchId, projectId, onProject }
|
||||
const created = await api.addAnnotation(frame.id, { class_id: activeClass, geometry })
|
||||
setAnnotations((rows) => [...rows, created])
|
||||
setSelectedId(created.id)
|
||||
if (hiddenClasses.has(created.class_id)) setOverriddenShapeIds((current) => new Set(current).add(created.id))
|
||||
patchFrameLocally(frame.id, { annotation_count: (frame.annotation_count ?? 0) + 1 })
|
||||
} catch (exc) { setError(exc.message) }
|
||||
}
|
||||
@@ -132,6 +135,7 @@ export default function ReviewPage({ batchId: rawBatchId, projectId, onProject }
|
||||
const created = await api.assist(frame.id, { box, class_id: activeClass })
|
||||
setAnnotations((rows) => [...rows, created])
|
||||
setSelectedId(created.id)
|
||||
if (hiddenClasses.has(created.class_id)) setOverriddenShapeIds((current) => new Set(current).add(created.id))
|
||||
} catch (exc) { setError(exc.message) } finally { setBusy(false) }
|
||||
}
|
||||
|
||||
@@ -192,24 +196,69 @@ export default function ReviewPage({ batchId: rawBatchId, projectId, onProject }
|
||||
else next.add(classId)
|
||||
return next
|
||||
})
|
||||
}, [])
|
||||
|
||||
// REQ-183: while a class is hidden, no shape of it may be selected or
|
||||
// marked — however it got there (draw, reclass, copy) — so Delete can
|
||||
// never hit an invisible shape.
|
||||
useEffect(() => {
|
||||
if (!hiddenClasses.size) return
|
||||
setSelectedId((currentId) => {
|
||||
const row = annotations.find((a) => a.id === currentId)
|
||||
return row && hiddenClasses.has(row.class_id) ? null : currentId
|
||||
// REQ-185: toggling a class drops its per-shape overrides — re-hiding a
|
||||
// class starts from a clean slate.
|
||||
const classShapeIds = annotations.filter((row) => row.class_id === classId).map((row) => row.id)
|
||||
setOverriddenShapeIds((current) => {
|
||||
if (!classShapeIds.some((id) => current.has(id))) return current
|
||||
const next = new Set(current)
|
||||
classShapeIds.forEach((id) => next.delete(id))
|
||||
return next
|
||||
})
|
||||
setMarkedIds((rows) => rows.filter((id) => {
|
||||
const row = annotations.find((a) => a.id === id)
|
||||
return !row || !hiddenClasses.has(row.class_id)
|
||||
}))
|
||||
}, [hiddenClasses, annotations])
|
||||
}, [annotations])
|
||||
|
||||
const visibleAnnotations = annotations.filter((row) => !hiddenClasses.has(row.class_id))
|
||||
// REQ-185: the eye first un-hides an H-hidden shape, otherwise it overrides
|
||||
// the class hide — one rule, in one place.
|
||||
function restoreShape(id) {
|
||||
if (hiddenShapeIds.has(id)) {
|
||||
setHiddenShapeIds((current) => {
|
||||
const next = new Set(current)
|
||||
next.delete(id)
|
||||
return next
|
||||
})
|
||||
} else {
|
||||
setOverriddenShapeIds((current) => (current.has(id) ? current : new Set(current).add(id)))
|
||||
}
|
||||
}
|
||||
|
||||
// REQ-185: neither the shape's own hide nor its class's hide (without an
|
||||
// override) may reach the canvas. Session-only, never persisted.
|
||||
const isShapeVisible = (row) =>
|
||||
!hiddenShapeIds.has(row.id) &&
|
||||
(!hiddenClasses.has(row.class_id) || overriddenShapeIds.has(row.id))
|
||||
|
||||
const visibleAnnotations = annotations.filter(isShapeVisible)
|
||||
|
||||
// REQ-185: H toggles hide on the single selection and every marked shape.
|
||||
function toggleHideSelected() {
|
||||
const ids = [...new Set([selectedId, ...markedIds].filter((id) => id != null))]
|
||||
const rows = ids.map((id) => annotations.find((row) => row.id === id)).filter(Boolean)
|
||||
if (!rows.length) return
|
||||
const toHide = rows.filter((row) => isShapeVisible(row)).map((row) => row.id)
|
||||
const toUnhide = rows.filter((row) => !isShapeVisible(row)).map((row) => row.id)
|
||||
if (toHide.length) {
|
||||
setHiddenShapeIds((current) => {
|
||||
const next = new Set(current)
|
||||
toHide.forEach((id) => next.add(id))
|
||||
return next
|
||||
})
|
||||
// Flags must not contradict: hiding drops any class-hide override.
|
||||
setOverriddenShapeIds((current) => {
|
||||
if (!toHide.some((id) => current.has(id))) return current
|
||||
const next = new Set(current)
|
||||
toHide.forEach((id) => next.delete(id))
|
||||
return next
|
||||
})
|
||||
}
|
||||
if (toUnhide.length) {
|
||||
setHiddenShapeIds((current) => {
|
||||
if (!toUnhide.some((id) => current.has(id))) return current
|
||||
const next = new Set(current)
|
||||
toUnhide.forEach((id) => next.delete(id))
|
||||
return next
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const markAll = useCallback(() => setMarkedIds(visibleAnnotations.map((row) => row.id)), [visibleAnnotations])
|
||||
|
||||
@@ -294,10 +343,16 @@ export default function ReviewPage({ batchId: rawBatchId, projectId, onProject }
|
||||
setAnnotations(rows => [...rows, ...created])
|
||||
patchFrameLocally(frame.id, { annotation_count: (frame.annotation_count ?? 0) + created.length })
|
||||
if (created.length > 0) setSelectedId(created[0].id)
|
||||
const overridden = created.filter((row) => hiddenClasses.has(row.class_id)).map((row) => row.id)
|
||||
if (overridden.length) setOverriddenShapeIds((current) => {
|
||||
const next = new Set(current)
|
||||
overridden.forEach((id) => next.add(id))
|
||||
return next
|
||||
})
|
||||
} catch (exc) {
|
||||
setError(exc.message)
|
||||
}
|
||||
}, [frames, index, frame])
|
||||
}, [frames, index, frame, hiddenClasses])
|
||||
|
||||
const trackForward = useCallback(async () => {
|
||||
if (selectedId == null || !frames?.length) return
|
||||
@@ -339,7 +394,7 @@ export default function ReviewPage({ batchId: rawBatchId, projectId, onProject }
|
||||
}, [annotations, selectedId, index, frames])
|
||||
|
||||
const stateRef = useRef({})
|
||||
stateRef.current = { frames, index, project, selectedId, setStatus, removeSelected, reclass, jumpToPending, jumpToNextAnnotated, setAssistMode, copyPrevious, trackForward, mode, setMode, markedIds, removeMarked, reclassMarked, setMarkedIds }
|
||||
stateRef.current = { frames, index, project, selectedId, setStatus, removeSelected, reclass, jumpToPending, jumpToNextAnnotated, setAssistMode, copyPrevious, trackForward, mode, setMode, markedIds, removeMarked, reclassMarked, setMarkedIds, toggleHideSelected }
|
||||
|
||||
useEffect(() => {
|
||||
function onKeyDown(event) {
|
||||
@@ -347,9 +402,9 @@ export default function ReviewPage({ batchId: rawBatchId, projectId, onProject }
|
||||
// Ctrl/Cmd/Alt combos belong to the browser and the OS: without this,
|
||||
// Ctrl+S approves the frame and Ctrl+A/C/X/N/T all fire review actions.
|
||||
if (event.ctrlKey || event.metaKey || event.altKey) return
|
||||
const { frames, project, setStatus, removeSelected, reclass, jumpToPending, jumpToNextAnnotated, setAssistMode, copyPrevious, trackForward, mode, setMode, markedIds, removeMarked, reclassMarked, setMarkedIds } = stateRef.current
|
||||
const { frames, project, setStatus, removeSelected, reclass, jumpToPending, jumpToNextAnnotated, setAssistMode, copyPrevious, trackForward, mode, setMode, markedIds, removeMarked, reclassMarked, setMarkedIds, toggleHideSelected } = stateRef.current
|
||||
const key = event.key
|
||||
const isShortcutKey = /^[1-9]$/.test(key) || ['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'Delete', 'Backspace', 'a', 'A', 'x', 'X', 'u', 'U', 's', 'S', 'n', 'N', 'c', 'C', 't', 'T', 'v', 'V', 'Escape'].includes(key)
|
||||
const isShortcutKey = /^[1-9]$/.test(key) || ['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'Delete', 'Backspace', 'a', 'A', 'x', 'X', 'u', 'U', 's', 'S', 'n', 'N', 'c', 'C', 't', 'T', 'v', 'V', 'h', 'H', 'Escape'].includes(key)
|
||||
if (isShortcutKey) { event.preventDefault(); event.stopPropagation() }
|
||||
|
||||
// In select mode the marquee owns Delete and the digits, otherwise a
|
||||
@@ -374,6 +429,7 @@ export default function ReviewPage({ batchId: rawBatchId, projectId, onProject }
|
||||
else if (key === 'n' || key === 'N') jumpToNextAnnotated?.()
|
||||
else if (key === 'c' || key === 'C') copyPrevious?.()
|
||||
else if (key === 't' || key === 'T') trackForward?.()
|
||||
else if (key === 'h' || key === 'H') toggleHideSelected?.()
|
||||
else if (key === 'Delete' || key === 'Backspace') removeSelected?.()
|
||||
else if (/^[1-9]$/.test(key)) {
|
||||
const classId = Number(key) - 1
|
||||
@@ -652,6 +708,9 @@ export default function ReviewPage({ batchId: rawBatchId, projectId, onProject }
|
||||
clearClassInFrame={clearClassInFrame}
|
||||
hiddenClasses={hiddenClasses}
|
||||
onToggleHide={toggleHide}
|
||||
hiddenShapeIds={hiddenShapeIds}
|
||||
overriddenShapeIds={overriddenShapeIds}
|
||||
onRestoreShape={restoreShape}
|
||||
annotations={annotations}
|
||||
selectedId={selectedId}
|
||||
setSelectedId={setSelectedId}
|
||||
|
||||
Reference in new issue
Block a user