diff --git a/frontend/javascripts/oxalis/model/actions/skeletontracing_actions.js b/frontend/javascripts/oxalis/model/actions/skeletontracing_actions.js index 8c7184e028..71e05756f2 100644 --- a/frontend/javascripts/oxalis/model/actions/skeletontracing_actions.js +++ b/frontend/javascripts/oxalis/model/actions/skeletontracing_actions.js @@ -74,6 +74,11 @@ type CreateBranchPointAction = { timestamp: number, }; type DeleteBranchPointAction = { type: "DELETE_BRANCHPOINT" }; +type DeleteBranchpointByIdAction = { + type: "DELETE_BRANCHPOINT_BY_ID", + nodeId: number, + treeId: number, +}; type ToggleTreeAction = { type: "TOGGLE_TREE", treeId: ?number, timestamp: number }; type SetTreeVisibilityAction = { type: "SET_TREE_VISIBILITY", @@ -146,6 +151,7 @@ export type SkeletonTracingAction = | SetNodePositionAction | CreateBranchPointAction | DeleteBranchPointAction + | DeleteBranchpointByIdAction | RequestDeleteBranchPointAction | CreateTreeAction | AddTreesAndGroupsAction @@ -186,6 +192,7 @@ export const SkeletonTracingSaveRelevantActions = [ "SET_NODE_RADIUS", "SET_NODE_POSITION", "CREATE_BRANCHPOINT", + "DELETE_BRANCHPOINT_BY_ID", "DELETE_BRANCHPOINT", "CREATE_TREE", "ADD_TREES_AND_GROUPS", @@ -314,6 +321,15 @@ export const deleteBranchPointAction = (): DeleteBranchPointAction => ({ type: "DELETE_BRANCHPOINT", }); +export const deleteBranchpointByIdAction = ( + nodeId: number, + treeId: number, +): DeleteBranchpointByIdAction => ({ + type: "DELETE_BRANCHPOINT_BY_ID", + nodeId, + treeId, +}); + export const requestDeleteBranchPointAction = (): RequestDeleteBranchPointAction => ({ type: "REQUEST_DELETE_BRANCHPOINT", }); diff --git a/frontend/javascripts/oxalis/model/reducers/skeletontracing_reducer.js b/frontend/javascripts/oxalis/model/reducers/skeletontracing_reducer.js index cb9ac489d0..39317b2fbe 100644 --- a/frontend/javascripts/oxalis/model/reducers/skeletontracing_reducer.js +++ b/frontend/javascripts/oxalis/model/reducers/skeletontracing_reducer.js @@ -609,6 +609,27 @@ function SkeletonTracingReducer(state: OxalisState, action: Action): OxalisState .getOrElse(state); } + case "DELETE_BRANCHPOINT_BY_ID": { + const { nodeId, treeId } = action; + return getTree(skeletonTracing, treeId) + .map(tree => + update(state, { + tracing: { + skeleton: { + trees: { + [treeId]: { + branchPoints: { + $set: tree.branchPoints.filter(bp => bp.nodeId !== nodeId), + }, + }, + }, + }, + }, + }), + ) + .getOrElse(state); + } + case "CREATE_TREE": { const { timestamp } = action; return createTree(state, timestamp) diff --git a/frontend/javascripts/oxalis/view/context_menu.js b/frontend/javascripts/oxalis/view/context_menu.js index 17f0b103a9..880e211c25 100644 --- a/frontend/javascripts/oxalis/view/context_menu.js +++ b/frontend/javascripts/oxalis/view/context_menu.js @@ -30,6 +30,8 @@ import { setActiveNodeAction, createTreeAction, setTreeVisibilityAction, + createBranchPointAction, + deleteBranchpointByIdAction, } from "oxalis/model/actions/skeletontracing_actions"; import { hasAgglomerateMapping, @@ -88,6 +90,8 @@ type DispatchProps = {| addNewBoundingBox: Vector3 => void, deleteBoundingBox: number => void, setActiveCell: (number, somePosition?: Vector3) => void, + createBranchPoint: (number, number) => void, + deleteBranchpointById: (number, number) => void, |}; type StateProps = {| @@ -242,6 +246,8 @@ function NodeContextMenuOptions({ deleteEdge, mergeTrees, deleteNode, + createBranchPoint, + deleteBranchpointById, setActiveNode, hideTree, useLegacyBindings, @@ -252,6 +258,7 @@ function NodeContextMenuOptions({ const { activeTreeId, trees, activeNodeId } = skeletonTracing; const clickedTree = findTreeByNodeId(trees, clickedNodeId).get(); const areInSameTree = activeTreeId === clickedTree.treeId; + const isBranchpoint = clickedTree.branchPoints.find(bp => bp.nodeId === clickedNodeId) != null; const isTheSameNode = activeNodeId === clickedNodeId; let areNodesConnected = false; if (areInSameTree && !isTheSameNode && activeNodeId != null) { @@ -295,6 +302,27 @@ function NodeContextMenuOptions({ > Delete this Node {activeNodeId === clickedNodeId ? shortcutBuilder(["Del"]) : null} + {isBranchpoint ? ( + + activeNodeId != null ? deleteBranchpointById(clickedNodeId, clickedTree.treeId) : null + } + > + Unmark as Branchpoint + + ) : ( + + activeNodeId != null ? createBranchPoint(clickedNodeId, clickedTree.treeId) : null + } + > + Mark as Branchpoint {activeNodeId === clickedNodeId ? shortcutBuilder(["B"]) : null} + + )} ) => ({ deleteNode(nodeId: number, treeId: number) { dispatch(deleteNodeAction(nodeId, treeId)); }, + createBranchPoint(nodeId: number, treeId: number) { + dispatch(createBranchPointAction(nodeId, treeId)); + }, + deleteBranchpointById(nodeId: number, treeId: number) { + dispatch(deleteBranchpointByIdAction(nodeId, treeId)); + }, setActiveNode(nodeId: number) { dispatch(setActiveNodeAction(nodeId)); }, diff --git a/frontend/javascripts/test/reducers/skeletontracing_reducer.spec.js b/frontend/javascripts/test/reducers/skeletontracing_reducer.spec.js index 9c444c3994..0ba29cdedf 100644 --- a/frontend/javascripts/test/reducers/skeletontracing_reducer.spec.js +++ b/frontend/javascripts/test/reducers/skeletontracing_reducer.spec.js @@ -632,6 +632,32 @@ test("SkeletonTracing should delete a branchpoint", t => { t.is(newState.tracing.skeleton.activeTreeId, 1); }); +test("SkeletonTracing should delete specific selected branchpoint", t => { + const createNodeAction = SkeletonTracingActions.createNodeAction( + position, + rotation, + viewport, + resolution, + ); + const createBranchPointAction = SkeletonTracingActions.createBranchPointAction(); + + // create one node and set it as branchpoint, create a second node and jump back to branchpoint + let newState = SkeletonTracingReducer(initialState, createNodeAction); + newState = SkeletonTracingReducer(newState, createBranchPointAction); + newState = SkeletonTracingReducer(newState, createNodeAction); + newState = SkeletonTracingReducer(newState, createBranchPointAction); + + const deleteBranchpointByIdAction = SkeletonTracingActions.deleteBranchpointByIdAction(1, 1); + newState = SkeletonTracingReducer(newState, deleteBranchpointByIdAction); + + t.not(newState, initialState); + t.is(newState.tracing.skeleton.trees[1].branchPoints.length, 1); + t.is(newState.tracing.skeleton.trees[1].branchPoints[0].nodeId, 2); + t.is(newState.tracing.skeleton.trees[1].nodes.size(), 2); + t.is(newState.tracing.skeleton.activeNodeId, 2); + t.is(newState.tracing.skeleton.activeTreeId, 1); +}); + test("SkeletonTracing should delete several branchpoints", t => { const createNodeAction = SkeletonTracingActions.createNodeAction( position,