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

Extend JS API with createNode functionality and refactor #7998

Merged
merged 13 commits into from
Aug 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
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