Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Show clicked Bounding Box in BB-Tab #7935

Merged
merged 7 commits into from
Jul 29, 2024
1 change: 1 addition & 0 deletions CHANGELOG.unreleased.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ For upgrade instructions, please check the [migration guide](MIGRATIONS.released
### Added
- 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=<url_to_datasource>` 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)
- 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)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ import {
setQuickSelectStateAction,
setLastMeasuredPositionAction,
setIsMeasuringAction,
setActiveUserBoundingBoxId,
} from "oxalis/model/actions/ui_actions";
import ArbitraryView from "oxalis/view/arbitrary_view";

Expand Down Expand Up @@ -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);
},
Expand Down
1 change: 1 addition & 0 deletions frontend/javascripts/oxalis/default_state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,7 @@ const defaultState: OxalisState = {
activeOrganization: null,
uiInformation: {
activeTool: "MOVE",
activeUserBoundingBoxId: null,
showDropzoneModal: false,
showVersionRestore: false,
showDownloadModal: false,
Expand Down
11 changes: 10 additions & 1 deletion frontend/javascripts/oxalis/model/actions/ui_actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ type SetIsMeasuringAction = ReturnType<typeof setIsMeasuringAction>;
type SetNavbarHeightAction = ReturnType<typeof setNavbarHeightAction>;
type ShowContextMenuAction = ReturnType<typeof showContextMenuAction>;
type HideContextMenuAction = ReturnType<typeof hideContextMenuAction>;
type SetActiveUserBoundingBoxId = ReturnType<typeof setActiveUserBoundingBoxId>;

type SetRenderAnimationModalVisibilityAction = ReturnType<
typeof setRenderAnimationModalVisibilityAction
Expand Down Expand Up @@ -58,7 +59,8 @@ export type UiAction =
| SetIsMeasuringAction
| SetNavbarHeightAction
| ShowContextMenuAction
| HideContextMenuAction;
| HideContextMenuAction
| SetActiveUserBoundingBoxId;

export const setDropzoneModalVisibilityAction = (visible: boolean) =>
({
Expand Down Expand Up @@ -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;
};
18 changes: 15 additions & 3 deletions frontend/javascripts/oxalis/model/reducers/annotation_reducer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down Expand Up @@ -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": {
Expand Down Expand Up @@ -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": {
Expand Down
5 changes: 5 additions & 0 deletions frontend/javascripts/oxalis/model/reducers/ui_reducer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
1 change: 1 addition & 0 deletions frontend/javascripts/oxalis/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -524,6 +524,7 @@ type UiInformation = {
readonly aIJobModalState: StartAIJobModalState;
readonly showRenderAnimationModal: boolean;
readonly activeTool: AnnotationTool;
readonly activeUserBoundingBoxId: number | null | undefined;
MichaelBuessemeyer marked this conversation as resolved.
Show resolved Hide resolved
readonly storedLayouts: Record<string, any>;
readonly isImportingMesh: boolean;
readonly isInAnnotationView: boolean;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -505,9 +505,10 @@ export class UserBoundingBoxInput extends React.PureComponent<UserBoundingBoxInp
isOwner,
);
return (
<React.Fragment>
<>
<Row
style={{
marginTop: 10,
marginBottom: 10,
}}
>
Expand Down Expand Up @@ -552,7 +553,7 @@ export class UserBoundingBoxInput extends React.PureComponent<UserBoundingBoxInp
</Row>
<Row
style={{
marginBottom: 20,
marginBottom: 10,
}}
align="top"
>
Expand Down Expand Up @@ -600,7 +601,7 @@ export class UserBoundingBoxInput extends React.PureComponent<UserBoundingBoxInp
</Tooltip>
</Col>
</Row>
</React.Fragment>
</>
);
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -19,13 +19,17 @@ import DownloadModalView from "../action-bar/download_modal_view";
import { APIJobType } from "types/api_flow_types";

export default function BoundingBoxTab() {
const bboxTableRef: Parameters<typeof Table>[0]["ref"] = useRef(null);
const [selectedBoundingBoxForExport, setSelectedBoundingBoxForExport] =
useState<UserBoundingBox | null>(null);
const tracing = useSelector((state: OxalisState) => state.tracing);
const allowUpdate = tracing.restrictions.allowUpdate;
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();

Expand Down Expand Up @@ -99,55 +103,85 @@ export default function BoundingBoxTab() {
APIJobType.EXPORT_TIFF,
);

// biome-ignore lint/correctness/useExhaustiveDependencies: Always try to scroll the active bounding box into view.
daniel-wer marked this conversation as resolved.
Show resolved Hide resolved
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) => (
<UserBoundingBoxInput
key={bb.id}
tooltipTitle="Format: minX, minY, minZ, width, height, depth"
value={Utils.computeArrayFromBoundingBox(bb.boundingBox)}
color={bb.color}
name={bb.name}
isExportEnabled={isExportEnabled}
isVisible={bb.isVisible}
onBoundingChange={_.partial(handleBoundingBoxBoundingChange, bb.id)}
onDelete={_.partial(deleteBoundingBox, bb.id)}
onExport={isExportEnabled ? _.partial(setSelectedBoundingBoxForExport, 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}
/>
),
},
];

const maybeAddBoundingBoxButton = allowUpdate ? (
<div style={{ display: "inline-block", width: "100%", textAlign: "center" }}>
<Tooltip title="Click to add another bounding box.">
<PlusSquareOutlined
onClick={addNewBoundingBox}
style={{
cursor: "pointer",
marginBottom: userBoundingBoxes.length === 0 ? 12 : 0,
}}
/>
</Tooltip>
</div>
) : null;

return (
<div
className="padded-tab-content"
style={{
minWidth: 300,
}}
>
{/* 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) => (
<UserBoundingBoxInput
key={bb.id}
tooltipTitle="Format: minX, minY, minZ, width, height, depth"
value={Utils.computeArrayFromBoundingBox(bb.boundingBox)}
color={bb.color}
name={bb.name}
isExportEnabled={isExportEnabled}
isVisible={bb.isVisible}
onBoundingChange={_.partial(handleBoundingBoxBoundingChange, bb.id)}
onDelete={_.partial(deleteBoundingBox, bb.id)}
onExport={isExportEnabled ? _.partial(setSelectedBoundingBoxForExport, 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 ? (
<Table
ref={bboxTableRef}
columns={boundingBoxWrapperTableColumns}
dataSource={userBoundingBoxes}
pagination={false}
rowKey="id"
showHeader={false}
className="bounding-box-table"
rowSelection={{
selectedRowKeys: activeBoundingBoxId != null ? [activeBoundingBoxId] : [],
getCheckboxProps: () => ({ disabled: true }),
}}
footer={() => maybeAddBoundingBoxButton}
/>
) : (
<div>No Bounding Boxes created yet.</div>
<>
<div>No Bounding Boxes created yet.</div>
{maybeAddBoundingBoxButton}
</>
)}
<Typography.Text type="secondary">{maybeUneditableExplanation}</Typography.Text>
{allowUpdate ? (
<div style={{ display: "inline-block", width: "100%", textAlign: "center" }}>
<Tooltip title="Click to add another bounding box.">
<PlusSquareOutlined
onClick={addNewBoundingBox}
style={{
cursor: "pointer",
marginBottom: userBoundingBoxes.length === 0 ? 12 : 0,
}}
/>
</Tooltip>
</div>
) : null}
{selectedBoundingBoxForExport != null ? (
<DownloadModalView
isOpen
Expand Down
23 changes: 23 additions & 0 deletions frontend/stylesheets/trace_view/_right_menu.less
Original file line number Diff line number Diff line change
Expand Up @@ -217,3 +217,26 @@
height: 24px;
}
}

.bounding-box-table {
MichaelBuessemeyer marked this conversation as resolved.
Show resolved Hide resolved
table {
border-bottom: none;
}
td {
padding: 0px !important;
}
tr:last-child {
border-bottom: none;
td {
border-bottom: none;
}
}
tr td:first-child {
// Hiding the extra checkbox column rendered due to using rowSelection in the table wrapping the bounding boxes.
display: none;
}
.ant-table-footer {
background-color: transparent;
padding: 8px;
}
}