Skip to content

Commit c240321

Browse files
Duplicate skeletons (#8662)
### Steps to test: - Open an annotation, add some trees (also within nested groups) - open their context menu -> duplicate tree - in the same group, a new tree with new nodes (same positions) should appear - all tree actions should be working on original and copied tree - check that NML import of skeletons is still working fine (as this uses the same action that was adjusted in this PR) ### Issues: - fixes #5291
1 parent c1225d9 commit c240321

File tree

5 files changed

+35
-5
lines changed

5 files changed

+35
-5
lines changed

CHANGELOG.unreleased.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ For upgrade instructions, please check the [migration guide](MIGRATIONS.released
1111
[Commits](https://github.com/scalableminds/webknossos/compare/25.06.1...HEAD)
1212

1313
### Added
14+
- Added the ability to duplicate trees in skeleton annotations. Users can create a copy of any tree (including all nodes, edges, and properties) via the context menu in the skeleton tab. [#8662](https://github.com/scalableminds/webknossos/pull/8662)
1415
- Meshes are now reloaded using their previous opacity value. [#8622](https://github.com/scalableminds/webknossos/pull/8622)
1516

1617
### Changed

frontend/javascripts/viewer/model/actions/skeletontracing_actions.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -327,12 +327,18 @@ export const addTreesAndGroupsAction = (
327327
trees: MutableTreeMap,
328328
treeGroups: Array<TreeGroup> | null | undefined,
329329
treeIdsCallback: ((ids: number[]) => void) | undefined = undefined,
330+
assignTreeToNewGroupId: boolean = true,
330331
) =>
332+
// If assignTreeToNewGroupId is false, the given group id of a tree will be kept. This is useful
333+
// when trees are duplicated, as the copied tree should be in the same group as the original tree.
334+
// If assignTreeToNewGroupId is true, the tree will be assigned to a new group id, as the original
335+
// group id might already be used by another group.
331336
({
332337
type: "ADD_TREES_AND_GROUPS",
333338
trees,
334339
treeGroups: treeGroups || [],
335340
treeIdsCallback,
341+
assignNewGroupId: assignTreeToNewGroupId,
336342
}) as const;
337343

338344
export const deleteTreeAction = (treeId?: number, suppressActivatingNextNode: boolean = false) =>

frontend/javascripts/viewer/model/reducers/skeletontracing_reducer.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -933,9 +933,14 @@ function SkeletonTracingReducer(state: WebknossosState, action: Action): Webknos
933933
}
934934

935935
case "ADD_TREES_AND_GROUPS": {
936-
const { trees, treeGroups } = action;
936+
const { trees, treeGroups, assignNewGroupId } = action;
937937
const treesWithNames = ensureTreeNames(state, trees);
938-
const treesResult = addTreesAndGroups(skeletonTracing, treesWithNames, treeGroups);
938+
const treesResult = addTreesAndGroups(
939+
skeletonTracing,
940+
treesWithNames,
941+
treeGroups,
942+
assignNewGroupId,
943+
);
939944
if (treesResult == null) {
940945
return state;
941946
}

frontend/javascripts/viewer/model/reducers/skeletontracing_reducer_helpers.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -557,6 +557,7 @@ export function addTreesAndGroups(
557557
skeletonTracing: SkeletonTracing,
558558
trees: MutableTreeMap,
559559
treeGroups: MutableTreeGroup[],
560+
assignNewGroupId: boolean = true,
560561
): [MutableTreeMap, TreeGroup[], number] | null {
561562
const hasInvalidTreeIds = trees
562563
.keys()
@@ -627,8 +628,11 @@ export function addTreesAndGroups(
627628
nodeId: idMap[bp.nodeId],
628629
}));
629630

630-
// Assign the new group id to the tree if the tree belongs to a group
631-
tree.groupId = tree.groupId != null ? groupIdMap[tree.groupId] : tree.groupId;
631+
// Assign the new group id to the tree if the tree belongs to a group that was newly added
632+
// or keep the old group id if the tree should be assigned to an existing group.
633+
if (tree.groupId != null && assignNewGroupId) {
634+
tree.groupId = groupIdMap[tree.groupId];
635+
}
632636
tree.treeId = newTreeId;
633637

634638
newTrees.mutableSet(newTreeId, tree);

frontend/javascripts/viewer/view/right-border-tabs/trees_tab/tree_hierarchy_renderers.tsx

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import FastTooltip from "components/fast_tooltip";
2424
import { formatLengthAsVx, formatNumberToLength } from "libs/format_utils";
2525
import messages from "messages";
2626
import {
27+
addTreesAndGroupsAction,
2728
deleteTreeAction,
2829
setActiveTreeAction,
2930
setActiveTreeGroupAction,
@@ -39,7 +40,7 @@ import {
3940
toggleInactiveTreesAction,
4041
} from "viewer/model/actions/skeletontracing_actions";
4142
import { getMaximumGroupId } from "viewer/model/reducers/skeletontracing_reducer_helpers";
42-
import type { Tree, TreeGroup, TreeMap } from "viewer/model/types/tree_types";
43+
import { type Tree, type TreeGroup, TreeMap } from "viewer/model/types/tree_types";
4344
import { Store, api } from "viewer/singletons";
4445
import EditableTextLabel from "viewer/view/components/editable_text_label";
4546
import {
@@ -147,6 +148,19 @@ const createMenuForTree = (tree: Tree, props: Props, hideContextMenu: () => void
147148
icon: <i className="fas fa-adjust" />,
148149
label: "Shuffle Tree Color",
149150
},
151+
{
152+
key: "duplicateTree",
153+
onClick: () => {
154+
const copyOfTree = { ..._.cloneDeep(tree), name: `${tree.name} (copy)` };
155+
const treeMap = new TreeMap([[tree.treeId, copyOfTree]]);
156+
Store.dispatch(addTreesAndGroupsAction(treeMap, null, undefined, false));
157+
hideContextMenu();
158+
},
159+
title: "Duplicate Tree",
160+
disabled: isEditingDisabled,
161+
icon: <i className="fas fa-clone" />,
162+
label: "Duplicate Tree",
163+
},
150164
{
151165
key: "deleteTree",
152166
onClick: () => {

0 commit comments

Comments
 (0)