Skip to content

Commit

Permalink
Extend JS API with createNode functionality and refactor (#7998)
Browse files Browse the repository at this point in the history
* rename setWaypoint to handleCreateNode in arbitrary controller

* rename setWaypoint to handleNodeCreation

* refactor

* implement createNode in api_latest; refactor to allow better parameteriziation; fix that centering node did not work when new node was not activated

* refactor getActiveNode accessor to not use a Maybe

* refactor getActiveTree accessor to not use a Maybe

* update changelog

* remove unused import

* allow to pass additionalCoordinates when creating nodes via API

* set skipCenteringAnimationInThirdDimension to false when using api
  • Loading branch information
philippotto authored and dieknolle3333 committed Sep 2, 2024
1 parent c126859 commit bc5f278
Show file tree
Hide file tree
Showing 20 changed files with 224 additions and 133 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.unreleased.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ For upgrade instructions, please check the [migration guide](MIGRATIONS.released
- To improve performance, only the visible bounding boxes are rendered in the bounding box tab (so-called virtualization). [#7974](https://github.com/scalableminds/webknossos/pull/7974)
- Added support for reading zstd-compressed zarr2 datasets [#7964](https://github.com/scalableminds/webknossos/pull/7964)
- The alignment job is in a separate tab of the "AI Tools" now. The "Align Sections" AI job now supports including manually created matches between adjacent section given as skeletons. [#7967](https://github.com/scalableminds/webknossos/pull/7967)
- Added `api.tracing.createNode(position, options)`` to the front-end API. [#7998](https://github.com/scalableminds/webknossos/pull/7998)
- Added a feature to register all segments for a given bounding box at once via the context menu of the bounding box. [#7979](https://github.com/scalableminds/webknossos/pull/7979)

### Changed
Expand Down
4 changes: 0 additions & 4 deletions frontend/javascripts/libs/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -669,10 +669,6 @@ export function withoutValues<T>(arr: Array<T>, elements: Array<T>): Array<T> {
return arr.filter((x) => !auxSet.has(x));
}

export function zipMaybe<T, U>(maybeA: Maybe<T>, maybeB: Maybe<U>): Maybe<[T, U]> {
return maybeA.chain((valueA) => maybeB.map((valueB) => [valueA, valueB]));
}

// Maybes getOrElse is defined as getOrElse(defaultValue: T): T, which is why
// you can't do getOrElse(null) without flow complaining
export function toNullable<T>(_maybe: Maybe<T>): T | null | undefined {
Expand Down
68 changes: 56 additions & 12 deletions frontend/javascripts/oxalis/api/api_latest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,10 @@ import { setLayerTransformsAction } from "oxalis/model/actions/dataset_actions";
import { ResolutionInfo } from "oxalis/model/helpers/resolution_info";
import { type AdditionalCoordinate } from "types/api_flow_types";
import { getMaximumGroupId } from "oxalis/model/reducers/skeletontracing_reducer_helpers";
import {
createSkeletonNode,
getOptionsForCreateSkeletonNode,
} from "oxalis/controller/combinations/skeleton_handlers";

type TransformSpec =
| { type: "scale"; args: [Vector3, Vector3] }
Expand Down Expand Up @@ -248,19 +252,15 @@ class TracingApi {
*/
getActiveNodeId(): number | null | undefined {
const tracing = assertSkeleton(Store.getState().tracing);
return getActiveNode(tracing)
.map((node) => node.id)
.getOrElse(null);
return getActiveNode(tracing)?.id ?? null;
}

/**
* Returns the id of the current active tree.
*/
getActiveTreeId(): number | null | undefined {
const tracing = assertSkeleton(Store.getState().tracing);
return getActiveTree(tracing)
.map((tree) => tree.treeId)
.getOrElse(null);
return getActiveTree(tracing)?.treeId ?? null;
}

/**
Expand Down Expand Up @@ -322,11 +322,21 @@ class TracingApi {
}

/**
* Creates a new and empty tree
* Creates a new and empty tree. Returns the
* id of that tree.
*/
createTree() {
assertSkeleton(Store.getState().tracing);
Store.dispatch(createTreeAction());
let treeId = null;
Store.dispatch(
createTreeAction((id) => {
treeId = id;
}),
);
if (treeId == null) {
throw new Error("Could not create tree.");
}
return treeId;
}

/**
Expand All @@ -337,6 +347,37 @@ class TracingApi {
Store.dispatch(deleteTreeAction(treeId));
}

/**
* Creates a new node in the current tree. If the active tree
* is not empty, the node will be connected with an edge to
* the currently active node.
*/
createNode(
position: Vector3,
options?: {
additionalCoordinates?: AdditionalCoordinate[];
rotation?: Vector3;
center?: boolean;
branchpoint?: boolean;
activate?: boolean;
skipCenteringAnimationInThirdDimension?: boolean;
},
) {
assertSkeleton(Store.getState().tracing);
const defaultOptions = getOptionsForCreateSkeletonNode();
createSkeletonNode(
position,
options?.additionalCoordinates ?? defaultOptions.additionalCoordinates,
options?.rotation ?? defaultOptions.rotation,
options?.center ?? defaultOptions.center,
options?.branchpoint ?? defaultOptions.branchpoint,
options?.activate ?? defaultOptions.activate,
// This is the only parameter where we don't fall back to the default option,
// as the parameter mostly makes sense when the user creates a node *manually*.
options?.skipCenteringAnimationInThirdDimension ?? false,
);
}

/**
* Completely resets the skeleton tracing.
*/
Expand Down Expand Up @@ -1296,20 +1337,23 @@ class TracingApi {
* Starts an animation to center the given position. See setCameraPosition for a non-animated version of this function.
*
* @param position - Vector3
* @param skipDimensions - Boolean which decides whether the third dimension shall also be animated (defaults to true)
* @param skipCenteringAnimationInThirdDimension -
* Boolean which decides whether the third dimension shall also be animated (defaults to true)
* When true, this lets the user still manipulate the "third dimension"
* during the animation (important because otherwise the user cannot continue to trace until
* the animation is over).
* @param rotation - Vector3 (optional) - Will only be noticeable in flight or oblique mode.
* @example
* api.tracing.centerPositionAnimated([0, 0, 0])
*/
centerPositionAnimated(
position: Vector3,
skipDimensions: boolean = true,
skipCenteringAnimationInThirdDimension: boolean = true,
rotation?: Vector3,
): void {
// Let the user still manipulate the "third dimension" during animation
const { activeViewport } = Store.getState().viewModeData.plane;
const dimensionToSkip =
skipDimensions && activeViewport !== OrthoViews.TDView
skipCenteringAnimationInThirdDimension && activeViewport !== OrthoViews.TDView
? dimensions.thirdDimensionForPlane(activeViewport)
: null;
const curPosition = getPosition(Store.getState().flycam);
Expand Down
8 changes: 2 additions & 6 deletions frontend/javascripts/oxalis/api/api_v2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,19 +92,15 @@ class TracingApi {
*/
getActiveNodeId(): number | null | undefined {
const tracing = assertSkeleton(Store.getState().tracing);
return getActiveNode(tracing)
.map((node) => node.id)
.getOrElse(null);
return getActiveNode(tracing)?.id ?? null;
}

/**
* Returns the id of the current active tree.
*/
getActiveTreeId(): number | null | undefined {
const tracing = assertSkeleton(Store.getState().tracing);
return getActiveTree(tracing)
.map((tree) => tree.treeId)
.getOrElse(null);
return getActiveTree(tracing)?.treeId ?? null;
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import type { OrthoView, OrthoViewMap, Point2, Vector3, Viewport } from "oxalis/
import { OrthoViews } from "oxalis/constants";
import { V3 } from "libs/mjs";
import _ from "lodash";
import { enforce, values } from "libs/utils";
import { values } from "libs/utils";
import {
enforceSkeletonTracing,
getSkeletonTracing,
Expand Down Expand Up @@ -47,6 +47,7 @@ import { getClosestHoveredBoundingBox } from "oxalis/controller/combinations/bou
import { getEnabledColorLayers } from "oxalis/model/accessors/dataset_accessor";
import ArbitraryView from "oxalis/view/arbitrary_view";
import { showContextMenuAction } from "oxalis/model/actions/ui_actions";
import { AdditionalCoordinate } from "types/api_flow_types";
const OrthoViewToNumber: OrthoViewMap<number> = {
[OrthoViews.PLANE_XY]: 0,
[OrthoViews.PLANE_YZ]: 1,
Expand All @@ -64,9 +65,10 @@ export function handleMergeTrees(

// otherwise we have hit the background and do nothing
if (nodeId != null && nodeId > 0) {
getActiveNode(skeletonTracing).map((activeNode) =>
Store.dispatch(mergeTreesAction(activeNode.id, nodeId)),
);
const activeNode = getActiveNode(skeletonTracing);
if (activeNode) {
Store.dispatch(mergeTreesAction(activeNode.id, nodeId));
}
}
}
export function handleDeleteEdge(
Expand All @@ -80,9 +82,10 @@ export function handleDeleteEdge(

// otherwise we have hit the background and do nothing
if (nodeId != null && nodeId > 0) {
getActiveNode(skeletonTracing).map((activeNode) =>
Store.dispatch(deleteEdgeAction(activeNode.id, nodeId)),
);
const activeNode = getActiveNode(skeletonTracing);
if (activeNode) {
Store.dispatch(deleteEdgeAction(activeNode.id, nodeId));
}
}
}
export function handleSelectNode(
Expand All @@ -101,7 +104,7 @@ export function handleSelectNode(

return false;
}
export function handleCreateNode(position: Point2, ctrlPressed: boolean) {
export function handleCreateNodeFromEvent(position: Point2, ctrlPressed: boolean) {
const state = Store.getState();

if (isMagRestrictionViolated(state)) {
Expand All @@ -123,7 +126,7 @@ export function handleCreateNode(position: Point2, ctrlPressed: boolean) {
}

const globalPosition = calculateGlobalPos(state, position);
setWaypoint(globalPosition, activeViewport, ctrlPressed);
handleCreateNodeFromGlobalPosition(globalPosition, activeViewport, ctrlPressed);
}
export function handleOpenContextMenu(
planeView: PlaneView,
Expand Down Expand Up @@ -233,53 +236,83 @@ export function finishNodeMovement(nodeId: number) {
);
}

export function setWaypoint(
export function handleCreateNodeFromGlobalPosition(
position: Vector3,
activeViewport: OrthoView,
ctrlIsPressed: boolean,
): void {
const skeletonTracing = enforceSkeletonTracing(Store.getState().tracing);
const activeNodeMaybe = getActiveNode(skeletonTracing);
const rotation = getRotationOrtho(activeViewport);
// set the new trace direction
activeNodeMaybe.map((activeNode) => {
const activeNodePosition = getNodePosition(activeNode, Store.getState());
return Store.dispatch(
setDirectionAction([
position[0] - activeNodePosition[0],
position[1] - activeNodePosition[1],
position[2] - activeNodePosition[2],
]),
);
});
const state = Store.getState();
// Create a new tree automatically if the corresponding setting is true and allowed
const createNewTree =
state.tracing.restrictions.somaClickingAllowed && state.userConfiguration.newNodeNewTree;
if (createNewTree) {
Store.dispatch(createTreeAction());
}

const {
additionalCoordinates,
rotation,
center,
branchpoint,
activate,
skipCenteringAnimationInThirdDimension,
} = getOptionsForCreateSkeletonNode(activeViewport, ctrlIsPressed);
createSkeletonNode(
position,
additionalCoordinates,
rotation,
center,
branchpoint,
activate,
skipCenteringAnimationInThirdDimension,
);
}

export function getOptionsForCreateSkeletonNode(
activeViewport: OrthoView | null = null,
ctrlIsPressed: boolean = false,
) {
const state = Store.getState();
const additionalCoordinates = state.flycam.additionalCoordinates;
const skeletonTracing = enforceSkeletonTracing(state.tracing);
const activeNode = getActiveNode(skeletonTracing);
const rotation = getRotationOrtho(activeViewport || state.viewModeData.plane.activeViewport);

// Center node if the corresponding setting is true. Only pressing CTRL can override this.
const center = state.userConfiguration.centerNewNode && !ctrlIsPressed;

// Only create a branchpoint if CTRL is pressed. Unless newNodeNewTree is activated (branchpoints make no sense then)
const branchpoint = ctrlIsPressed && !state.userConfiguration.newNodeNewTree;

// Always activate the new node unless CTRL is pressed. If there is no current node,
// the new one is still activated regardless of CTRL (otherwise, using CTRL+click in an empty tree multiple times would
// not create any edges; see https://github.com/scalableminds/webknossos/issues/5303).
const activate = !ctrlIsPressed || activeNodeMaybe.isNothing;
addNode(position, rotation, createNewTree, center, branchpoint, activate);
const activate = !ctrlIsPressed || activeNode == null;

const skipCenteringAnimationInThirdDimension = true;

return {
additionalCoordinates,
rotation,
center,
branchpoint,
activate,
skipCenteringAnimationInThirdDimension,
};
}

function addNode(
export function createSkeletonNode(
position: Vector3,
additionalCoordinates: AdditionalCoordinate[] | null,
rotation: Vector3,
createNewTree: boolean,
center: boolean,
branchpoint: boolean,
activate: boolean,
skipCenteringAnimationInThirdDimension: boolean,
): void {
if (createNewTree) {
Store.dispatch(createTreeAction());
}
updateTraceDirection(position);

const state = Store.getState();
let state = Store.getState();
const enabledColorLayers = getEnabledColorLayers(state.dataset, state.datasetConfiguration);
const activeMagIndices = getActiveMagIndicesForLayers(state);
const activeMagIndicesOfEnabledColorLayers = _.pick(
Expand All @@ -292,7 +325,7 @@ function addNode(
Store.dispatch(
createNodeAction(
untransformNodePosition(position, state),
state.flycam.additionalCoordinates,
additionalCoordinates,
rotation,
OrthoViewToNumber[Store.getState().viewModeData.plane.activeViewport],
// This is the magnification index at which the node was created. Since
Expand All @@ -305,21 +338,43 @@ function addNode(
),
);

// We need a reference to the new store state, so that the new node exists in it.
state = Store.getState();

if (center) {
// we created a new node, so get a new reference from the current store state
const newState = Store.getState();
enforce(getActiveNode)(newState.tracing.skeleton).map((newActiveNode) =>
// Center the position of the active node without modifying the "third" dimension (see centerPositionAnimated)
// This is important because otherwise the user cannot continue to trace until the animation is over
api.tracing.centerPositionAnimated(getNodePosition(newActiveNode, state), true),
);
const newSkeleton = enforceSkeletonTracing(state.tracing);
// Note that the new node isn't necessarily active
const newNodeId = newSkeleton.cachedMaxNodeId;

const { activeTreeId } = newSkeleton;
getNodeAndTree(newSkeleton, newNodeId, activeTreeId).map(([, newNode]) => {
api.tracing.centerPositionAnimated(
getNodePosition(newNode, state),
skipCenteringAnimationInThirdDimension,
);
});
}

if (branchpoint) {
Store.dispatch(createBranchPointAction());
}
}

function updateTraceDirection(position: Vector3) {
const skeletonTracing = enforceSkeletonTracing(Store.getState().tracing);
const activeNode = getActiveNode(skeletonTracing);
if (activeNode != null) {
const activeNodePosition = getNodePosition(activeNode, Store.getState());
return Store.dispatch(
setDirectionAction([
position[0] - activeNodePosition[0],
position[1] - activeNodePosition[1],
position[2] - activeNodePosition[2],
]),
);
}
}

export function moveAlongDirection(reverse: boolean = false): void {
const directionInverter = reverse ? -1 : 1;
const { flycam } = Store.getState();
Expand Down
Loading

0 comments on commit bc5f278

Please sign in to comment.