Skip to content

Commit 55fa4e9

Browse files
Add explicit units to annotation settings (#9127)
### URL of deployed dev instance (used for testing): - https://explicitunitsinsidebar.webknossos.xyz/ ### Steps to test: - Open an annotation and check the following annotation settings have an explicit unit. The unit should correspond to the datasets scale unit (which can be changed in the dataset settings) - move value - position (nav and status bar) - skeleton layer settings - node radius - min. particle size - clipping distance - check out the position input (navbar): there should be a tooltip with the current position in vx and metric units. the tooltip should update upon closing and opening it again. - outside of the datasets bounding box, hovering the position should only open the tooltip that is warning about being outside the DS' bounds. - I made some minor changes made to slightly improve consistency, e.g. that everywhere Vx and metric units are shown, they are in the same order and that "voxel" as a unit always use "Vx" as abbreviation. Check that this looks alright and is (at least) as understandable as before ### context discussion about tooltip in navbar: https://scm.slack.com/archives/C5AKLAV0B/p1764771532133509 ### Issues: - contributes to #8938 ------ (Please delete unneeded items, merge only when none are left open) - [x] Added changelog entry (create a `$PR_NUMBER.md` file in `unreleased_changes` or use `./tools/create-changelog-entry.py`) - [ ] Added migration guide entry if applicable (edit the same file as for the changelog) - [x] Updated [documentation](../blob/master/docs) if applicable - [ ] Adapted [wk-libs python client](https://github.com/scalableminds/webknossos-libs/tree/master/webknossos/webknossos/client) if relevant API parts change - [ ] Removed dev-only changes like prints and application.conf edits - [ ] Considered [common edge cases](../blob/master/.github/common_edge_cases.md) - [ ] Needs datastore update after deployment
1 parent ce1ee60 commit 55fa4e9

File tree

11 files changed

+110
-34
lines changed

11 files changed

+110
-34
lines changed

docs/ui/layers.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ Note, not all control/viewport settings are available in every annotation mode.
5454

5555
- `Keyboard delay (ms)`: The initial delay before an operation will be executed when pressing a keyboard shortcut. A low value will immediately execute a keyboard's associated operation, whereas a high value will delay the execution of an operation. This is useful for preventing an operation being called multiple times when rapidly pressing a key in short succession, e.g., for movement.
5656

57-
- `Move Value (nm/s)`: A high value will speed up movement through the dataset, e.g., when holding down the spacebar. Vice-versa, a low value will slow down the movement allowing for more precision. This setting is especially useful in `Flight mode`.
57+
- `Move Value`: A high value will speed up movement through the dataset, e.g., when holding down the spacebar. Vice-versa, a low value will slow down the movement allowing for more precision. This setting is especially useful in `Flight mode`.
5858

5959
- `d/f-Switching`: If d/f switching is disabled, moving through the dataset with `f` will always go *f*orward by _increasing_ the coordinate orthogonal to the current slice. Correspondingly, `d` will move backwards by decreasing that coordinate. However, if d/f is enabled, the meaning of "forward" and "backward" will change depending on how you create nodes. For example, when a node is placed at z == 100 and afterwards another node is created at z == 90, z will be _decreased_ when going forward.
6060

frontend/javascripts/components/fast_tooltip.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ export type FastTooltipPlacement =
4949
| "left-end";
5050

5151
// See docstring above for context.
52-
const uniqueKeyToDynamicRenderer: Record<string, () => React.ReactElement> = {};
52+
const uniqueKeyToDynamicRenderer: Record<string, () => React.ReactElement | null> = {};
5353

5454
export default function FastTooltip({
5555
title,
@@ -76,7 +76,7 @@ export default function FastTooltip({
7676
className?: string; // class name attached to the wrapper
7777
style?: React.CSSProperties; // style attached to the wrapper
7878
variant?: "dark" | "light" | "success" | "warning" | "error" | "info";
79-
dynamicRenderer?: () => React.ReactElement;
79+
dynamicRenderer?: () => React.ReactElement | null;
8080
}) {
8181
const Tag = wrapper || "span";
8282
const [uniqueKeyForDynamic, setUniqueDynamicId] = useState<string | undefined>(undefined);

frontend/javascripts/libs/format_utils.ts

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,7 @@ export function formatScale(scale: VoxelSize | null | undefined, roundTo: number
157157
(value) => Utils.roundTo(value / conversionFactor, roundTo),
158158
scaleFactor,
159159
);
160-
return `${scaleInNmRounded.join(ThinSpace + MultiplicationSymbol + ThinSpace)} ${newUnit}³/voxel`;
160+
return `${scaleInNmRounded.join(ThinSpace + MultiplicationSymbol + ThinSpace)} ${newUnit}³/Vx`;
161161
}
162162

163163
function toOptionalFixed(num: number, decimalPrecision: number): string {
@@ -399,7 +399,7 @@ function findBestUnitForFormatting(
399399
}
400400
export function formatLengthAsVx(lengthInVx: number, roundTo: number = 2): string {
401401
const roundedLength = Utils.roundTo(lengthInVx, roundTo);
402-
return `${roundedLength} vx`;
402+
return `${roundedLength}${ThinSpace}Vx`;
403403
}
404404
export function formatAreaAsVx(areaInVx: number, roundTo: number = 2): string {
405405
return `${formatLengthAsVx(areaInVx, roundTo)}²`;
@@ -533,6 +533,28 @@ export function formatVoxels(voxelCount: number) {
533533
return `${voxelCount} Vx`;
534534
}
535535

536+
export function formatVoxelsForHighNumbers(voxelCount: number) {
537+
if (voxelCount == null) {
538+
return "";
539+
}
540+
if (!Number.isFinite(voxelCount)) {
541+
return "Infinity";
542+
}
543+
if (voxelCount > 10 ** 15) {
544+
return `${(voxelCount / 10 ** 15).toPrecision(4)}${ThinSpace}PVx`;
545+
}
546+
if (voxelCount > 10 ** 12) {
547+
return `${(voxelCount / 10 ** 12).toPrecision(4)}${ThinSpace}TVx`;
548+
}
549+
if (voxelCount > 10 ** 9) {
550+
return `${(voxelCount / 10 ** 9).toPrecision(4)}${ThinSpace}GVx`;
551+
}
552+
if (voxelCount > 10 ** 6) {
553+
return `${(voxelCount / 10 ** 6).toPrecision(4)}${ThinSpace}MVx`;
554+
}
555+
return `${voxelCount}${ThinSpace}Vx`;
556+
}
557+
536558
export function formatNumber(num: number): string {
537559
return new Intl.NumberFormat("en-US").format(num);
538560
}

frontend/javascripts/messages.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ export const settings: Partial<Record<keyof RecommendedConfiguration, string>> =
1919
displayScalebars: "Show Scalebars",
2020
dynamicSpaceDirection: "d/f-Switching",
2121
keyboardDelay: "Keyboard delay (ms)",
22-
moveValue: "Move Value (nm/s)",
22+
moveValue: "Move Value",
2323
newNodeNewTree: "Single-node-tree mode (Soma clicking)",
2424
centerNewNode: "Auto-center Nodes",
2525
applyNodeRotationOnActivation: "Auto-rotate to Nodes",

frontend/javascripts/viewer/model/accessors/dataset_accessor.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import type {
1616
ElementClass,
1717
} from "types/api_types";
1818
import type { DataLayer } from "types/schemas/datasource.types";
19-
import { LongUnitToShortUnitMap, type Vector3, type ViewMode } from "viewer/constants";
19+
import { LongUnitToShortUnitMap, Unicode, type Vector3, type ViewMode } from "viewer/constants";
2020
import constants, { ViewModeValues, Vector3Indicies, MappingStatusEnum } from "viewer/constants";
2121
import type {
2222
ActiveMappingInfo,
@@ -31,6 +31,8 @@ import { getSupportedValueRangeForElementClass } from "../bucket_data_handling/d
3131
import { MagInfo, convertToDenseMags } from "../helpers/mag_info";
3232
import { reuseInstanceOnEquality } from "./accessor_helpers";
3333

34+
const { ThinSpace } = Unicode;
35+
3436
function _getMagInfo(magnifications: Array<{ mag: Vector3 }>): MagInfo {
3537
return new MagInfo(magnifications.map((magObj) => magObj.mag));
3638
}
@@ -297,7 +299,7 @@ export function getDatasetExtentAsString(
297299

298300
if (inVoxel) {
299301
const extentInVoxel = getDatasetExtentInVoxel(dataset);
300-
return `${formatExtentInUnitWithLength(extentInVoxel, (x) => `${x}`)} voxel`;
302+
return `${formatExtentInUnitWithLength(extentInVoxel, (x) => `${x}`)}${ThinSpace}Vx`;
301303
}
302304

303305
const extent = getDatasetExtentInUnit(dataset);

frontend/javascripts/viewer/model/sagas/settings_saga.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import ErrorHandling from "libs/error_handling";
33
import Toast from "libs/toast";
44
import messages from "messages";
55
import { all, call, debounce, put, retry, takeEvery } from "typed-redux-saga";
6-
import { ControlModeEnum } from "viewer/constants";
6+
import { ControlModeEnum, LongUnitToShortUnitMap } from "viewer/constants";
77
import {
88
type SetViewModeAction,
99
type UpdateUserSettingAction,
@@ -108,7 +108,10 @@ function* showUserSettingToast(action: UpdateUserSettingAction): Saga<void> {
108108

109109
if (propertyName === "moveValue" || propertyName === "moveValue3d") {
110110
const moveValue = yield* select((state) => state.userConfiguration[propertyName]);
111-
const moveValueMessage = messages["tracing.changed_move_value"] + moveValue;
111+
const unit = yield* select(
112+
(state) => LongUnitToShortUnitMap[state.dataset.dataSource.scale.unit],
113+
);
114+
const moveValueMessage = messages["tracing.changed_move_value"] + moveValue + ` ${unit}/s`;
112115
Toast.success(moveValueMessage, {
113116
key: "CHANGED_MOVE_VALUE",
114117
});

frontend/javascripts/viewer/view/action-bar/dataset_position_view.tsx

Lines changed: 46 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,19 @@
11
import { PushpinOutlined } from "@ant-design/icons";
2-
import { Space } from "antd";
2+
import { Col, Row, Space } from "antd";
33
import FastTooltip from "components/fast_tooltip";
4+
import { formatNumberToLength, formatVoxelsForHighNumbers } from "libs/format_utils";
45
import { V3 } from "libs/mjs";
56
import { useWkSelector } from "libs/react_hooks";
67
import Toast from "libs/toast";
78
import { Vector3Input } from "libs/vector_input";
89
import message from "messages";
910
import type React from "react";
10-
import type { Vector3 } from "viewer/constants";
11+
import { useCallback, useRef } from "react";
12+
import { LongUnitToShortUnitMap, type Vector3 } from "viewer/constants";
1113
import { getDatasetExtentInVoxel } from "viewer/model/accessors/dataset_accessor";
1214
import { getPosition } from "viewer/model/accessors/flycam_accessor";
1315
import { setPositionAction } from "viewer/model/actions/flycam_actions";
16+
import { convertVoxelSizeToUnit } from "viewer/model/scaleinfo";
1417
import Store from "viewer/store";
1518
import { ShareButton } from "viewer/view/action-bar/share_modal_view";
1619
import ButtonComponent from "viewer/view/components/button_component";
@@ -36,7 +39,9 @@ const positionInputErrorStyle: React.CSSProperties = {
3639
function DatasetPositionView() {
3740
const flycam = useWkSelector((state) => state.flycam);
3841
const dataset = useWkSelector((state) => state.dataset);
42+
const voxelSize = useWkSelector((state) => state.dataset.dataSource.scale);
3943
const task = useWkSelector((state) => state.task);
44+
const maybeErrorMessageRef = useRef<string | null>(null);
4045

4146
const copyPositionToClipboard = async () => {
4247
const position = V3.floor(getPosition(flycam)).join(", ");
@@ -90,6 +95,36 @@ function DatasetPositionView() {
9095
} else if (!maybeErrorMessage && isOutOfTaskBounds) {
9196
maybeErrorMessage = message["tracing.out_of_task_bounds"];
9297
}
98+
maybeErrorMessageRef.current = maybeErrorMessage;
99+
100+
const getPositionTooltipContent = useCallback(() => {
101+
if (maybeErrorMessageRef.current != null) return null;
102+
const currentFlycam = Store.getState().flycam;
103+
const currentPosition = V3.floor(getPosition(currentFlycam));
104+
const shortUnit = LongUnitToShortUnitMap[voxelSize.unit];
105+
const positionInVxStrings = currentPosition.map((coord) => formatVoxelsForHighNumbers(coord));
106+
const voxelSizeInMetricUnit = convertVoxelSizeToUnit(voxelSize, shortUnit);
107+
const positionInMetrics = currentPosition.map(
108+
(coord, index) => coord * voxelSizeInMetricUnit[index],
109+
);
110+
const positionInMetricStrings = positionInMetrics.map((coord) =>
111+
formatNumberToLength(coord, shortUnit),
112+
);
113+
return (
114+
<div>
115+
<Row justify="space-between" gutter={16} wrap={false}>
116+
<Col span={8}>{positionInVxStrings[0]},</Col>
117+
<Col span={8}>{positionInVxStrings[1]},</Col>
118+
<Col span={8}>{positionInVxStrings[2]}</Col>
119+
</Row>
120+
<Row justify="space-between" gutter={16} wrap={false}>
121+
<Col span={8}>{positionInMetricStrings[0]},</Col>
122+
<Col span={8}>{positionInMetricStrings[1]},</Col>
123+
<Col span={8}>{positionInMetricStrings[2]}</Col>
124+
</Row>
125+
</div>
126+
);
127+
}, [voxelSize]);
93128

94129
return (
95130
<FastTooltip title={maybeErrorMessage || null} wrapper="div">
@@ -107,13 +142,15 @@ function DatasetPositionView() {
107142
<PushpinOutlined style={positionIconStyle} />
108143
</ButtonComponent>
109144
</FastTooltip>
110-
<Vector3Input
111-
value={position}
112-
onChange={handleChangePosition}
113-
autoSize
114-
style={positionInputStyle}
115-
allowDecimals
116-
/>
145+
<FastTooltip dynamicRenderer={getPositionTooltipContent}>
146+
<Vector3Input
147+
value={position}
148+
onChange={handleChangePosition}
149+
autoSize
150+
style={positionInputStyle}
151+
allowDecimals
152+
/>
153+
</FastTooltip>
117154
<DatasetRotationPopoverButtonView style={iconColoringStyle} />
118155
<ShareButton dataset={dataset} style={iconColoringStyle} />
119156
</Space.Compact>

frontend/javascripts/viewer/view/distance_measurement_tooltip.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,8 +129,8 @@ export default function DistanceMeasurementTooltip() {
129129
pointerEvents: isMeasuring ? "none" : "auto",
130130
}}
131131
>
132-
<DistanceEntry distance={valueInMetricUnit} />
133132
<DistanceEntry distance={valueInVx} />
133+
<DistanceEntry distance={valueInMetricUnit} />
134134
</div>
135135
);
136136
}

frontend/javascripts/viewer/view/left-border-tabs/controls_and_rendering_settings_tab.tsx

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import type { APIDataset, APIUser } from "types/api_types";
1414
import type { ArrayElement } from "types/globals";
1515
import { userSettings } from "types/schemas/user_settings.schema";
1616
import type { ViewMode } from "viewer/constants";
17-
import Constants, { BLEND_MODES } from "viewer/constants";
17+
import Constants, { BLEND_MODES, LongUnitToShortUnitMap } from "viewer/constants";
1818
import defaultState from "viewer/default_state";
1919
import { getValidZoomRangeForUser } from "viewer/model/accessors/flycam_accessor";
2020
import { setZoomStepAction } from "viewer/model/actions/flycam_actions";
@@ -238,11 +238,11 @@ class ControlsAndRenderingSettingsTab extends PureComponent<ControlsAndRendering
238238
};
239239

240240
render() {
241+
const datasetScaleUnit = LongUnitToShortUnitMap[this.props.dataset.dataSource.scale.unit];
242+
const moveValueString = `${settingsLabels.moveValue} (${datasetScaleUnit}/s)`;
241243
const moveValueSetting = Constants.MODES_ARBITRARY.includes(this.props.viewMode) ? (
242244
<NumberSliderSetting
243-
label={
244-
<FastTooltip title={settingsTooltips.moveValue}>{settingsLabels.moveValue}</FastTooltip>
245-
}
245+
label={<FastTooltip title={settingsTooltips.moveValue}>{moveValueString}</FastTooltip>}
246246
min={userSettings.moveValue3d.minimum}
247247
max={userSettings.moveValue3d.maximum}
248248
step={10}
@@ -252,9 +252,7 @@ class ControlsAndRenderingSettingsTab extends PureComponent<ControlsAndRendering
252252
/>
253253
) : (
254254
<NumberSliderSetting
255-
label={
256-
<FastTooltip title={settingsTooltips.moveValue}>{settingsLabels.moveValue}</FastTooltip>
257-
}
255+
label={<FastTooltip title={settingsTooltips.moveValue}>{moveValueString}</FastTooltip>}
258256
min={userSettings.moveValue.minimum}
259257
max={userSettings.moveValue.maximum}
260258
step={10}

frontend/javascripts/viewer/view/left-border-tabs/layer_settings_tab.tsx

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,12 @@ import {
6262
import { getSpecificDefaultsForLayer } from "types/schemas/dataset_view_configuration_defaults";
6363
import { userSettings } from "types/schemas/user_settings.schema";
6464
import type { Vector3 } from "viewer/constants";
65-
import Constants, { ControlModeEnum, IdentityTransform, MappingStatusEnum } from "viewer/constants";
65+
import Constants, {
66+
ControlModeEnum,
67+
IdentityTransform,
68+
LongUnitToShortUnitMap,
69+
MappingStatusEnum,
70+
} from "viewer/constants";
6671
import defaultState from "viewer/default_state";
6772
import {
6873
getDefaultValueRangeOfLayer,
@@ -1169,8 +1174,14 @@ class DatasetSettings extends React.PureComponent<DatasetSettingsProps, State> {
11691174
};
11701175

11711176
getSkeletonLayer = () => {
1172-
const { controlMode, annotation, onChangeRadius, userConfiguration, onChangeShowSkeletons } =
1173-
this.props;
1177+
const {
1178+
controlMode,
1179+
annotation,
1180+
onChangeRadius,
1181+
userConfiguration,
1182+
onChangeShowSkeletons,
1183+
dataset,
1184+
} = this.props;
11741185
const isPublicViewMode = controlMode === ControlModeEnum.VIEW;
11751186

11761187
if (isPublicViewMode || annotation.skeleton == null) {
@@ -1182,6 +1193,7 @@ class DatasetSettings extends React.PureComponent<DatasetSettingsProps, State> {
11821193
const isOnlyAnnotationLayer = annotation.annotationLayers.length === 1;
11831194
const { showSkeletons, tracingId } = skeletonTracing;
11841195
const activeNodeRadius = getActiveNode(skeletonTracing)?.radius ?? 0;
1196+
const unit = LongUnitToShortUnitMap[dataset.dataSource.scale.unit];
11851197
return (
11861198
<React.Fragment>
11871199
<div
@@ -1247,7 +1259,7 @@ class DatasetSettings extends React.PureComponent<DatasetSettingsProps, State> {
12471259
}}
12481260
>
12491261
<LogSliderSetting
1250-
label="Node Radius"
1262+
label={`Node Radius (${unit})`}
12511263
min={userSettings.nodeRadius.minimum}
12521264
max={userSettings.nodeRadius.maximum}
12531265
roundTo={0}
@@ -1258,9 +1270,9 @@ class DatasetSettings extends React.PureComponent<DatasetSettingsProps, State> {
12581270
/>
12591271
<NumberSliderSetting
12601272
label={
1261-
userConfiguration.overrideNodeRadius
1273+
(userConfiguration.overrideNodeRadius
12621274
? settings.particleSize
1263-
: `Min. ${settings.particleSize}`
1275+
: `Min. ${settings.particleSize}`) + ` (${unit})`
12641276
}
12651277
min={userSettings.particleSize.minimum}
12661278
max={userSettings.particleSize.maximum}
@@ -1280,7 +1292,7 @@ class DatasetSettings extends React.PureComponent<DatasetSettingsProps, State> {
12801292
/>
12811293
) : (
12821294
<LogSliderSetting
1283-
label={settings.clippingDistance}
1295+
label={settings.clippingDistance + ` (${unit})`}
12841296
roundTo={3}
12851297
min={userSettings.clippingDistance.minimum}
12861298
max={userSettings.clippingDistance.maximum}

0 commit comments

Comments
 (0)