Skip to content

feat: Implement 'find path' tool for spatially indexed skeleton - #221

Draft
afonsobspinto wants to merge 3 commits into
feature/edit-modefrom
feature/find-path
Draft

feat: Implement 'find path' tool for spatially indexed skeleton#221
afonsobspinto wants to merge 3 commits into
feature/edit-modefrom
feature/find-path

Conversation

@afonsobspinto

@afonsobspinto afonsobspinto commented Aug 12, 2026

Copy link
Copy Markdown
Member

feat: Implement Find Path tool for spatially indexed skeletons

Summary

Adds a datasource-owned Find Path tool for spatially indexed skeletons.

The tool lets users select two exact nodes in a visible skeleton and renders the
shortest route between them as a white annotation polyline. It is intended
primarily as a debugging aid for locating erroneous connections in merged
neurons.

The interaction follows Graphene's Find Path tool where applicable:

  1. Activate Find Path from the Skeleton tab.
  2. Control-left-click the source node.
  3. Control-left-click the target node. Shift may also be held.
  4. Press Enter or click Submit.
  5. Use Clear or delete an endpoint annotation to reset the selection.

Unlike Graphene, path calculation is performed locally using the complete
skeleton topology already cached in the client. Submitting Find Path does not
initiate a skeleton download.

Closes https://metacell.atlassian.net/browse/NGLANCERSU-12

User-facing behavior

  • Available for read-only spatial skeleton sources through
    SpatialSkeletonActions.inspect.
  • Accepts exact skeleton nodes only; edge-only picks are rejected.
  • Requires both endpoints to be:
    • Distinct nodes.
    • In the same skeleton segment.
    • In the same spatial skeleton datasource.
    • In a currently visible skeleton.
  • Ignores a third pick until an endpoint is deleted or the state is cleared.
  • Uses Submit or Enter to resolve the route.
  • Uses Clear to remove both endpoints and the route.
  • Renders:
    • A white source point.
    • A white target point.
    • A white source-to-target polyline.
  • If the visible skeleton has not finished loading into the client cache, the
    tool asks the user to wait and submit again.
  • Supports multiple spatial skeleton datasources without allowing node or
    segment IDs from different sources to alias.

Technical design

Datasource-owned state

Find Path state is stored with the skeleton datasource rather than directly on
SegmentationUserLayer.

A new generic SkeletonDataSourceState contains a SkeletonFindPathState and
serializes as:

{
  "findPath": {
    "source": {
      "segmentId": "42",
      "nodeId": "101",
      "position": [10, 20, 30]
    },
    "target": {
      "segmentId": "42",
      "nodeId": "205",
      "position": [40, 50, 60]
    },
    "result": [
      {
        "nodeId": "101",
        "position": [10, 20, 30]
      },
      {
        "nodeId": "150",
        "position": [25, 35, 45]
      },
      {
        "nodeId": "205",
        "position": [40, 50, 60]
      }
    ]
  }
}

Because this state is returned as DataSource.state, the existing datasource
machinery persists it under the corresponding source[].state entry:

{
  "source": [
    {
      "url": "catmaid://...",
      "state": {
        "findPath": {
          "...": "..."
        }
      }
    }
  ]
}

The datasource itself provides the source identity, so no datasource index or
subsource locator is serialized. Reordering datasource entries does not require
rebasing a source locator because the state moves with its owning
LayerDataSource.

Endpoint and result IDs are represented as uint64 values and serialized as
decimal strings. Restore validation requires:

  • Positive uint64 IDs.
  • Finite three-dimensional positions.
  • Distinct source and target nodes.
  • Source and target in the same segment.
  • A non-empty result whose first and last nodes match the saved endpoints.

Runtime annotation references and request generations are never serialized.

Generic Find Path state

SkeletonFindPathState is independent of spatial rendering and CATMAID-specific
APIs. It owns:

  • Source endpoint.
  • Target endpoint.
  • Ordered resolved route.
  • Change notifications.
  • Latest-request-wins generation bookkeeping.
  • Serialization and restore validation.

Changing either endpoint automatically invalidates the previous result. A
topology change clears only the resolved route and preserves the endpoints.

This separation is intended to allow a future regular-skeleton adapter to reuse
the same state and persistence lifecycle. Such an adapter could map a regular
skeleton object ID to segmentId and a stable vertex index to nodeId.

Local path calculation

getPathBetweenNodes is added to the existing spatial skeleton navigation graph
helpers.

The algorithm:

  • Treats parent/child links as undirected graph edges.
  • Uses iterative breadth-first search.
  • Returns a route with the fewest edges.
  • Includes both selected endpoints.
  • Uses a visited/predecessor map, making traversal safe for malformed or generic
    cycles.
  • Visits neighbors in ascending node-ID order for deterministic shortest-path
    tie-breaking.
  • Returns undefined for missing or disconnected nodes.

CATMAID skeletons are expected to be trees, but the implementation does not rely
on that restriction.

Only topology expressible through the current parentNodeId representation is
considered. Arbitrary additional graph edges are outside the scope of this
change.

Annotation adapter

SpatialSkeletonFindPathAnnotationController projects the generic state into a
source-local LocalAnnotationSource.

Each active spatial skeleton subsource receives a controller using that
subsource's coordinate transform. The controller creates and synchronizes:

  • find path source point.
  • find path target point.
  • find path result polyline.

All annotations are related to the selected skeleton segment through the
associated segments relationship.

The persisted Find Path state is canonical. Annotation IDs and
AnnotationReference instances remain controller-owned runtime objects.

Synchronization is bidirectional for deletion:

Annotation operation State operation
Delete source point Clear source and invalidate result
Delete target point Clear target and invalidate result
Delete result polyline Invalidate result only

If an annotation editing tool directly modifies one of these derived
annotations, the controller restores it from the canonical Find Path state.

Programmatic update/deletion guards prevent controller operations from being
interpreted as user edits and creating event feedback loops.

Disposing the controller removes its runtime annotations and invalidates pending
completion, but does not clear the datasource-owned persisted state.

Multiple spatial skeleton sources

A segmentation layer may contain multiple active spatial skeleton datasources.

Each active source receives a SpatialSkeletonFindPathContext containing:

  • Its SpatiallyIndexedSkeletonLayer.
  • Its LoadedDataSubsource.
  • Its SkeletonDataSourceState.
  • Its annotation controller.

The contexts are indexed by the concrete spatial skeleton base layer, allowing
mouse picks to identify the owning datasource through the picked render layer.

The UI intentionally supports one active route per segmentation layer:

  • If one datasource has non-empty restored state, that context becomes active.
  • Otherwise, the picked render layer is preferred.
  • Datasource order provides a deterministic fallback.
  • The first endpoint claims its datasource.
  • Claiming an empty datasource clears Find Path state in the other datasources.
  • A second endpoint from another datasource is rejected.
  • The user must Clear before switching datasources.
  • If manually supplied JSON contains multiple non-empty datasource states, the
    lowest datasource-index state is retained and the others are reset.

Disabling a spatial subsource disposes its annotations and cache scope but
retains its datasource state for reactivation.

Source-scoped complete-skeleton cache

Before this change, complete-skeleton inspection data was stored in layer-wide
maps keyed only by numeric IDs:

segmentNodes[segmentId]
nodesById[nodeId]
pendingFetches[segmentId]

That was only safe while a segmentation layer effectively contained one spatial
skeleton source. If two datasources both contained segment 42 or node 101, a
cache lookup or pending request from one datasource could be reused by the
other.

The cache is now partitioned into FullSkeletonCacheScope objects keyed by
SpatiallyIndexedSkeletonLayer:

SpatialSkeletonState
├── scope for source A
│   ├── segmentNodes
│   ├── nodesById
│   └── pendingSegmentNodeFetches
└── scope for source B
    ├── segmentNodes
    ├── nodesById
    └── pendingSegmentNodeFetches

The effective cache identities are now:

(skeletonLayer, segmentId)
(skeletonLayer, nodeId)

This provides:

  • Independent topology for sources with identical IDs.
  • Pending request deduplication only within one source.
  • Source-specific cache reads, eviction, and invalidation.
  • Abortion of pending requests when a spatial layer is disposed.
  • Detection of source replacement on an existing spatial layer.
  • Scope generations that prevent stale async completions from repopulating
    replaced or disposed caches.

Legacy unscoped mutation methods remain available where an ID resolves to
exactly one cache scope. Ambiguous unscoped operations do nothing rather than
modifying an arbitrary datasource.

Find Path does not call the full-skeleton fetch API directly. Visible-skeleton
rendering continues to populate these caches through the existing inspection
path, while Find Path submission only reads
getCachedSegmentNodes(segmentId, skeletonLayer).

Comparison with Graphene Find Path

The implementation intentionally follows Graphene's interaction and
datasource-owned persistence model, but the route providers and runtime
lifecycles differ.

Concern Graphene Spatial skeleton Find Path
Tool ID grapheneFindPath spatialSkeletonFindPath
State owner GrapheneState.findPathState SkeletonDataSourceState.findPathState
Endpoint identity Graphene root ID, supervoxel ID, arbitrary position Skeleton segment ID, exact node ID and position
Endpoint picking Arbitrary picked segmentation position Exact skeleton node only
Route provider Remote /graph/find_path service Local cached skeleton topology
Submission Performs an HTTP request Reads cached client data only
Result geometry Returned representative centroids plus endpoints Every exact route node, including endpoints
Algorithm Defined by the Graphene service Deterministic breadth-first search
Precision mode Supported and persisted Not required because skeleton node positions are exact
Cycle behavior Defined by the service Cycle-safe, deterministic fewest-edge route
Active sources One segmentation graph connection per layer Multiple spatial skeleton sources
Runtime owner GraphConnection One context/controller per spatial skeleton base
Annotation synchronization Embedded in GraphConnection Separate annotation controller
Annotation references Temporarily stored on Graphene endpoint selections Runtime-only in the controller
Topology changes Root replacement may update IDs and resubmit Preserve endpoints and invalidate the resolved result
Backend changes Uses the existing Graphene server endpoint No new server endpoint

Shared behavior

Both tools provide:

  • Control-left-click endpoint selection, optionally with Shift.
  • Source-then-target selection order.
  • Enter and Submit.
  • Clear.
  • Deletable endpoint annotation rows.
  • White endpoint and result annotations.
  • Datasource-owned persisted state.
  • Read-only inspection behavior.

Lifecycle differences

Graphene uses the segmentation graph connection infrastructure:

Graphene datasource
└── GrapheneState
    └── FindPathState

Active graph subsource
└── GraphConnection
    ├── annotation synchronization
    └── remote path request

A segmentation layer supports only one active segmentation graph, so
layer.graphConnection implicitly identifies the Find Path owner.

Spatial skeletons are activated through mesh/skeleton subsources and may have
several active sources:

Skeleton datasource
└── SkeletonDataSourceState
    └── SkeletonFindPathState

Active spatial subsource
└── SpatialSkeletonFindPathContext
    ├── SpatiallyIndexedSkeletonLayer
    ├── LoadedDataSubsource
    ├── annotation controller
    └── source-scoped cache

The explicit context map and active-route reconciliation are therefore required
for spatial skeletons but not for Graphene.

Invalidation and disposal behavior

  • Changing an endpoint clears the resolved route.
  • Clear removes both endpoints and the result.
  • Deleting the result annotation preserves the endpoints.
  • A skeleton node-data version change clears resolved routes for all loaded
    skeleton datasource states while preserving endpoints.
  • Repeated or stale request completions cannot restore invalidated state.
  • Disabling a subsource:
    • Removes its runtime annotations.
    • Aborts source-scoped pending cache work.
    • Removes its cache scope.
    • Retains its persisted datasource state.
  • Replacing a source invalidates the old cache generation and rejects stale
    completion from that source.

Implementation map

  • src/skeleton/find_path.ts

    • Generic Find Path state, datasource wrapper, validation and persistence.
  • src/skeleton/find_path.spec.ts

    • State mutation, JSON validation and stale-request tests.
  • src/skeleton/find_path_annotations.ts

    • Spatial annotation projection and synchronization.
  • src/skeleton/find_path_annotations.spec.ts

    • Annotation creation, deletion, update, transform and disposal tests.
  • src/skeleton/navigation_graph.ts

    • Deterministic cycle-safe shortest-path traversal.
  • src/skeleton/navigation_graph.spec.ts

    • Direction, branches, cycles, tie-breaking, disconnected graph and
      long-chain tests.
  • src/skeleton/spatial_skeleton_manager.ts

    • Source-scoped complete-skeleton caches and pending request lifecycle.
  • src/skeleton/spatial_skeleton_manager.spec.ts

    • Equal-ID source isolation, deduplication, invalidation, replacement and
      disposal tests.
  • src/datasource/catmaid/frontend.ts

    • CATMAID datasource-owned SkeletonDataSourceState construction and
      restoration.
  • src/layer/segmentation/index.ts

    • Spatial context creation, lifecycle, active-route selection and topology
      invalidation.
  • src/layer/segmentation/index.spec.ts

    • Persistence ownership, deterministic context selection and invalidation
      tests.
  • src/skeleton/frontend.ts

    • Passes concrete spatial skeleton identity through renderer cache operations.
  • src/ui/skeleton_edit_tools.ts

    • Find Path tool registration, interaction, validation, submission and status
      UI.
  • src/ui/skeleton_edit_tools.spec.ts

    • Pick validation, read-only behavior, cache-only routing, errors and
      multi-source tests.
  • src/ui/skeleton_edit_tools.css

    • Find Path status and endpoint-row styling.
  • src/ui/skeleton_tab.ts

    • Find Path button and source-scoped navigation cache access.
  • docs/user-guide/skeleton_editing.rst

    • User-facing operation and constraints.

Validation

Focused test coverage includes:

  • Generic state serialization and malformed-state rejection.
  • Direct, reverse, branched and disconnected paths.
  • Cyclic graphs and deterministic shortest-path tie-breaking.
  • Long iterative paths without recursive traversal.
  • Endpoint and result annotation synchronization.
  • Annotation deletion and direct-edit correction.
  • Read-only source availability.
  • Hidden, edge-only, repeated and cross-segment endpoint rejection.
  • Cross-datasource selection rejection and Clear-then-switch behavior.
  • Identical node and segment IDs in multiple datasources.
  • Same-source request deduplication.
  • Source replacement, cache disposal and stale async completion.
  • Deterministic restoration when multiple datasource states are non-empty.

Local validation performed:

  • Focused Vitest suites: 148 tests passed.
  • Production build: passed.
  • Oxlint and ESLint on the resolved UI/layer files: passed.
  • Full type-check remains blocked by pre-existing syntax errors in the vendored
    catmaid_src/static/libs/neuroglancer/tfjs-library.bundle.js.

Compatibility and non-goals

  • No Graphene behavior or persistence format is changed.
  • No CATMAID or other server API is changed.
  • No Python wrapper changes are required; datasource state already round-trips
    as JSON.
  • The tool does not modify skeleton topology.
  • Arbitrary points on skeleton edges are not supported.
  • Precision mode is not included.
  • Regular/precomputed skeleton picking and graph adapters remain future work.
  • Broader multi-source skeleton editing behavior is outside the scope of this
    PR.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant