diff --git a/CHANGELOG.unreleased.md b/CHANGELOG.unreleased.md index 1652c3a181..91785f2a02 100644 --- a/CHANGELOG.unreleased.md +++ b/CHANGELOG.unreleased.md @@ -14,6 +14,7 @@ For upgrade instructions, please check the [migration guide](MIGRATIONS.released - WEBKNOSSOS now automatically searches in subfolder / sub-collection identifiers for valid datasets in case a provided link to a remote dataset does not directly point to a dataset. [#7912](https://github.com/scalableminds/webknossos/pull/7912) - Added the option to move a bounding box via dragging while pressing ctrl / meta. [#7892](https://github.com/scalableminds/webknossos/pull/7892) - Added route `/import?url=` to automatically import and view remote datasets. [#7844](https://github.com/scalableminds/webknossos/pull/7844) +- Added that newly created, modified and clicked on bounding boxes are now highlighted and scrolled into view, while the bounding box tool is active. [#7935](https://github.com/scalableminds/webknossos/pull/7935) - Added option to expand or collapse all subgroups of a segment group in the segments tab. [#7911](https://github.com/scalableminds/webknossos/pull/7911) - The context menu that is opened upon right-clicking a segment in the dataview port now contains the segment's name. [#7920](https://github.com/scalableminds/webknossos/pull/7920) - Upgraded backend dependencies for improved performance and stability. [#7922](https://github.com/scalableminds/webknossos/pull/7922) diff --git a/frontend/javascripts/oxalis/controller/combinations/tool_controls.ts b/frontend/javascripts/oxalis/controller/combinations/tool_controls.ts index 3288d7d093..63715b3ee3 100644 --- a/frontend/javascripts/oxalis/controller/combinations/tool_controls.ts +++ b/frontend/javascripts/oxalis/controller/combinations/tool_controls.ts @@ -51,6 +51,7 @@ import { setQuickSelectStateAction, setLastMeasuredPositionAction, setIsMeasuringAction, + setActiveUserBoundingBoxId, } from "oxalis/model/actions/ui_actions"; import ArbitraryView from "oxalis/view/arbitrary_view"; @@ -604,6 +605,12 @@ export class BoundingBoxTool { highlightAndSetCursorOnHoveredBoundingBox(position, planeId, event); } }, + leftClick: (pos: Point2, _plane: OrthoView, _event: MouseEvent) => { + const currentlyHoveredEdge = getClosestHoveredBoundingBox(pos, planeId); + if (currentlyHoveredEdge) { + Store.dispatch(setActiveUserBoundingBoxId(currentlyHoveredEdge[0].boxId)); + } + }, rightClick: (pos: Point2, plane: OrthoView, event: MouseEvent, isTouch: boolean) => { SkeletonHandlers.handleOpenContextMenu(planeView, pos, plane, isTouch, event); }, diff --git a/frontend/javascripts/oxalis/default_state.ts b/frontend/javascripts/oxalis/default_state.ts index e7eac96a5d..8158cfde03 100644 --- a/frontend/javascripts/oxalis/default_state.ts +++ b/frontend/javascripts/oxalis/default_state.ts @@ -227,6 +227,7 @@ const defaultState: OxalisState = { activeOrganization: null, uiInformation: { activeTool: "MOVE", + activeUserBoundingBoxId: null, showDropzoneModal: false, showVersionRestore: false, showDownloadModal: false, diff --git a/frontend/javascripts/oxalis/model/actions/ui_actions.ts b/frontend/javascripts/oxalis/model/actions/ui_actions.ts index 2109903f39..d8cac7c4d0 100644 --- a/frontend/javascripts/oxalis/model/actions/ui_actions.ts +++ b/frontend/javascripts/oxalis/model/actions/ui_actions.ts @@ -27,6 +27,7 @@ type SetIsMeasuringAction = ReturnType; type SetNavbarHeightAction = ReturnType; type ShowContextMenuAction = ReturnType; type HideContextMenuAction = ReturnType; +type SetActiveUserBoundingBoxId = ReturnType; type SetRenderAnimationModalVisibilityAction = ReturnType< typeof setRenderAnimationModalVisibilityAction @@ -58,7 +59,8 @@ export type UiAction = | SetIsMeasuringAction | SetNavbarHeightAction | ShowContextMenuAction - | HideContextMenuAction; + | HideContextMenuAction + | SetActiveUserBoundingBoxId; export const setDropzoneModalVisibilityAction = (visible: boolean) => ({ @@ -210,3 +212,10 @@ export const hideContextMenuAction = () => ({ type: "HIDE_CONTEXT_MENU", }) as const; + +export const setActiveUserBoundingBoxId = (id: number | null) => { + return { + type: "SET_ACTIVE_USER_BOUNDING_BOX_ID", + id, + } as const; +}; diff --git a/frontend/javascripts/oxalis/model/reducers/annotation_reducer.ts b/frontend/javascripts/oxalis/model/reducers/annotation_reducer.ts index 1decc661b3..42f6746924 100644 --- a/frontend/javascripts/oxalis/model/reducers/annotation_reducer.ts +++ b/frontend/javascripts/oxalis/model/reducers/annotation_reducer.ts @@ -148,7 +148,10 @@ function AnnotationReducer(state: OxalisState, action: Action): OxalisState { } : bbox, ); - return updateUserBoundingBoxes(state, updatedUserBoundingBoxes); + const updatedState = updateUserBoundingBoxes(state, updatedUserBoundingBoxes); + return updateKey(updatedState, "uiInformation", { + activeUserBoundingBoxId: action.id, + }); } case "ADD_NEW_USER_BOUNDING_BOX": { @@ -201,7 +204,10 @@ function AnnotationReducer(state: OxalisState, action: Action): OxalisState { } const updatedUserBoundingBoxes = [...userBoundingBoxes, newUserBoundingBox]; - return updateUserBoundingBoxes(state, updatedUserBoundingBoxes); + const updatedState = updateUserBoundingBoxes(state, updatedUserBoundingBoxes); + return updateKey(updatedState, "uiInformation", { + activeUserBoundingBoxId: newUserBoundingBox.id, + }); } case "ADD_USER_BOUNDING_BOXES": { @@ -237,7 +243,13 @@ function AnnotationReducer(state: OxalisState, action: Action): OxalisState { const updatedUserBoundingBoxes = tracing.userBoundingBoxes.filter( (bbox) => bbox.id !== action.id, ); - return updateUserBoundingBoxes(state, updatedUserBoundingBoxes); + const updatedState = updateUserBoundingBoxes(state, updatedUserBoundingBoxes); + if (action.id === state.uiInformation.activeUserBoundingBoxId) { + return updateKey(updatedState, "uiInformation", { + activeUserBoundingBoxId: null, + }); + } + return updatedState; } case "UPDATE_MESH_VISIBILITY": { diff --git a/frontend/javascripts/oxalis/model/reducers/ui_reducer.ts b/frontend/javascripts/oxalis/model/reducers/ui_reducer.ts index 39da3dfd0c..5030dadabd 100644 --- a/frontend/javascripts/oxalis/model/reducers/ui_reducer.ts +++ b/frontend/javascripts/oxalis/model/reducers/ui_reducer.ts @@ -179,6 +179,11 @@ function UiReducer(state: OxalisState, action: Action): OxalisState { unmappedSegmentId: null, }); } + case "SET_ACTIVE_USER_BOUNDING_BOX_ID": { + return updateKey(state, "uiInformation", { + activeUserBoundingBoxId: action.id, + }); + } default: return state; diff --git a/frontend/javascripts/oxalis/store.ts b/frontend/javascripts/oxalis/store.ts index cb6a71b3e0..3cf1c78cb7 100644 --- a/frontend/javascripts/oxalis/store.ts +++ b/frontend/javascripts/oxalis/store.ts @@ -525,6 +525,7 @@ type UiInformation = { readonly aIJobModalState: StartAIJobModalState; readonly showRenderAnimationModal: boolean; readonly activeTool: AnnotationTool; + readonly activeUserBoundingBoxId: number | null | undefined; readonly storedLayouts: Record; readonly isImportingMesh: boolean; readonly isInAnnotationView: boolean; diff --git a/frontend/javascripts/oxalis/view/components/setting_input_views.tsx b/frontend/javascripts/oxalis/view/components/setting_input_views.tsx index 98b29e3945..ef2baccd66 100644 --- a/frontend/javascripts/oxalis/view/components/setting_input_views.tsx +++ b/frontend/javascripts/oxalis/view/components/setting_input_views.tsx @@ -505,9 +505,10 @@ export class UserBoundingBoxInput extends React.PureComponent + <> @@ -552,7 +553,7 @@ export class UserBoundingBoxInput extends React.PureComponent @@ -600,7 +601,7 @@ export class UserBoundingBoxInput extends React.PureComponent - + ); } } diff --git a/frontend/javascripts/oxalis/view/right-border-tabs/bounding_box_tab.tsx b/frontend/javascripts/oxalis/view/right-border-tabs/bounding_box_tab.tsx index 863a512ce2..c3ba70bc77 100644 --- a/frontend/javascripts/oxalis/view/right-border-tabs/bounding_box_tab.tsx +++ b/frontend/javascripts/oxalis/view/right-border-tabs/bounding_box_tab.tsx @@ -1,7 +1,7 @@ -import { Tooltip, Typography } from "antd"; +import { Table, Tooltip, Typography } from "antd"; import { PlusSquareOutlined } from "@ant-design/icons"; import { useSelector, useDispatch } from "react-redux"; -import React, { useState } from "react"; +import React, { useEffect, useRef, useState } from "react"; import _ from "lodash"; import { UserBoundingBoxInput } from "oxalis/view/components/setting_input_views"; import { Vector3, Vector6, BoundingBoxType, ControlModeEnum } from "oxalis/constants"; @@ -19,6 +19,7 @@ import DownloadModalView from "../action-bar/download_modal_view"; import { APIJobType } from "types/api_flow_types"; export default function BoundingBoxTab() { + const bboxTableRef: Parameters[0]["ref"] = useRef(null); const [selectedBoundingBoxForExport, setSelectedBoundingBoxForExport] = useState(null); const tracing = useSelector((state: OxalisState) => state.tracing); @@ -26,6 +27,9 @@ export default function BoundingBoxTab() { const isLockedByOwner = tracing.isLockedByOwner; const isOwner = useSelector((state: OxalisState) => isAnnotationOwner(state)); const dataset = useSelector((state: OxalisState) => state.dataset); + const activeBoundingBoxId = useSelector( + (state: OxalisState) => state.uiInformation.activeUserBoundingBoxId, + ); const { userBoundingBoxes } = getSomeTracing(tracing); const dispatch = useDispatch(); @@ -99,6 +103,55 @@ export default function BoundingBoxTab() { APIJobType.EXPORT_TIFF, ); + // biome-ignore lint/correctness/useExhaustiveDependencies: Always try to scroll the active bounding box into view. + useEffect(() => { + if (bboxTableRef.current != null && activeBoundingBoxId != null) { + bboxTableRef.current.scrollTo({ key: activeBoundingBoxId }); + } + }, [activeBoundingBoxId, bboxTableRef.current]); + + const boundingBoxWrapperTableColumns = [ + { + title: "Bounding Boxes", + key: "id", + render: (_id: number, bb: UserBoundingBox) => ( + {}} + onGoToBoundingBox={_.partial(handleGoToBoundingBox, bb.id)} + onVisibilityChange={_.partial(setBoundingBoxVisibility, bb.id)} + onNameChange={_.partial(setBoundingBoxName, bb.id)} + onColorChange={_.partial(setBoundingBoxColor, bb.id)} + disabled={!allowUpdate} + isLockedByOwner={isLockedByOwner} + isOwner={isOwner} + /> + ), + }, + ]; + + const maybeAddBoundingBoxButton = allowUpdate ? ( +
+ + + +
+ ) : null; + return (
- {/* In view mode, it's okay to render an empty list, since there will be - an explanation below, anyway. - */} - {userBoundingBoxes.length > 0 || isViewMode ? ( - userBoundingBoxes.map((bb) => ( - {}} - onGoToBoundingBox={_.partial(handleGoToBoundingBox, bb.id)} - onVisibilityChange={_.partial(setBoundingBoxVisibility, bb.id)} - onNameChange={_.partial(setBoundingBoxName, bb.id)} - onColorChange={_.partial(setBoundingBoxColor, bb.id)} - disabled={!allowUpdate} - isLockedByOwner={isLockedByOwner} - isOwner={isOwner} - /> - )) + {/* Don't render a table in view mode. */} + {isViewMode ? null : userBoundingBoxes.length > 0 ? ( + ({ disabled: true }), + }} + footer={() => maybeAddBoundingBoxButton} + /> ) : ( -
No Bounding Boxes created yet.
+ <> +
No Bounding Boxes created yet.
+ {maybeAddBoundingBoxButton} + )} {maybeUneditableExplanation} - {allowUpdate ? ( -
- - - -
- ) : null} {selectedBoundingBoxForExport != null ? (