Skip to content

Commit 621da28

Browse files
authored
Merge pull request #213 from MetaCell/feature/walk-past-branch-points
feat: walk past branch points and break next branch ties randomly
2 parents 5cf1cd6 + f0ec5e8 commit 621da28

2 files changed

Lines changed: 63 additions & 8 deletions

File tree

src/skeleton/navigation_graph.spec.ts

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,11 +65,41 @@ describe("skeleton/navigation", () => {
6565
it("finds the skeleton root and branch starts", () => {
6666
expect(getSkeletonRootNode(graph).nodeId).toBe(1);
6767
expect(getBranchStart(graph, 6).nodeId).toBe(3);
68-
expect(getBranchStart(graph, 3).nodeId).toBe(3);
69-
expect(getBranchStart(graph, 2).nodeId).toBe(2);
68+
expect(getBranchStart(graph, 3).nodeId).toBe(1);
69+
expect(getBranchStart(graph, 2).nodeId).toBe(1);
7070
expect(getBranchStart(graph, 1).nodeId).toBe(1);
7171
});
7272

73+
it("walks a degenerate, unbranched skeleton to the root", () => {
74+
const chainGraph = buildSpatiallyIndexedSkeletonNavigationGraph([
75+
makeNode(1, undefined),
76+
makeNode(2, 1),
77+
makeNode(3, 2),
78+
makeNode(4, 3),
79+
]);
80+
81+
expect(getBranchStart(chainGraph, 4).nodeId).toBe(1);
82+
expect(getBranchStart(chainGraph, 3).nodeId).toBe(1);
83+
expect(getBranchStart(chainGraph, 2).nodeId).toBe(1);
84+
expect(getBranchStart(chainGraph, 1).nodeId).toBe(1);
85+
});
86+
87+
it("walks past a branch point to the previous branch point or root", () => {
88+
const nestedForkGraph = buildSpatiallyIndexedSkeletonNavigationGraph([
89+
makeNode(1, undefined),
90+
makeNode(2, 1),
91+
makeNode(3, 2),
92+
makeNode(4, 2),
93+
makeNode(5, 3),
94+
makeNode(6, 3),
95+
]);
96+
97+
expect(getBranchStart(nestedForkGraph, 6).nodeId).toBe(3);
98+
expect(getBranchStart(nestedForkGraph, 3).nodeId).toBe(2);
99+
expect(getBranchStart(nestedForkGraph, 2).nodeId).toBe(1);
100+
expect(getBranchStart(nestedForkGraph, 1).nodeId).toBe(1);
101+
});
102+
73103
it("prefers a downstream branch over a leaf for branch-end navigation", () => {
74104
const preferenceGraph = buildSpatiallyIndexedSkeletonNavigationGraph([
75105
makeNode(1, undefined),
@@ -85,6 +115,21 @@ describe("skeleton/navigation", () => {
85115
expect(getBranchEnd(preferenceGraph, 2).nodeId).toBe(2);
86116
});
87117

118+
it("randomly resolves ties between equally preferred branch-end candidates", () => {
119+
const tiedGraph = buildSpatiallyIndexedSkeletonNavigationGraph([
120+
makeNode(1, undefined),
121+
makeNode(2, 1),
122+
makeNode(3, 2),
123+
makeNode(4, 2),
124+
makeNode(5, 1),
125+
makeNode(6, 5),
126+
makeNode(7, 5),
127+
]);
128+
129+
expect(getBranchEnd(tiedGraph, 1, { random: () => 0 }).nodeId).toBe(2);
130+
expect(getBranchEnd(tiedGraph, 1, { random: () => 0.99 }).nodeId).toBe(5);
131+
});
132+
88133
it("orders flat-list rows in leaf-first pre-order", () => {
89134
const listGraph = buildSpatiallyIndexedSkeletonNavigationGraph([
90135
makeNode(1, undefined),

src/skeleton/navigation_graph.ts

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -506,7 +506,7 @@ export function getBranchStart(
506506
while (true) {
507507
const parentNodeId = getParentNodeId(graph, currentNodeId);
508508
if (parentNodeId === undefined || visited.has(parentNodeId)) {
509-
return getNodeTarget(graph, nodeId);
509+
return getNodeTarget(graph, currentNodeId);
510510
}
511511
currentNodeId = parentNodeId;
512512
visited.add(currentNodeId);
@@ -519,17 +519,27 @@ export function getBranchStart(
519519
export function getBranchEnd(
520520
graph: SpatiallyIndexedSkeletonNavigationGraph,
521521
nodeId: number,
522+
options: { random?: () => number } = {},
522523
) {
523524
getNodeOrThrow(graph, nodeId);
524525
const branchEndNodeIds = getBranchEndNodeIds(graph, nodeId);
525526
if (branchEndNodeIds.length === 0) {
526527
return getNodeTarget(graph, nodeId);
527528
}
528-
const preferredBranchEndNodeId =
529-
branchEndNodeIds.find(
530-
(branchEndNodeId) => getChildNodeIds(graph, branchEndNodeId).length > 1,
531-
) ?? branchEndNodeIds[0];
532-
return getNodeTarget(graph, preferredBranchEndNodeId);
529+
const forkNodeIds = branchEndNodeIds.filter(
530+
(branchEndNodeId) => getChildNodeIds(graph, branchEndNodeId).length > 1,
531+
);
532+
const preferredNodeIds =
533+
forkNodeIds.length > 0 ? forkNodeIds : branchEndNodeIds;
534+
const { random = Math.random } = options;
535+
const randomValue = random();
536+
const index = Number.isFinite(randomValue)
537+
? Math.min(
538+
preferredNodeIds.length - 1,
539+
Math.max(0, Math.floor(randomValue * preferredNodeIds.length)),
540+
)
541+
: 0;
542+
return getNodeTarget(graph, preferredNodeIds[index]);
533543
}
534544

535545
export function getParentNode(

0 commit comments

Comments
 (0)