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

Add measurement tools #7258

Merged
merged 34 commits into from
Oct 12, 2023
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
7e9cca9
WIP: add measurement tool
MichaelBuessemeyer Aug 10, 2023
7b66507
Make line mesh visible
MichaelBuessemeyer Aug 10, 2023
feffad5
add ui to show measured length
MichaelBuessemeyer Aug 17, 2023
a833b93
fix linting and tests
MichaelBuessemeyer Aug 17, 2023
aa0b825
only show tooltip, add option to switch units and to copy the measure…
MichaelBuessemeyer Aug 17, 2023
0e40634
Merge branch 'master' of github.com:scalableminds/webknossos into add…
MichaelBuessemeyer Aug 17, 2023
59dc2cd
Merge branch 'master' of github.com:scalableminds/webknossos into add…
MichaelBuessemeyer Aug 24, 2023
d2187c2
add area measurement tool
MichaelBuessemeyer Aug 24, 2023
ba61b1a
fix area calculation for all viewports
MichaelBuessemeyer Aug 24, 2023
a96589b
Merge branch 'master' of github.com:scalableminds/webknossos into add…
MichaelBuessemeyer Aug 31, 2023
bb1e52c
fix the tooltip position after drawing a measured area
MichaelBuessemeyer Aug 31, 2023
ac930f2
- bug fixes,
MichaelBuessemeyer Aug 31, 2023
725b966
remove filling area and add highlighted connection between start and …
MichaelBuessemeyer Sep 1, 2023
1f64636
clean up for review & add comments
MichaelBuessemeyer Sep 1, 2023
74f4c5f
adjust unit string for area measurement
MichaelBuessemeyer Sep 1, 2023
fc5a064
add changelog entry
MichaelBuessemeyer Sep 1, 2023
9fdb2f7
Merge branch 'master' of github.com:scalableminds/webknossos into add…
MichaelBuessemeyer Sep 1, 2023
7fc447c
Merge branch 'master' of github.com:scalableminds/webknossos into add…
MichaelBuessemeyer Sep 7, 2023
5fbc107
Merge branch 'master' of github.com:scalableminds/webknossos into add…
MichaelBuessemeyer Sep 7, 2023
f824392
apply feedback
MichaelBuessemeyer Sep 7, 2023
d2ccb2c
Merge branch 'master' of github.com:scalableminds/webknossos into add…
MichaelBuessemeyer Sep 21, 2023
6348abc
properly check for double clicks in measurement tools
MichaelBuessemeyer Sep 21, 2023
0ea1ee7
Merge branch 'master' of github.com:scalableminds/webknossos into add…
MichaelBuessemeyer Sep 21, 2023
12a348a
apply code review feedback
MichaelBuessemeyer Sep 21, 2023
3b1791d
make tooltip follow measurement when camera is moved
MichaelBuessemeyer Sep 21, 2023
49db897
Merge branch 'master' of github.com:scalableminds/webknossos into add…
MichaelBuessemeyer Sep 28, 2023
3d4a5fa
fix alt movement in measurement tools
MichaelBuessemeyer Sep 28, 2023
2979b81
Add tooltip to distance entry in context menu to explain it better
MichaelBuessemeyer Sep 28, 2023
50e92e5
reset measurement tools on deselect
MichaelBuessemeyer Sep 28, 2023
61d0ab1
don't let measurement tooltip get pointer events while measuring
MichaelBuessemeyer Sep 28, 2023
1b66fa3
fix bug where the measurement was directly reset after starting
MichaelBuessemeyer Sep 28, 2023
a4c9bf6
Merge branch 'master' into add-measurement-tool
MichaelBuessemeyer Oct 5, 2023
197a7dc
Merge branch 'master' of github.com:scalableminds/webknossos into add…
MichaelBuessemeyer Oct 11, 2023
ead52f7
extract magic value into constant
MichaelBuessemeyer Oct 11, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions frontend/javascripts/libs/format_utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,8 +140,8 @@ const nmFactorToUnit = new Map([
[1e9, "m"],
[1e12, "km"],
]);
export function formatNumberToLength(lengthInNm: number): string {
return formatNumberToUnit(lengthInNm, nmFactorToUnit);
export function formatNumberToLength(lengthInNm: number, roundTo: number = 2): string {
return formatNumberToUnit(lengthInNm, nmFactorToUnit, false, roundTo);
}

const byteFactorToUnit = new Map([
Expand Down
7 changes: 6 additions & 1 deletion frontend/javascripts/libs/resizable_buffer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,9 @@ class ResizableBuffer<T extends Float32Array> {
}

set(element: Array<number> | T, i: number): void {
this.ensureCapacity((i + 1) * this.elementLength);
this.buffer.set(element, i * this.elementLength);
this.length = Math.max(this.length, (i + 1) * this.elementLength);
}

push(element: Array<number> | T): void {
Expand Down Expand Up @@ -132,7 +134,10 @@ class ResizableBuffer<T extends Float32Array> {
const { buffer } = this;

while (this.capacity < newCapacity) {
this.capacity = Math.floor(this.capacity * GROW_MULTIPLIER);
this.capacity = Math.max(
this.capacity + this.elementLength,
Math.floor(this.capacity * GROW_MULTIPLIER),
);
this.capacity -= this.capacity % this.elementLength;
}

Expand Down
7 changes: 7 additions & 0 deletions frontend/javascripts/oxalis/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,8 @@ export enum AnnotationToolEnum {
QUICK_SELECT = "QUICK_SELECT",
BOUNDING_BOX = "BOUNDING_BOX",
PROOFREAD = "PROOFREAD",
LINE_MEASUREMENT = "LINE_MEASUREMENT",
AREA_MEASUREMENT = "AREA_MEASUREMENT",
}
export const VolumeTools: Array<keyof typeof AnnotationToolEnum> = [
AnnotationToolEnum.BRUSH,
Expand All @@ -205,6 +207,11 @@ export const ToolsWithInterpolationCapabilities: Array<keyof typeof AnnotationTo
AnnotationToolEnum.QUICK_SELECT,
];

export const MeasurementTools: Array<keyof typeof AnnotationToolEnum> = [
AnnotationToolEnum.LINE_MEASUREMENT,
AnnotationToolEnum.AREA_MEASUREMENT,
];

export type AnnotationTool = keyof typeof AnnotationToolEnum;
export const enum ContourModeEnum {
DRAW = "DRAW",
Expand Down
161 changes: 160 additions & 1 deletion frontend/javascripts/oxalis/controller/combinations/tool_controls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,11 @@ import {
} from "oxalis/model/actions/proofread_actions";
import { calculateGlobalPos } from "oxalis/model/accessors/view_mode_accessor";
import { V3 } from "libs/mjs";
import { setQuickSelectStateAction } from "oxalis/model/actions/ui_actions";
import {
hideMeasurementTooltipAction,
setQuickSelectStateAction,
showMeasurementTooltipAction,
} from "oxalis/model/actions/ui_actions";

export type ActionDescriptor = {
leftClick?: string;
Expand Down Expand Up @@ -780,6 +784,159 @@ export class QuickSelectTool {
static onToolDeselected() {}
}

export class LineMeasurementTool {
static DOUBLE_CLICK_TIME_THRESHOLD = 600;
static getPlaneMouseControls(_planeId: OrthoView): any {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getPlaneMouseControls of the area measurement tool doesn't accept that parameter. maybe make these interfaces consistent?

let initialPlane: OrthoView = OrthoViews.PLANE_XY;
let isMeasuring = false;
let lastLeftClickTime = 0;
const SceneController = getSceneController();
const { lineMeasurementGeometry } = SceneController;
const guardFromResettedTool = (func: Function) => {
// TODO: Problem typing is lost
return (...args: any) => {
if (lineMeasurementGeometry.isResetted && isMeasuring) {
isMeasuring = false;
return;
}
func(...args);
};
};
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All of the following three functions need to be guarded against the user having used escape while still in the process of measuring, to interrupt the measurement. When escape was pressed, the lineMeasurementGeometry carries that information in the field isResetted.

The guarding function takes care of guarding these three functions against the user using escape while still measuring. But the typing is lost (as far as I would guess) and the code ready quite quirky IMO. Maybe you have a better idea?

const mouseMove = guardFromResettedTool(
(_delta: Point2, pos: Point2, plane: OrthoView | null | undefined, evt: MouseEvent) => {
if (plane !== initialPlane || !isMeasuring) {
return;
}
const state = Store.getState();
const newPos = V3.floor(calculateGlobalPos(state, pos, initialPlane));
lineMeasurementGeometry.setTopPoint(newPos);
Store.dispatch(showMeasurementTooltipAction([evt.clientX, evt.clientY]));
},
);
const rightClick = guardFromResettedTool((pos: Point2, plane: OrthoView, event: MouseEvent) => {
if (isMeasuring) {
mouseMove({ x: 0, y: 0 }, pos, plane, event);
isMeasuring = false;
} else {
lineMeasurementGeometry.reset();
lineMeasurementGeometry.hide();
Store.dispatch(hideMeasurementTooltipAction());
}
});
const leftClick = guardFromResettedTool((pos: Point2, plane: OrthoView, event: MouseEvent) => {
const currentTime = Date.now();
if (currentTime - lastLeftClickTime <= this.DOUBLE_CLICK_TIME_THRESHOLD) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wouldn't it make sense to check that the two clicks were registered roughly at the same spot?

rightClick(pos, plane, event);
return;
}
lastLeftClickTime = currentTime;
const state = Store.getState();
const position = V3.floor(calculateGlobalPos(state, pos, plane));
initialPlane = plane;
if (!isMeasuring) {
lineMeasurementGeometry.setStartPoint(position, plane);
isMeasuring = true;
} else {
lineMeasurementGeometry.addPoint(position);
Store.dispatch(showMeasurementTooltipAction([event.clientX, event.clientY]));
}
});
return {
mouseMove,
rightClick,
leftClick,
};
}

static getActionDescriptors(
_activeTool: AnnotationTool,
_useLegacyBindings: boolean,
_shiftKey: boolean,
_ctrlKey: boolean,
_altKey: boolean,
): ActionDescriptor {
return {
leftClick: "Left Click to measure distance",
rightClick: "Finish Measurement and reset",
};
}

static onToolDeselected() {
const { lineMeasurementGeometry } = getSceneController();
lineMeasurementGeometry.reset();
lineMeasurementGeometry.hide();
Comment on lines +910 to +911
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are several places where reset and hide is called. How about adding a resetAndHide helper method?

Store.dispatch(hideMeasurementTooltipAction());
}
}

export class AreaMeasurementTool {
static getPlaneMouseControls(): any {
let initialPlane: OrthoView = OrthoViews.PLANE_XY;
let isMeasuring = false;
const SceneController = getSceneController();
const { areaMeasurementGeometry } = SceneController;
return {
leftDownMove: (
_delta: Point2,
pos: Point2,
id: string | null | undefined,
event: MouseEvent,
) => {
if (id == null) {
return;
}
if (!isMeasuring) {
initialPlane = id as OrthoView;
isMeasuring = true;
areaMeasurementGeometry.reset();
areaMeasurementGeometry.show();
areaMeasurementGeometry.setViewport(id as OrthoView);
}
if (id !== initialPlane) {
return;
}
const state = Store.getState();
const position = V3.floor(calculateGlobalPos(state, pos, initialPlane));
areaMeasurementGeometry.addEdgePoint(position);
Store.dispatch(showMeasurementTooltipAction([event.clientX, event.clientY]));
},
leftMouseUp: (event: MouseEvent) => {
if (!isMeasuring) {
return;
}
// Stop drawing area and close the drawn area if still measuring.
isMeasuring = false;
areaMeasurementGeometry.connectToStartPoint();
Store.dispatch(showMeasurementTooltipAction([event.clientX, event.clientY]));
},
rightClick: () => {
areaMeasurementGeometry.reset();
Store.dispatch(hideMeasurementTooltipAction());
},
};
}

static getActionDescriptors(
_activeTool: AnnotationTool,
_useLegacyBindings: boolean,
_shiftKey: boolean,
_ctrlKey: boolean,
_altKey: boolean,
): ActionDescriptor {
return {
leftDrag: "Drag to measure area",
rightClick: "Reset Measurement",
};
}

static onToolDeselected() {
const { areaMeasurementGeometry } = getSceneController();
areaMeasurementGeometry.reset();
areaMeasurementGeometry.hide();
Store.dispatch(hideMeasurementTooltipAction());
}
}

export class ProofreadTool {
static getPlaneMouseControls(_planeId: OrthoView, planeView: PlaneView): any {
return {
Expand Down Expand Up @@ -849,6 +1006,8 @@ const toolToToolClass = {
[AnnotationToolEnum.ERASE_BRUSH]: EraseTool,
[AnnotationToolEnum.FILL_CELL]: FillCellTool,
[AnnotationToolEnum.PICK_CELL]: PickCellTool,
[AnnotationToolEnum.LINE_MEASUREMENT]: LineMeasurementTool,
[AnnotationToolEnum.AREA_MEASUREMENT]: AreaMeasurementTool,
};
export function getToolClassForAnnotationTool(activeTool: AnnotationTool) {
return toolToToolClass[activeTool];
Expand Down
22 changes: 20 additions & 2 deletions frontend/javascripts/oxalis/controller/scene_controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,11 @@ import { getRenderer } from "oxalis/controller/renderer";
import { setSceneController } from "oxalis/controller/scene_controller_provider";
import ArbitraryPlane from "oxalis/geometries/arbitrary_plane";
import Cube from "oxalis/geometries/cube";
import { ContourGeometry, QuickSelectGeometry } from "oxalis/geometries/helper_geometries";
import {
ContourGeometry,
LineMeasurementGeometry,
QuickSelectGeometry,
} from "oxalis/geometries/helper_geometries";
import Plane from "oxalis/geometries/plane";
import Skeleton from "oxalis/geometries/skeleton";
import {
Expand Down Expand Up @@ -60,6 +64,10 @@ class SceneController {
contour: ContourGeometry;
// @ts-expect-error ts-migrate(2564) FIXME: Property 'quickSelectGeometry' has no initializer and is not d... Remove this comment to see the full error message
quickSelectGeometry: QuickSelectGeometry;
// @ts-expect-error ts-migrate(2564) FIXME: Property 'lineMeasurementGeometry' has no initializer and is not d... Remove this comment to see the full error message
lineMeasurementGeometry: LineMeasurementGeometry;
// @ts-expect-error ts-migrate(2564) FIXME: Property 'areaMeasurementGeometry' has no initializer and is not d... Remove this comment to see the full error message
areaMeasurementGeometry: ContourGeometry;
// @ts-expect-error ts-migrate(2304) FIXME: Cannot find name 'OrthoViewWithoutTDMap'.
planes: OrthoViewWithoutTDMap<Plane>;
// @ts-expect-error ts-migrate(2564) FIXME: Property 'rootNode' has no initializer and is not ... Remove this comment to see the full error message
Expand Down Expand Up @@ -238,6 +246,15 @@ class SceneController {
this.quickSelectGeometry = new QuickSelectGeometry();
this.annotationToolsGeometryGroup.add(this.quickSelectGeometry.getMeshGroup());

this.lineMeasurementGeometry = new LineMeasurementGeometry();
this.lineMeasurementGeometry
.getMeshes()
.forEach((mesh) => this.annotationToolsGeometryGroup.add(mesh));
this.areaMeasurementGeometry = new ContourGeometry(true);
this.areaMeasurementGeometry
.getMeshes()
.forEach((mesh) => this.annotationToolsGeometryGroup.add(mesh));

if (state.tracing.skeleton != null) {
this.addSkeleton((_state) => getSkeletonTracing(_state.tracing), true);
}
Expand Down Expand Up @@ -324,7 +341,8 @@ class SceneController {
this.taskBoundingBox?.updateForCam(id);

this.segmentMeshController.isosurfacesLODRootGroup.visible = id === OrthoViews.TDView;
this.annotationToolsGeometryGroup.visible = id !== OrthoViews.TDView;
this.annotationToolsGeometryGroup.visible = true || id !== OrthoViews.TDView;
this.lineMeasurementGeometry.updateForCam(id);

const originalPosition = getPosition(Store.getState().flycam);
if (id !== OrthoViews.TDView) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ import {
BoundingBoxTool,
QuickSelectTool,
ProofreadTool,
LineMeasurementTool,
AreaMeasurementTool,
} from "oxalis/controller/combinations/tool_controls";
import type {
ShowContextMenuFunction,
Expand Down Expand Up @@ -334,6 +336,8 @@ class PlaneController extends React.PureComponent<Props> {
this.props.showContextMenuAt,
);
const proofreadControls = ProofreadTool.getPlaneMouseControls(planeId, this.planeView);
const lineMeasurementControls = LineMeasurementTool.getPlaneMouseControls(planeId);
const areaMeasurementControls = AreaMeasurementTool.getPlaneMouseControls();

const allControlKeys = _.union(
Object.keys(moveControls),
Expand All @@ -345,6 +349,8 @@ class PlaneController extends React.PureComponent<Props> {
Object.keys(boundingBoxControls),
Object.keys(quickSelectControls),
Object.keys(proofreadControls),
Object.keys(lineMeasurementControls),
Object.keys(areaMeasurementControls),
);

const controls: Record<string, any> = {};
Expand All @@ -363,6 +369,8 @@ class PlaneController extends React.PureComponent<Props> {
[AnnotationToolEnum.BOUNDING_BOX]: boundingBoxControls[controlKey],
[AnnotationToolEnum.QUICK_SELECT]: quickSelectControls[controlKey],
[AnnotationToolEnum.PROOFREAD]: proofreadControls[controlKey],
[AnnotationToolEnum.LINE_MEASUREMENT]: lineMeasurementControls[controlKey],
[AnnotationToolEnum.AREA_MEASUREMENT]: areaMeasurementControls[controlKey],
});
}

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 @@ -222,6 +222,7 @@ const defaultState: OxalisState = {
activeUser: null,
activeOrganization: null,
uiInformation: {
measurementTooltipPosition: null,
activeTool: "MOVE",
showDropzoneModal: false,
showVersionRestore: false,
Expand Down
Loading