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 context menu for nodes #4950

Merged
merged 22 commits into from
Jan 15, 2021
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
82a2dfe
first context menu version
MichaelBuessemeyer Nov 20, 2020
94581f6
Merge branch 'master' of github.com:scalableminds/webknossos into add…
MichaelBuessemeyer Nov 23, 2020
cad6981
add more functionally to node context menu
MichaelBuessemeyer Nov 23, 2020
bf806c7
added measure length between nodes using the dijkstra algorithm
MichaelBuessemeyer Nov 23, 2020
cf8a967
add popover with shortcut hints and some more options
MichaelBuessemeyer Nov 30, 2020
41b487b
add option to hide a tree
MichaelBuessemeyer Nov 30, 2020
10f04b6
add context menu shown when there is no selected node
MichaelBuessemeyer Nov 30, 2020
9774288
Merge branch 'master' into add-context-menu-for-nodes
MichaelBuessemeyer Nov 30, 2020
8f05458
apply feedback
MichaelBuessemeyer Dec 4, 2020
ebbaa7a
apply some feedback
MichaelBuessemeyer Dec 8, 2020
8edf3cd
remove isUsingFirefox method
MichaelBuessemeyer Dec 11, 2020
70eb945
Merge branch 'master' of github.com:scalableminds/webknossos into add…
MichaelBuessemeyer Jan 8, 2021
6bfc47f
remove possible interactions hint
MichaelBuessemeyer Jan 8, 2021
63ce6c7
added doc entry
MichaelBuessemeyer Jan 8, 2021
36a9487
revert using short assignment
MichaelBuessemeyer Jan 8, 2021
223411a
remove unused css
MichaelBuessemeyer Jan 8, 2021
34dd8f0
add changelog entry
MichaelBuessemeyer Jan 8, 2021
acff406
Merge branch 'master' of github.com:scalableminds/webknossos into add…
MichaelBuessemeyer Jan 11, 2021
4c3b2ef
revert using compound assignment
MichaelBuessemeyer Jan 11, 2021
4e8392d
Merge branch 'master' into add-context-menu-for-nodes
MichaelBuessemeyer Jan 14, 2021
827b446
fix linting errors by disabing eslint rule
MichaelBuessemeyer Jan 15, 2021
84c6588
Merge branch 'add-context-menu-for-nodes' of github.com:scalableminds…
MichaelBuessemeyer Jan 15, 2021
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
12 changes: 12 additions & 0 deletions frontend/javascripts/libs/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import Maybe from "data.maybe";
import _ from "lodash";
import naturalSort from "javascript-natural-sort";
import { V3 } from "libs/mjs";

import type { APIUser } from "types/api_flow_types";
import type { BoundingBoxObject } from "oxalis/store";
Expand Down Expand Up @@ -333,6 +334,17 @@ export function vector3ToPoint3([x, y, z]: Vector3): Point3 {
return { x, y, z };
}

export function distanceBetweenVectors(
firstPosition: Vector3,
secondPosition: Vector3,
datasetScale: Vector3,
): number {
const scaledFirstPosition = V3.scale3(firstPosition, datasetScale);
const scaledSecondPosition = V3.scale3(secondPosition, datasetScale);
const diffVector = V3.sub(scaledFirstPosition, scaledSecondPosition);
return V3.length(diffVector);
}
MichaelBuessemeyer marked this conversation as resolved.
Show resolved Hide resolved

export function isUserTeamManager(user: APIUser): boolean {
return _.findIndex(user.teams, team => team.isTeamManager) >= 0;
}
Expand Down
2 changes: 2 additions & 0 deletions frontend/javascripts/messages.js
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,8 @@ instead. Only enable this option if you understand its effect. All layers will n
"tracing.out_of_task_bounds": "The current position is outside of the task's bounding box.",
"tracing.copy_position": "Copy position to clipboard.",
"tracing.copy_rotation": "Copy rotation to clipboard.",
"tracing.tree_length_notification": (treeName: string, length: string) =>
`The tree ${treeName} has a total path length of ${length}.`,
"tracing.sharing_modal_basic_information": (sharingActiveNode?: boolean) =>
`This link includes the ${
sharingActiveNode ? "active tree node," : ""
Expand Down
56 changes: 56 additions & 0 deletions frontend/javascripts/oxalis/api/api_latest.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import {
import {
findTreeByNodeId,
getNodeAndTree,
getNodeAndTreeOrNull,
getActiveNode,
getActiveTree,
getTree,
Expand Down Expand Up @@ -100,6 +101,7 @@ import Store, {
type VolumeTracing,
} from "oxalis/store";
import Toast, { type ToastStyle } from "libs/toast";
import PriorityQueue from "js-priority-queue";
import UrlManager from "oxalis/controller/url_manager";
import Request from "libs/request";
import * as Utils from "libs/utils";
Expand Down Expand Up @@ -631,6 +633,60 @@ class TracingApi {
return totalLength;
}

measureLengthBetweenNodes(sourceNodeId: number, targetNodeId: number): number {
MichaelBuessemeyer marked this conversation as resolved.
Show resolved Hide resolved
const skeletonTracing = assertSkeleton(Store.getState().tracing);
const { node: sourceNode, tree: sourceTree } = getNodeAndTreeOrNull(
skeletonTracing,
sourceNodeId,
);
const { node: targetNode, tree: targetTree } = getNodeAndTreeOrNull(
skeletonTracing,
targetNodeId,
);
if (
sourceNode == null ||
targetNode == null ||
sourceTree == null ||
sourceTree !== targetTree
) {
return 0;
MichaelBuessemeyer marked this conversation as resolved.
Show resolved Hide resolved
}
const firstScaledPosition = new Float32Array([0, 0, 0]);
const secondScaledPosition = new Float32Array([0, 0, 0]);
const diffVector = new Float32Array([0, 0, 0]);
MichaelBuessemeyer marked this conversation as resolved.
Show resolved Hide resolved
const datasetScale = Store.getState().dataset.dataSource.scale;
// We use the Dijkstra algorithm to get the shortest path between the nodes.
const distanceMap = {};
for (const node of sourceTree.nodes.values()) {
distanceMap[node.id] = Number.POSITIVE_INFINITY;
}
MichaelBuessemeyer marked this conversation as resolved.
Show resolved Hide resolved
distanceMap[sourceNode.id] = 0;
// The priority queue saves node id and distance tuples.
const priorityQueue = new PriorityQueue<[number, number]>({
comparator: ([_first, firstDistance], [_second, secondDistance]) =>
firstDistance <= secondDistance ? -1 : 1,
});
priorityQueue.queue([sourceNodeId, 0]);
while (priorityQueue.length > 0) {
const [nextNodeId, distance] = priorityQueue.dequeue();
const nextNodePosition = sourceTree.nodes.get(nextNodeId).position;
V3.scale3(nextNodePosition, datasetScale, firstScaledPosition);
// Calculate the distance to all neighbours and update the distances.
for (const { source, target } of sourceTree.edges.getEdgesForNode(nextNodeId)) {
const neighbourNodeId = source === nextNodeId ? target : source;
const neightbourPosition = sourceTree.nodes.get(neighbourNodeId).position;
V3.scale3(neightbourPosition, datasetScale, secondScaledPosition);
V3.sub(firstScaledPosition, secondScaledPosition, diffVector);
const neighbourDistance = distance + V3.length(diffVector);
if (neighbourDistance < distanceMap[neighbourNodeId]) {
distanceMap[neighbourNodeId] = neighbourDistance;
priorityQueue.queue([neighbourNodeId, neighbourDistance]);
}
}
}
return distanceMap[targetNodeId];
}

/**
* Starts an animation to center the given position.
*
Expand Down
5 changes: 3 additions & 2 deletions frontend/javascripts/oxalis/controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ import * as Utils from "libs/utils";
import type { APIUser } from "types/api_flow_types";

import app from "app";
import constants, { ControlModeEnum, type ViewMode } from "oxalis/constants";
import constants, { ControlModeEnum, type ViewMode, type Vector3 } from "oxalis/constants";
import messages from "messages";
import window, { document } from "libs/window";

Expand All @@ -49,6 +49,7 @@ type OwnProps = {|
initialCommandType: TraceOrViewCommand,
controllerStatus: ControllerStatus,
setControllerStatus: ControllerStatus => void,
showNodeContextMenuAt: (number, number, ?number, Vector3, Vector3) => void,
|};
type StateProps = {|
viewMode: ViewMode,
Expand Down Expand Up @@ -331,7 +332,7 @@ class Controller extends React.PureComponent<PropsWithRouter, State> {
if (isArbitrary) {
return <ArbitraryController viewMode={viewMode} />;
} else if (isPlane) {
return <PlaneController />;
return <PlaneController showNodeContextMenuAt={this.props.showNodeContextMenuAt} />;
} else {
// At the moment, all possible view modes consist of the union of MODES_ARBITRARY and MODES_PLANE
// In case we add new viewmodes, the following error will be thrown.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,11 +69,14 @@ function simulateTracing(nodesPerTree: number = -1, nodesAlreadySet: number = 0)
}

const [x, y, z] = getPosition(Store.getState().flycam);
setWaypoint([x + 1, y + 1, z], false);
setWaypoint([x + 1, y + 1, z], [0, 0, 0], false);
_.defer(() => simulateTracing(nodesPerTree, nodesAlreadySet + 1));
}

export function getPlaneMouseControls(planeView: PlaneView) {
export function getPlaneMouseControls(
planeView: PlaneView,
showNodeContextMenuAt: (number, number, ?number, Vector3, Vector3) => void,
) {
return {
leftDownMove: (delta: Point2, pos: Point2, _id: ?string, event: MouseEvent) => {
const { tracing } = Store.getState();
Expand All @@ -85,26 +88,18 @@ export function getPlaneMouseControls(planeView: PlaneView) {
},
leftClick: (pos: Point2, plane: OrthoView, event: MouseEvent, isTouch: boolean) =>
onClick(planeView, pos, event.shiftKey, event.altKey, event.ctrlKey, plane, isTouch, event),
rightClick: (pos: Point2, plane: OrthoView, event: MouseEvent) => {
const state = Store.getState();
if (isMagRestrictionViolated(state)) {
// The current zoom value violates the specified magnification-restriction in the
// task type. Therefore, we abort the action here.
// Actually, one would need to handle more skeleton actions (e.g., deleting a node),
// but not all (e.g., deleting a tree from the tree tab should be allowed). Therefore,
// this solution is a bit of a shortcut. However, it should cover 90% of the use case
// for restricting the rendered magnification.
// See https://github.com/scalableminds/webknossos/pull/4891 for context and
// https://github.com/scalableminds/webknossos/issues/4838 for the follow-up issue.
return;
}

const { volume } = state.tracing;
if (!volume || volume.activeTool === VolumeToolEnum.MOVE) {
// We avoid creating nodes when in brushing mode.
setWaypoint(calculateGlobalPos(pos), event.ctrlKey);
}
},
rightClick: (pos: Point2, plane: OrthoView, event: MouseEvent, isTouch: boolean) =>
onRightClick(
planeView,
pos,
event.shiftKey,
event.altKey,
event.ctrlKey,
plane,
isTouch,
event,
showNodeContextMenuAt,
),
};
}

Expand Down Expand Up @@ -279,20 +274,12 @@ export function getLoopedKeyboardControls() {
};
}

function onClick(
function maybeGetNodeIdFromPosition(
planeView: PlaneView,
position: Point2,
shiftPressed: boolean,
altPressed: boolean,
ctrlPressed: boolean,
plane: OrthoView,
isTouch: boolean,
event?: MouseEvent,
): void {
if (!shiftPressed && !isTouch && !(ctrlPressed && event != null)) {
// do nothing
return;
}
): ?number {
const SceneController = getSceneController();

// render the clicked viewport with picking enabled
Expand Down Expand Up @@ -320,10 +307,28 @@ function onClick(

// prevent flickering sometimes caused by picking
planeView.renderFunction(true);
return nodeId > 0 ? nodeId : null;
}

function onClick(
planeView: PlaneView,
position: Point2,
shiftPressed: boolean,
altPressed: boolean,
ctrlPressed: boolean,
plane: OrthoView,
isTouch: boolean,
event?: MouseEvent,
): void {
if (!shiftPressed && !isTouch && !(ctrlPressed && event != null)) {
// do nothing
return;
}
const nodeId = maybeGetNodeIdFromPosition(planeView, position, plane, isTouch);

const skeletonTracing = enforceSkeletonTracing(Store.getState().tracing);
// otherwise we have hit the background and do nothing
if (nodeId > 0) {
if (nodeId != null && nodeId > 0) {
if (altPressed) {
getActiveNode(skeletonTracing).map(activeNode =>
Store.dispatch(mergeTreesAction(activeNode.id, nodeId)),
Expand All @@ -340,11 +345,51 @@ function onClick(
}
}

function setWaypoint(position: Vector3, ctrlPressed: boolean): void {
function onRightClick(
planeView: PlaneView,
position: Point2,
shiftPressed: boolean,
altPressed: boolean,
ctrlPressed: boolean,
plane: OrthoView,
isTouch: boolean,
event: MouseEvent,
showNodeContextMenuAt: (number, number, ?number, Vector3, Vector3) => void,
) {
const state = Store.getState();
if (isMagRestrictionViolated(state)) {
// The current zoom value violates the specified magnification-restriction in the
// task type. Therefore, we abort the action here.
// Actually, one would need to handle more skeleton actions (e.g., deleting a node),
// but not all (e.g., deleting a tree from the tree tab should be allowed). Therefore,
// this solution is a bit of a shortcut. However, it should cover 90% of the use case
// for restricting the rendered magnification.
// See https://github.com/scalableminds/webknossos/pull/4891 for context and
// https://github.com/scalableminds/webknossos/issues/4838 for the follow-up issue.
return;
}
const { activeViewport } = Store.getState().viewModeData.plane;
if (activeViewport === OrthoViews.TDView) {
return;
}

const { volume } = state.tracing;
if (!volume || volume.activeTool === VolumeToolEnum.MOVE) {
// We avoid creating nodes when in brushing mode.
const nodeId = event.shiftKey
? maybeGetNodeIdFromPosition(planeView, position, plane, isTouch)
: null;
const rotation = getRotationOrtho(activeViewport);
MichaelBuessemeyer marked this conversation as resolved.
Show resolved Hide resolved
const globalPosition = calculateGlobalPos(position);
if (event.shiftKey) {
showNodeContextMenuAt(event.pageX, event.pageY, nodeId, globalPosition, rotation);
} else {
setWaypoint(globalPosition, rotation, ctrlPressed);
}
}
}

export function setWaypoint(position: Vector3, rotation: Vector3, ctrlPressed: boolean): void {
const skeletonTracing = enforceSkeletonTracing(Store.getState().tracing);
const activeNodeMaybe = getActiveNode(skeletonTracing);

Expand All @@ -359,7 +404,6 @@ function setWaypoint(position: Vector3, ctrlPressed: boolean): void {
),
);

const rotation = getRotationOrtho(activeViewport);
addNode(position, rotation, !ctrlPressed);

// Ctrl + right click to set new not active branchpoint
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,11 +69,17 @@ function ensureNonConflictingHandlers(skeletonControls: Object, volumeControls:
}
}

type OwnProps = {| showNodeContextMenuAt: (number, number, ?number, Vector3, Vector3) => void |};

type StateProps = {|
tracing: Tracing,
is2d: boolean,
|};
type Props = {| ...StateProps |};

type Props = {|
...StateProps,
...OwnProps,
|};

export const movePlane = (v: Vector3, increaseSpeedWithZoom: boolean = true) => {
const { activeViewport } = Store.getState().viewModeData.plane;
Expand Down Expand Up @@ -173,7 +179,7 @@ class PlaneController extends React.PureComponent<Props> {
...skeletonControls
} =
this.props.tracing.skeleton != null
? skeletonController.getPlaneMouseControls(this.planeView)
? skeletonController.getPlaneMouseControls(this.planeView, this.props.showNodeContextMenuAt)
: emptyDefaultHandler;

const {
Expand Down Expand Up @@ -625,4 +631,4 @@ export function mapStateToProps(state: OxalisState): StateProps {
}

export { PlaneController as PlaneControllerClass };
export default connect<Props, {||}, _, _, _, _>(mapStateToProps)(PlaneController);
export default connect<Props, OwnProps, _, _, _, _>(mapStateToProps)(PlaneController);
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,11 @@ type CreateNodeAction = {
treeId?: ?number,
dontActivate?: boolean,
};
type CreateEdgeAction = {
type: "CREATE_EDGE",
sourceNodeId: number,
targetNodeId: number,
};
type DeleteNodeAction = {
type: "DELETE_NODE",
nodeId?: number,
Expand Down Expand Up @@ -128,6 +133,7 @@ type NoAction = { type: "NONE" };
export type SkeletonTracingAction =
| InitializeSkeletonTracingAction
| CreateNodeAction
| CreateEdgeAction
| DeleteNodeAction
| DeleteEdgeAction
| SetActiveNodeAction
Expand Down Expand Up @@ -233,6 +239,12 @@ export const createNodeAction = (
timestamp,
});

export const createEdgeAction = (sourceNodeId: number, targetNodeId: number): CreateEdgeAction => ({
type: "CREATE_EDGE",
sourceNodeId,
targetNodeId,
});
MichaelBuessemeyer marked this conversation as resolved.
Show resolved Hide resolved

export const deleteNodeAction = (
nodeId?: number,
treeId?: number,
Expand Down
Loading