From fc75c5e690fbeeb0952939d8b35ca52695ba2030 Mon Sep 17 00:00:00 2001 From: Tom Herold Date: Wed, 10 Sep 2025 15:50:33 +0200 Subject: [PATCH 01/19] Add new camera perspectives for animations (#8909) This PR adds two new camera perspectives for animation jobs: - static camera looking at XY plane (top-down view on the stack) - static, isometric camera looking a the corner between XY, XZ, YZ plane - CI should be enough for the frontend, backend changes - Otherwise see worker PR - [ ] Merge related [worker PR first](https://github.com/scalableminds/voxelytics/pull/4279) - None ------ (Please delete unneeded items, merge only when none are left open) - [ ] Added changelog entry (create a `$PR_NUMBER.md` file in `unreleased_changes` or use `./tools/create-changelog-entry.py`) - [ ] Added migration guide entry if applicable (edit the same file as for the changelog) - [x] Updated [documentation](../blob/master/docs) if applicable - [ ] Adapted [wk-libs python client](https://github.com/scalableminds/webknossos-libs/tree/master/webknossos/webknossos/client) if relevant API parts change - [ ] Removed dev-only changes like prints and application.conf edits - [ ] Considered [common edge cases](../blob/master/.github/common_edge_cases.md) - [ ] Needs datastore update after deployment --- app/controllers/JobController.scala | 2 +- docs/automation/animations.md | 2 +- frontend/javascripts/admin/rest_api.ts | 7 ++++--- frontend/javascripts/types/api_types.ts | 2 ++ .../viewer/model/sagas/meshes/ad_hoc_mesh_saga.ts | 7 +++++++ .../viewer/view/action-bar/create_animation_modal.tsx | 6 ++++++ unreleased_changes/8909.md | 2 ++ 7 files changed, 23 insertions(+), 5 deletions(-) create mode 100644 unreleased_changes/8909.md diff --git a/app/controllers/JobController.scala b/app/controllers/JobController.scala index a0d871364d4..e485507f4ba 100644 --- a/app/controllers/JobController.scala +++ b/app/controllers/JobController.scala @@ -31,7 +31,7 @@ object MovieResolutionSetting extends ExtendedEnumeration { } object CameraPositionSetting extends ExtendedEnumeration { - val MOVING, STATIC_XZ, STATIC_YZ = Value + val MOVING, STATIC_ISOMETRIC, STATIC_XY, STATIC_XZ, STATIC_YZ = Value } case class AnimationJobOptions( diff --git a/docs/automation/animations.md b/docs/automation/animations.md index 6f8410b9d26..937327b7fbd 100644 --- a/docs/automation/animations.md +++ b/docs/automation/animations.md @@ -12,7 +12,7 @@ Creating an animation is easy: 2. Optionally, load some [3D meshes](../meshes/index.md) for any segments that you wish to highlight. 3. For larger datasets, use the bounding box tool to create a bounding box around your area of interest. Smaller datasets can be used in their entirety. 4. From the `Menu` dropdown in navbar at the top of the screen, select "Create Animation". -5. Configure the animation options as desired, i.e. camera movement or resolution. +5. Configure the animation options as desired, i.e. camera movement and positioning or video resolution. 6. Click the `Start animation` button to launch the animation creation. diff --git a/frontend/javascripts/admin/rest_api.ts b/frontend/javascripts/admin/rest_api.ts index 41eec6f5957..6a304f16c93 100644 --- a/frontend/javascripts/admin/rest_api.ts +++ b/frontend/javascripts/admin/rest_api.ts @@ -1940,23 +1940,24 @@ export function computeAdHocMesh( } export function getBucketPositionsForAdHocMesh( - tracingStoreUrl: string, - tracingId: string, + requestUrl: string, segmentId: number, cubeSize: Vector3, mag: Vector3, additionalCoordinates: AdditionalCoordinate[] | null | undefined, + mappingName: string | null | undefined, ): Promise { return doWithToken(async (token) => { const params = new URLSearchParams(); params.set("token", token); const positions = await Request.sendJSONReceiveJSON( - `${tracingStoreUrl}/tracings/volume/${tracingId}/segmentIndex/${segmentId}?${params}`, + `${requestUrl}/segmentIndex/${segmentId}?${params}`, { data: { cubeSize, mag, additionalCoordinates, + mappingName, }, method: "POST", }, diff --git a/frontend/javascripts/types/api_types.ts b/frontend/javascripts/types/api_types.ts index 3e1ff1bdfa0..d72c71efe24 100644 --- a/frontend/javascripts/types/api_types.ts +++ b/frontend/javascripts/types/api_types.ts @@ -1280,6 +1280,8 @@ export type FolderUpdater = { export enum CAMERA_POSITIONS { MOVING = "MOVING", + STATIC_ISOMETRIC = "STATIC_ISOMETRIC", + STATIC_XY = "STATIC_XY", STATIC_XZ = "STATIC_XZ", STATIC_YZ = "STATIC_YZ", } diff --git a/frontend/javascripts/viewer/model/sagas/meshes/ad_hoc_mesh_saga.ts b/frontend/javascripts/viewer/model/sagas/meshes/ad_hoc_mesh_saga.ts index 3a0ec587430..f2c742b164c 100644 --- a/frontend/javascripts/viewer/model/sagas/meshes/ad_hoc_mesh_saga.ts +++ b/frontend/javascripts/viewer/model/sagas/meshes/ad_hoc_mesh_saga.ts @@ -341,6 +341,13 @@ function* loadFullAdHocMesh( volumeTracing?.hasSegmentIndex && !volumeTracing.hasEditableMapping && visibleSegmentationLayer?.tracingId != null; + + // TODO: also use segment index if there is no volume tracing, but a static segmentation layer that has a segment index file. + // build requestUrl to go to the dataset’s datastore instead. + // use hasSegmentIndexInDataStore from rest_api. (should probably be cached) + // let’s skip the volumeTracing+editableMapping case for this issue, + // even though it could also be added by combining the segment index entries for the oversegment ids + let positionsToRequest = usePositionsFromSegmentIndex ? yield* getChunkPositionsFromSegmentIndex( tracingStoreHost, diff --git a/frontend/javascripts/viewer/view/action-bar/create_animation_modal.tsx b/frontend/javascripts/viewer/view/action-bar/create_animation_modal.tsx index 5b48f2c0706..33713faa7c3 100644 --- a/frontend/javascripts/viewer/view/action-bar/create_animation_modal.tsx +++ b/frontend/javascripts/viewer/view/action-bar/create_animation_modal.tsx @@ -406,12 +406,18 @@ function CreateAnimationModal(props: Props) { Camera circling around the dataset + + Static camera looking at XY-viewport{" "} + Static camera looking at XZ-viewport{" "} Static camera looking at YZ-viewport{" "} + + Static camera with an isometric perspective looking at all 3 viewports{" "} + diff --git a/unreleased_changes/8909.md b/unreleased_changes/8909.md new file mode 100644 index 00000000000..b8897649d37 --- /dev/null +++ b/unreleased_changes/8909.md @@ -0,0 +1,2 @@ +### Added +- Added new camera positioning option for animations. From c7127bab89f9a5eee37a44f07ca886e32726c967 Mon Sep 17 00:00:00 2001 From: Charlie Meister Date: Fri, 26 Sep 2025 16:34:01 +0200 Subject: [PATCH 02/19] WIP: use segment index for static segmentation layers with segment index --- .../model/sagas/meshes/ad_hoc_mesh_saga.ts | 38 +++++++++++++++---- 1 file changed, 31 insertions(+), 7 deletions(-) diff --git a/frontend/javascripts/viewer/model/sagas/meshes/ad_hoc_mesh_saga.ts b/frontend/javascripts/viewer/model/sagas/meshes/ad_hoc_mesh_saga.ts index 242ebd6e4b3..e9babfdb61c 100644 --- a/frontend/javascripts/viewer/model/sagas/meshes/ad_hoc_mesh_saga.ts +++ b/frontend/javascripts/viewer/model/sagas/meshes/ad_hoc_mesh_saga.ts @@ -1,6 +1,7 @@ import { computeAdHocMesh, getBucketPositionsForAdHocMesh, + hasSegmentIndexInDataStore, sendAnalyticsEvent, } from "admin/rest_api"; import ThreeDMap from "libs/ThreeDMap"; @@ -321,6 +322,8 @@ function* loadFullAdHocMesh( const cubeSize = marchingCubeSizeInTargetMag(); const tracingStoreHost = yield* select((state) => state.annotation.tracingStore.url); + const datasetId = yield* select((state) => state.dataset.id); + const datastoreUrl = yield* select((state) => state.dataset.dataStore.url); const mag = magInfo.getMagByIndexOrThrow(zoomStep); const volumeTracing = yield* select((state) => getActiveSegmentationTracing(state)); @@ -335,13 +338,34 @@ function* loadFullAdHocMesh( useDataStore = false; } + const dataset = yield* select((state) => state.dataset); + + let isStaticSegmentationLayerWithSegmentIndex = false; + + if (visibleSegmentationLayer != null && volumeTracing == null) { + isStaticSegmentationLayerWithSegmentIndex = yield* call( + hasSegmentIndexInDataStore, + dataset.dataStore.url, + dataset.id, + visibleSegmentationLayer?.name, + ); + } // Segment stats can only be used for volume tracings that have a segment index // and that don't have editable mappings. - const usePositionsFromSegmentIndex = + const usePositionsFromSegmentIndexForVolumeTracing = volumeTracing?.hasSegmentIndex && !volumeTracing.hasEditableMapping && visibleSegmentationLayer?.tracingId != null; + const usePositionsFromSegmentIndex = + usePositionsFromSegmentIndexForVolumeTracing || isStaticSegmentationLayerWithSegmentIndex; + + const dataStoreUrl = `${datastoreUrl}/data/datasets/${datasetId}/layers/${ + layer.fallbackLayer != null ? layer.fallbackLayer : layer.name + }`; + const tracingStoreUrl = `${tracingStoreHost}/tracings/volume/${layer.name}`; + const requestUrl = useDataStore ? dataStoreUrl : tracingStoreUrl; + // TODO: also use segment index if there is no volume tracing, but a static segmentation layer that has a segment index file. // build requestUrl to go to the dataset’s datastore instead. // use hasSegmentIndexInDataStore from rest_api. (should probably be cached) @@ -350,13 +374,13 @@ function* loadFullAdHocMesh( let positionsToRequest = usePositionsFromSegmentIndex ? yield* getChunkPositionsFromSegmentIndex( - tracingStoreHost, - layer, + requestUrl, segmentId, cubeSize, mag, clippedPosition, additionalCoordinates, + mappingName, ) : [clippedPosition]; @@ -397,22 +421,22 @@ function* loadFullAdHocMesh( } function* getChunkPositionsFromSegmentIndex( - tracingStoreHost: string, - layer: DataLayer, + requestUrl: string, segmentId: number, cubeSize: Vector3, mag: Vector3, clippedPosition: Vector3, additionalCoordinates: AdditionalCoordinate[] | null | undefined, + mappingName: string | null | undefined, ) { const targetMagPositions = yield* call( getBucketPositionsForAdHocMesh, - tracingStoreHost, - layer.name, + requestUrl, segmentId, cubeSize, mag, additionalCoordinates, + mappingName, ); const mag1Positions = targetMagPositions.map((pos) => V3.scale3(pos, mag)); return sortByDistanceTo(mag1Positions, clippedPosition) as Vector3[]; From bc96b4350773bf296cf75f48c2dbcc3fb0c6fa76 Mon Sep 17 00:00:00 2001 From: Charlie Meister Date: Fri, 26 Sep 2025 17:29:01 +0200 Subject: [PATCH 03/19] memoize hasSegmentIndexInDataStore --- frontend/javascripts/admin/rest_api.ts | 2 ++ .../model/sagas/meshes/ad_hoc_mesh_saga.ts | 17 +++++------------ 2 files changed, 7 insertions(+), 12 deletions(-) diff --git a/frontend/javascripts/admin/rest_api.ts b/frontend/javascripts/admin/rest_api.ts index f70b5a5ce56..6fcba49685d 100644 --- a/frontend/javascripts/admin/rest_api.ts +++ b/frontend/javascripts/admin/rest_api.ts @@ -855,6 +855,8 @@ export function hasSegmentIndexInDataStore( ); } +export const hasSegmentIndexInDataStoreCached = _.memoize(hasSegmentIndexInDataStore); + export function getSegmentVolumes( requestUrl: string, mag: Vector3, diff --git a/frontend/javascripts/viewer/model/sagas/meshes/ad_hoc_mesh_saga.ts b/frontend/javascripts/viewer/model/sagas/meshes/ad_hoc_mesh_saga.ts index e9babfdb61c..2965223810e 100644 --- a/frontend/javascripts/viewer/model/sagas/meshes/ad_hoc_mesh_saga.ts +++ b/frontend/javascripts/viewer/model/sagas/meshes/ad_hoc_mesh_saga.ts @@ -1,7 +1,7 @@ import { computeAdHocMesh, getBucketPositionsForAdHocMesh, - hasSegmentIndexInDataStore, + hasSegmentIndexInDataStoreCached, sendAnalyticsEvent, } from "admin/rest_api"; import ThreeDMap from "libs/ThreeDMap"; @@ -322,8 +322,9 @@ function* loadFullAdHocMesh( const cubeSize = marchingCubeSizeInTargetMag(); const tracingStoreHost = yield* select((state) => state.annotation.tracingStore.url); - const datasetId = yield* select((state) => state.dataset.id); - const datastoreUrl = yield* select((state) => state.dataset.dataStore.url); + const dataset = yield* select((state) => state.dataset); + const datasetId = dataset.id; + const datastoreUrl = dataset.dataStore.url; const mag = magInfo.getMagByIndexOrThrow(zoomStep); const volumeTracing = yield* select((state) => getActiveSegmentationTracing(state)); @@ -338,13 +339,11 @@ function* loadFullAdHocMesh( useDataStore = false; } - const dataset = yield* select((state) => state.dataset); - let isStaticSegmentationLayerWithSegmentIndex = false; if (visibleSegmentationLayer != null && volumeTracing == null) { isStaticSegmentationLayerWithSegmentIndex = yield* call( - hasSegmentIndexInDataStore, + hasSegmentIndexInDataStoreCached, dataset.dataStore.url, dataset.id, visibleSegmentationLayer?.name, @@ -366,12 +365,6 @@ function* loadFullAdHocMesh( const tracingStoreUrl = `${tracingStoreHost}/tracings/volume/${layer.name}`; const requestUrl = useDataStore ? dataStoreUrl : tracingStoreUrl; - // TODO: also use segment index if there is no volume tracing, but a static segmentation layer that has a segment index file. - // build requestUrl to go to the dataset’s datastore instead. - // use hasSegmentIndexInDataStore from rest_api. (should probably be cached) - // let’s skip the volumeTracing+editableMapping case for this issue, - // even though it could also be added by combining the segment index entries for the oversegment ids - let positionsToRequest = usePositionsFromSegmentIndex ? yield* getChunkPositionsFromSegmentIndex( requestUrl, From 46fa4df9ff52797e7de0cd1512d42a13b23d7bf0 Mon Sep 17 00:00:00 2001 From: Charlie Meister Date: Fri, 26 Sep 2025 19:17:56 +0200 Subject: [PATCH 04/19] apply coderabbit review --- frontend/javascripts/admin/rest_api.ts | 4 +++- .../viewer/model/sagas/meshes/ad_hoc_mesh_saga.ts | 11 +++++------ 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/frontend/javascripts/admin/rest_api.ts b/frontend/javascripts/admin/rest_api.ts index 6fcba49685d..ff90697c942 100644 --- a/frontend/javascripts/admin/rest_api.ts +++ b/frontend/javascripts/admin/rest_api.ts @@ -855,7 +855,9 @@ export function hasSegmentIndexInDataStore( ); } -export const hasSegmentIndexInDataStoreCached = _.memoize(hasSegmentIndexInDataStore); +export const hasSegmentIndexInDataStoreCached = _.memoize(hasSegmentIndexInDataStore, (...args) => + args.join("::"), +); export function getSegmentVolumes( requestUrl: string, diff --git a/frontend/javascripts/viewer/model/sagas/meshes/ad_hoc_mesh_saga.ts b/frontend/javascripts/viewer/model/sagas/meshes/ad_hoc_mesh_saga.ts index 2965223810e..58623bb7bd8 100644 --- a/frontend/javascripts/viewer/model/sagas/meshes/ad_hoc_mesh_saga.ts +++ b/frontend/javascripts/viewer/model/sagas/meshes/ad_hoc_mesh_saga.ts @@ -324,7 +324,7 @@ function* loadFullAdHocMesh( const tracingStoreHost = yield* select((state) => state.annotation.tracingStore.url); const dataset = yield* select((state) => state.dataset); const datasetId = dataset.id; - const datastoreUrl = dataset.dataStore.url; + const dataStoreHost = dataset.dataStore.url; const mag = magInfo.getMagByIndexOrThrow(zoomStep); const volumeTracing = yield* select((state) => getActiveSegmentationTracing(state)); @@ -339,14 +339,15 @@ function* loadFullAdHocMesh( useDataStore = false; } + const maybeFallbackLayerName = layer.fallbackLayer != null ? layer.fallbackLayer : layer.name; let isStaticSegmentationLayerWithSegmentIndex = false; - if (visibleSegmentationLayer != null && volumeTracing == null) { + if (volumeTracing == null) { isStaticSegmentationLayerWithSegmentIndex = yield* call( hasSegmentIndexInDataStoreCached, dataset.dataStore.url, dataset.id, - visibleSegmentationLayer?.name, + maybeFallbackLayerName, ); } // Segment stats can only be used for volume tracings that have a segment index @@ -359,9 +360,7 @@ function* loadFullAdHocMesh( const usePositionsFromSegmentIndex = usePositionsFromSegmentIndexForVolumeTracing || isStaticSegmentationLayerWithSegmentIndex; - const dataStoreUrl = `${datastoreUrl}/data/datasets/${datasetId}/layers/${ - layer.fallbackLayer != null ? layer.fallbackLayer : layer.name - }`; + const dataStoreUrl = `${dataStoreHost}/data/datasets/${datasetId}/layers/${maybeFallbackLayerName}`; const tracingStoreUrl = `${tracingStoreHost}/tracings/volume/${layer.name}`; const requestUrl = useDataStore ? dataStoreUrl : tracingStoreUrl; From 5e58f24b36fa2b9d79f37f827a1c74ed51ae6497 Mon Sep 17 00:00:00 2001 From: Charlie Meister Date: Fri, 26 Sep 2025 19:39:43 +0200 Subject: [PATCH 05/19] changelog --- unreleased_changes/8922.md | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 unreleased_changes/8922.md diff --git a/unreleased_changes/8922.md b/unreleased_changes/8922.md new file mode 100644 index 00000000000..648eba1c141 --- /dev/null +++ b/unreleased_changes/8922.md @@ -0,0 +1,2 @@ +### Added +- The segment index file is now used while rendering ad-hoc meshes for static segmentation layers, e.g. when viewing a dataset. From 2cfdbb398ab87470cb3a96122863c69dd5cf4ed1 Mon Sep 17 00:00:00 2001 From: Charlie Meister Date: Mon, 29 Sep 2025 11:03:42 +0200 Subject: [PATCH 06/19] use layer.name for static segmentation layers --- .../viewer/model/sagas/meshes/ad_hoc_mesh_saga.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/frontend/javascripts/viewer/model/sagas/meshes/ad_hoc_mesh_saga.ts b/frontend/javascripts/viewer/model/sagas/meshes/ad_hoc_mesh_saga.ts index 58623bb7bd8..41099830316 100644 --- a/frontend/javascripts/viewer/model/sagas/meshes/ad_hoc_mesh_saga.ts +++ b/frontend/javascripts/viewer/model/sagas/meshes/ad_hoc_mesh_saga.ts @@ -339,7 +339,6 @@ function* loadFullAdHocMesh( useDataStore = false; } - const maybeFallbackLayerName = layer.fallbackLayer != null ? layer.fallbackLayer : layer.name; let isStaticSegmentationLayerWithSegmentIndex = false; if (volumeTracing == null) { @@ -347,7 +346,7 @@ function* loadFullAdHocMesh( hasSegmentIndexInDataStoreCached, dataset.dataStore.url, dataset.id, - maybeFallbackLayerName, + layer.name, ); } // Segment stats can only be used for volume tracings that have a segment index @@ -360,7 +359,7 @@ function* loadFullAdHocMesh( const usePositionsFromSegmentIndex = usePositionsFromSegmentIndexForVolumeTracing || isStaticSegmentationLayerWithSegmentIndex; - const dataStoreUrl = `${dataStoreHost}/data/datasets/${datasetId}/layers/${maybeFallbackLayerName}`; + const dataStoreUrl = `${dataStoreHost}/data/datasets/${datasetId}/layers/${layer.name}`; const tracingStoreUrl = `${tracingStoreHost}/tracings/volume/${layer.name}`; const requestUrl = useDataStore ? dataStoreUrl : tracingStoreUrl; From a55cfb8cbaf6580d805d4bb4f9ae4eda3444e257 Mon Sep 17 00:00:00 2001 From: Florian M Date: Mon, 29 Sep 2025 14:38:23 +0200 Subject: [PATCH 07/19] Also use segment index in datastore fullMesh.stl route, if available --- .../controllers/BinaryDataController.scala | 2 +- .../datastore/models/DataRequests.scala | 2 +- .../services/mesh/DSFullMeshService.scala | 83 ++++++++++++++++--- .../EditableMappingService.scala | 2 +- .../volume/VolumeTracingService.scala | 2 +- 5 files changed, 76 insertions(+), 15 deletions(-) diff --git a/webknossos-datastore/app/com/scalableminds/webknossos/datastore/controllers/BinaryDataController.scala b/webknossos-datastore/app/com/scalableminds/webknossos/datastore/controllers/BinaryDataController.scala index 44b97928a68..5e53fbf3a2d 100644 --- a/webknossos-datastore/app/com/scalableminds/webknossos/datastore/controllers/BinaryDataController.scala +++ b/webknossos-datastore/app/com/scalableminds/webknossos/datastore/controllers/BinaryDataController.scala @@ -224,7 +224,7 @@ class BinaryDataController @Inject()( adHocMeshRequest = AdHocMeshRequest( Some(dataSource.id), segmentationLayer, - request.body.cuboid(dataLayer), + request.body.cuboid, request.body.segmentId, request.body.voxelSizeFactorInUnit, tokenContextForRequest(request), diff --git a/webknossos-datastore/app/com/scalableminds/webknossos/datastore/models/DataRequests.scala b/webknossos-datastore/app/com/scalableminds/webknossos/datastore/models/DataRequests.scala index d1fb0d6e537..403a1fe35b9 100644 --- a/webknossos-datastore/app/com/scalableminds/webknossos/datastore/models/DataRequests.scala +++ b/webknossos-datastore/app/com/scalableminds/webknossos/datastore/models/DataRequests.scala @@ -56,7 +56,7 @@ case class WebknossosAdHocMeshRequest( additionalCoordinates: Option[Seq[AdditionalCoordinate]] = None, findNeighbors: Boolean = true ) { - def cuboid(dataLayer: DataLayer): Cuboid = + def cuboid: Cuboid = Cuboid(VoxelPosition(position.x, position.y, position.z, mag), cubeSize.x, cubeSize.y, cubeSize.z) } diff --git a/webknossos-datastore/app/com/scalableminds/webknossos/datastore/services/mesh/DSFullMeshService.scala b/webknossos-datastore/app/com/scalableminds/webknossos/datastore/services/mesh/DSFullMeshService.scala index 25033128191..c537393df65 100644 --- a/webknossos-datastore/app/com/scalableminds/webknossos/datastore/services/mesh/DSFullMeshService.scala +++ b/webknossos-datastore/app/com/scalableminds/webknossos/datastore/services/mesh/DSFullMeshService.scala @@ -13,6 +13,7 @@ import com.scalableminds.webknossos.datastore.services._ import com.typesafe.scalalogging.LazyLogging import com.scalableminds.util.tools.Box.tryo import com.scalableminds.webknossos.datastore.services.mapping.MappingService +import com.scalableminds.webknossos.datastore.services.segmentindex.SegmentIndexFileService import play.api.i18n.MessagesProvider import play.api.libs.json.{Json, OFormat} @@ -40,6 +41,7 @@ class DSFullMeshService @Inject()(meshFileService: MeshFileService, val dsRemoteTracingstoreClient: DSRemoteTracingstoreClient, mappingService: MappingService, config: DataStoreConfig, + segmentIndexFileService: SegmentIndexFileService, adHocMeshServiceHolder: AdHocMeshServiceHolder) extends LazyLogging with FullMeshHelper @@ -66,26 +68,85 @@ class DSFullMeshService @Inject()(meshFileService: MeshFileService, fullMeshRequest: FullMeshRequest)(implicit ec: ExecutionContext, tc: TokenContext): Fox[Array[Byte]] = for { mag <- fullMeshRequest.mag.toFox ?~> "mag.neededForAdHoc" - seedPosition <- fullMeshRequest.seedPosition.toFox ?~> "seedPosition.neededForAdHoc" segmentationLayer <- tryo(dataLayer.asInstanceOf[SegmentationLayer]).toFox ?~> "dataLayer.mustBeSegmentation" + hasSegmentIndexFile = segmentationLayer.attachments.flatMap(_.segmentIndex).isDefined before = Instant.now - verticesForChunks <- getAllAdHocChunks(dataSource, - segmentationLayer, - fullMeshRequest, - VoxelPosition(seedPosition.x, seedPosition.y, seedPosition.z, mag), - adHocChunkSize) + verticesForChunks <- if (hasSegmentIndexFile) + getAllAdHocChunksWithSegmentIndex(dataSource, segmentationLayer, fullMeshRequest, mag) + else { + for { + seedPosition <- fullMeshRequest.seedPosition.toFox ?~> "seedPosition.neededForAdHocWithoutSegmentIndex" + chunks <- getAllAdHocChunksWithNeighborLogic( + dataSource, + segmentationLayer, + fullMeshRequest, + VoxelPosition(seedPosition.x, seedPosition.y, seedPosition.z, mag), + adHocChunkSize) + } yield chunks + } + encoded = verticesForChunks.map(adHocMeshToStl) array = combineEncodedChunksToStl(encoded) _ = logMeshingDuration(before, "ad-hoc meshing", array.length) } yield array - private def getAllAdHocChunks( + private def getAllAdHocChunksWithSegmentIndex( dataSource: UsableDataSource, segmentationLayer: SegmentationLayer, fullMeshRequest: FullMeshRequest, - topLeft: VoxelPosition, - chunkSize: Vec3Int, - visited: collection.mutable.Set[VoxelPosition] = collection.mutable.Set[VoxelPosition]())( + mag: Vec3Int, + )(implicit ec: ExecutionContext, tc: TokenContext): Fox[List[Array[Float]]] = + for { + segmentIndexFileKey <- segmentIndexFileService.lookUpSegmentIndexFileKey(dataSource.id, segmentationLayer) + segmentIds <- segmentIdsForAgglomerateIdIfNeeded( + dataSource.id, + segmentationLayer, + fullMeshRequest.mappingName, + fullMeshRequest.editableMappingTracingId, + fullMeshRequest.segmentId, + mappingNameForMeshFile = None, + omitMissing = false + ) + topLeftsNested: Seq[Array[Vec3Int]] <- Fox.serialCombined(segmentIds)(sId => + segmentIndexFileService.readSegmentIndex(segmentIndexFileKey, sId)) + topLefts: Array[Vec3Int] = topLeftsNested.toArray.flatten + targetMagPositions = segmentIndexFileService.topLeftsToDistinctTargetMagBucketPositions(topLefts, mag) + vertexChunksWithNeighbors: List[(Array[Float], List[Int])] <- Fox.serialCombined(targetMagPositions) { + targetMagPosition => + val adHocMeshRequest = AdHocMeshRequest( + Some(dataSource.id), + segmentationLayer, + Cuboid( + VoxelPosition( + targetMagPosition.x * mag.x * DataLayer.bucketLength, + targetMagPosition.y * mag.y * DataLayer.bucketLength, + targetMagPosition.z * mag.z * DataLayer.bucketLength, + mag + ), + DataLayer.bucketLength + 1, + DataLayer.bucketLength + 1, + DataLayer.bucketLength + 1 + ), + fullMeshRequest.segmentId, + dataSource.scale.factor, + tc, + fullMeshRequest.mappingName, + fullMeshRequest.mappingType, + fullMeshRequest.additionalCoordinates, + findNeighbors = false, + ) + adHocMeshService.requestAdHocMeshViaActor(adHocMeshRequest) + } + allVertices = vertexChunksWithNeighbors.map(_._1) + } yield allVertices + + private def getAllAdHocChunksWithNeighborLogic(dataSource: UsableDataSource, + segmentationLayer: SegmentationLayer, + fullMeshRequest: FullMeshRequest, + topLeft: VoxelPosition, + chunkSize: Vec3Int, + visited: collection.mutable.Set[VoxelPosition] = + collection.mutable.Set[VoxelPosition]())( implicit ec: ExecutionContext, tc: TokenContext): Fox[List[Array[Float]]] = { val adHocMeshRequest = AdHocMeshRequest( @@ -105,7 +166,7 @@ class DSFullMeshService @Inject()(meshFileService: MeshFileService, nextPositions: List[VoxelPosition] = generateNextTopLeftsFromNeighbors(topLeft, neighbors, chunkSize, visited) _ = visited ++= nextPositions neighborVerticesNested <- Fox.serialCombined(nextPositions) { position: VoxelPosition => - getAllAdHocChunks(dataSource, segmentationLayer, fullMeshRequest, position, chunkSize, visited) + getAllAdHocChunksWithNeighborLogic(dataSource, segmentationLayer, fullMeshRequest, position, chunkSize, visited) } allVertices: List[Array[Float]] = vertices +: neighborVerticesNested.flatten } yield allVertices diff --git a/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/editablemapping/EditableMappingService.scala b/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/editablemapping/EditableMappingService.scala index eaf50dbf8cb..6ac627d74f4 100644 --- a/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/editablemapping/EditableMappingService.scala +++ b/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/editablemapping/EditableMappingService.scala @@ -401,7 +401,7 @@ class EditableMappingService @Inject()( val adHocMeshRequest = AdHocMeshRequest( dataSourceId = None, dataLayer = editableMappingLayer, - cuboid = request.cuboid(editableMappingLayer), + cuboid = request.cuboid, segmentId = request.segmentId, voxelSizeFactor = request.voxelSizeFactorInUnit, tokenContext = tc, diff --git a/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/volume/VolumeTracingService.scala b/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/volume/VolumeTracingService.scala index 631c809d012..894e9fe90be 100644 --- a/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/volume/VolumeTracingService.scala +++ b/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/volume/VolumeTracingService.scala @@ -681,7 +681,7 @@ class VolumeTracingService @Inject()( adHocMeshRequest = AdHocMeshRequest( None, volumeLayer, - request.cuboid(volumeLayer), + request.cuboid, request.segmentId, request.voxelSizeFactorInUnit, tc, From be864da35101b6de06ccf1d51e74b00680b75ba9 Mon Sep 17 00:00:00 2001 From: Florian M Date: Mon, 29 Sep 2025 14:51:22 +0200 Subject: [PATCH 08/19] changelog --- unreleased_changes/8922.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/unreleased_changes/8922.md b/unreleased_changes/8922.md index 648eba1c141..192fd0217c7 100644 --- a/unreleased_changes/8922.md +++ b/unreleased_changes/8922.md @@ -1,2 +1,2 @@ ### Added -- The segment index file is now used while rendering ad-hoc meshes for static segmentation layers, e.g. when viewing a dataset. +- If a segmentation layer has a segment index file, it is now used to query ad-hoc meshes. That way, ad-hoc meshes can be correctly fetched also for unconnected segments. From c5f4b80ba63d01ffde8e668541c7e12aac453fcf Mon Sep 17 00:00:00 2001 From: Charlie Meister Date: Tue, 30 Sep 2025 19:20:07 +0200 Subject: [PATCH 09/19] WIP not tested: address review 1/2 --- .../model/sagas/meshes/ad_hoc_mesh_saga.ts | 45 +++++++++++-------- 1 file changed, 27 insertions(+), 18 deletions(-) diff --git a/frontend/javascripts/viewer/model/sagas/meshes/ad_hoc_mesh_saga.ts b/frontend/javascripts/viewer/model/sagas/meshes/ad_hoc_mesh_saga.ts index 41099830316..3b4ebf590ec 100644 --- a/frontend/javascripts/viewer/model/sagas/meshes/ad_hoc_mesh_saga.ts +++ b/frontend/javascripts/viewer/model/sagas/meshes/ad_hoc_mesh_saga.ts @@ -48,7 +48,7 @@ import { zoomedAddressToAnotherZoomStepWithInfo } from "viewer/model/helpers/pos import type { Saga } from "viewer/model/sagas/effect-generators"; import { select } from "viewer/model/sagas/effect-generators"; import { Model } from "viewer/singletons"; -import Store from "viewer/store"; +import Store, { type StoreDataset, type VolumeTracing } from "viewer/store"; import { getAdditionalCoordinatesAsString } from "../../accessors/flycam_accessor"; import { ensureSceneControllerReady, ensureWkReady } from "../ready_sagas"; @@ -294,6 +294,25 @@ function removeMeshWithoutVoxels( } } +function* getUsePositionsFromSegmentIndex( + volumeTracing: VolumeTracing | null | undefined, + dataset: StoreDataset, + layerName: string, + maybeTracingId?: string | null, +): Saga { + if (volumeTracing == null) { + return yield* call( + hasSegmentIndexInDataStoreCached, + dataset.dataStore.url, + dataset.id, + layerName, + ); + } + return ( + volumeTracing?.hasSegmentIndex && !volumeTracing.hasEditableMapping && maybeTracingId != null + ); +} + function* loadFullAdHocMesh( layer: DataLayer, segmentId: number, @@ -339,25 +358,15 @@ function* loadFullAdHocMesh( useDataStore = false; } - let isStaticSegmentationLayerWithSegmentIndex = false; - - if (volumeTracing == null) { - isStaticSegmentationLayerWithSegmentIndex = yield* call( - hasSegmentIndexInDataStoreCached, - dataset.dataStore.url, - dataset.id, - layer.name, - ); - } - // Segment stats can only be used for volume tracings that have a segment index + // Segment stats can only be used for segmentation layers that have a segment index // and that don't have editable mappings. - const usePositionsFromSegmentIndexForVolumeTracing = - volumeTracing?.hasSegmentIndex && - !volumeTracing.hasEditableMapping && - visibleSegmentationLayer?.tracingId != null; - const usePositionsFromSegmentIndex = - usePositionsFromSegmentIndexForVolumeTracing || isStaticSegmentationLayerWithSegmentIndex; + const usePositionsFromSegmentIndex = getUsePositionsFromSegmentIndex( + volumeTracing, + dataset, + layer.name, + visibleSegmentationLayer?.tracingId, + ); const dataStoreUrl = `${dataStoreHost}/data/datasets/${datasetId}/layers/${layer.name}`; const tracingStoreUrl = `${tracingStoreHost}/tracings/volume/${layer.name}`; From c97b9dff3ef0f725f680687dd0820de2846bbc79 Mon Sep 17 00:00:00 2001 From: Charlie Meister Date: Thu, 2 Oct 2025 11:37:30 +0200 Subject: [PATCH 10/19] WIP: move function to get request url to rest_api --- frontend/javascripts/admin/rest_api.ts | 17 +++++++++++++++-- .../bucket_data_handling/wkstore_adapter.ts | 16 ++++++++++++++-- .../model/sagas/meshes/ad_hoc_mesh_saga.ts | 1 - .../javascripts/viewer/view/context_menu.tsx | 13 +++++-------- .../segments_tab/segment_statistics_modal.tsx | 17 ++++++----------- .../segments_tab/segments_view_helper.tsx | 16 +--------------- 6 files changed, 41 insertions(+), 39 deletions(-) diff --git a/frontend/javascripts/admin/rest_api.ts b/frontend/javascripts/admin/rest_api.ts index ff90697c942..f4dc10d1ab4 100644 --- a/frontend/javascripts/admin/rest_api.ts +++ b/frontend/javascripts/admin/rest_api.ts @@ -80,6 +80,7 @@ import type { DatasourceConfiguration } from "types/schemas/datasource.types"; import type { AnnotationTypeFilterEnum, LOG_LEVELS, Vector3 } from "viewer/constants"; import Constants, { ControlModeEnum, AnnotationStateFilterEnum } from "viewer/constants"; import type BoundingBox from "viewer/model/bucket_data_handling/bounding_box"; +import type { LayerSourceInfo } from "viewer/model/bucket_data_handling/wkstore_adapter"; import { parseProtoAnnotation, parseProtoListOfLong, @@ -859,13 +860,24 @@ export const hasSegmentIndexInDataStoreCached = _.memoize(hasSegmentIndexInDataS args.join("::"), ); +export function getVolumeRequestUrl(layerSourceInfo: LayerSourceInfo) { + const { dataset, annotation, visibleSegmentationLayer, tracingId } = layerSourceInfo; + if (annotation == null || tracingId == null) { + return `${dataset.dataStore.url}/data/datasets/${dataset.id}/layers/${visibleSegmentationLayer.name}`; + } else { + const tracingStoreHost = annotation?.tracingStore.url; + return `${tracingStoreHost}/tracings/volume/${tracingId}`; + } +} + export function getSegmentVolumes( - requestUrl: string, + layerSourceInfo: LayerSourceInfo, mag: Vector3, segmentIds: Array, additionalCoordinates: AdditionalCoordinate[] | undefined | null, mappingName: string | null | undefined, ): Promise { + const requestUrl = getVolumeRequestUrl(layerSourceInfo); return doWithToken((token) => Request.sendJSONReceiveJSON(`${requestUrl}/segmentStatistics/volume?token=${token}`, { data: { additionalCoordinates, mag, segmentIds, mappingName }, @@ -875,12 +887,13 @@ export function getSegmentVolumes( } export function getSegmentBoundingBoxes( - requestUrl: string, + layerSourceInfo: LayerSourceInfo, mag: Vector3, segmentIds: Array, additionalCoordinates: AdditionalCoordinate[] | undefined | null, mappingName: string | null | undefined, ): Promise> { + const requestUrl = getVolumeRequestUrl(layerSourceInfo); return doWithToken((token) => Request.sendJSONReceiveJSON(`${requestUrl}/segmentStatistics/boundingBox?token=${token}`, { data: { additionalCoordinates, mag, segmentIds, mappingName }, diff --git a/frontend/javascripts/viewer/model/bucket_data_handling/wkstore_adapter.ts b/frontend/javascripts/viewer/model/bucket_data_handling/wkstore_adapter.ts index afffe7d476b..6ea14d2dad7 100644 --- a/frontend/javascripts/viewer/model/bucket_data_handling/wkstore_adapter.ts +++ b/frontend/javascripts/viewer/model/bucket_data_handling/wkstore_adapter.ts @@ -5,7 +5,12 @@ import { parseMaybe } from "libs/utils"; import WebworkerPool from "libs/webworker_pool"; import window from "libs/window"; import _ from "lodash"; -import type { AdditionalCoordinate } from "types/api_types"; +import type { + APIDataLayer, + APIDataset, + APISegmentationLayer, + AdditionalCoordinate, +} from "types/api_types"; import type { BucketAddress, Vector3 } from "viewer/constants"; import constants, { MappingStatusEnum } from "viewer/constants"; import { @@ -22,7 +27,7 @@ import type { DataBucket } from "viewer/model/bucket_data_handling/bucket"; import { bucketPositionToGlobalAddress } from "viewer/model/helpers/position_converter"; import type { UpdateActionWithoutIsolationRequirement } from "viewer/model/sagas/volume/update_actions"; import { updateBucket } from "viewer/model/sagas/volume/update_actions"; -import type { DataLayerType, VolumeTracing } from "viewer/store"; +import type { DataLayerType, StoreAnnotation, VolumeTracing } from "viewer/store"; import Store from "viewer/store"; import ByteArraysToLz4Base64Worker from "viewer/workers/byte_arrays_to_lz4_base64.worker"; import { createWorker } from "viewer/workers/comlink_wrapper"; @@ -30,6 +35,13 @@ import DecodeFourBitWorker from "viewer/workers/decode_four_bit.worker"; import { getGlobalDataConnectionInfo } from "../data_connection_info"; import type { MagInfo } from "../helpers/mag_info"; +export type LayerSourceInfo = { + dataset: APIDataset; + annotation: StoreAnnotation | null; + tracingId: string | undefined; + visibleSegmentationLayer: APISegmentationLayer | APIDataLayer; +}; + const decodeFourBit = createWorker(DecodeFourBitWorker); // For 32-bit buckets with 32^3 voxels, a COMPRESSION_BATCH_SIZE of diff --git a/frontend/javascripts/viewer/model/sagas/meshes/ad_hoc_mesh_saga.ts b/frontend/javascripts/viewer/model/sagas/meshes/ad_hoc_mesh_saga.ts index 3b4ebf590ec..66fd826f378 100644 --- a/frontend/javascripts/viewer/model/sagas/meshes/ad_hoc_mesh_saga.ts +++ b/frontend/javascripts/viewer/model/sagas/meshes/ad_hoc_mesh_saga.ts @@ -360,7 +360,6 @@ function* loadFullAdHocMesh( // Segment stats can only be used for segmentation layers that have a segment index // and that don't have editable mappings. - const usePositionsFromSegmentIndex = getUsePositionsFromSegmentIndex( volumeTracing, dataset, diff --git a/frontend/javascripts/viewer/view/context_menu.tsx b/frontend/javascripts/viewer/view/context_menu.tsx index ceae161419f..df699b78f4e 100644 --- a/frontend/javascripts/viewer/view/context_menu.tsx +++ b/frontend/javascripts/viewer/view/context_menu.tsx @@ -146,10 +146,7 @@ import type { import { deleteNodeAsUserAction } from "viewer/model/actions/skeletontracing_actions_with_effects"; import { type MutableNode, type Tree, TreeMap } from "viewer/model/types/tree_types"; import Store from "viewer/store"; -import { - getVolumeRequestUrl, - withMappingActivationConfirmation, -} from "viewer/view/right-border-tabs/segments_tab/segments_view_helper"; +import { withMappingActivationConfirmation } from "viewer/view/right-border-tabs/segments_tab/segments_view_helper"; import { LayoutEvents, layoutEmitter } from "./layouting/layout_persistence"; import { LoadMeshMenuItemLabel } from "./right-border-tabs/segments_tab/load_mesh_menu_item_label"; @@ -1690,26 +1687,26 @@ function ContextMenuInner() { if (visibleSegmentationLayer == null || !isSegmentIndexAvailable) return []; const tracingId = volumeTracing?.tracingId; const additionalCoordinates = flycam.additionalCoordinates; - const requestUrl = getVolumeRequestUrl( + const infoForRequestUrl = { dataset, annotation, tracingId, visibleSegmentationLayer, - ); + }; const magInfo = getMagInfo(visibleSegmentationLayer.resolutions); const layersFinestMag = magInfo.getFinestMag(); const voxelSize = dataset.dataSource.scale; try { const [segmentSize] = await getSegmentVolumes( - requestUrl, + infoForRequestUrl, layersFinestMag, [clickedSegmentOrMeshId], additionalCoordinates, mappingName, ); const [boundingBoxInRequestedMag] = await getSegmentBoundingBoxes( - requestUrl, + infoForRequestUrl, layersFinestMag, [clickedSegmentOrMeshId], additionalCoordinates, diff --git a/frontend/javascripts/viewer/view/right-border-tabs/segments_tab/segment_statistics_modal.tsx b/frontend/javascripts/viewer/view/right-border-tabs/segments_tab/segment_statistics_modal.tsx index bf2c9f0f05c..a610b7d3ec3 100644 --- a/frontend/javascripts/viewer/view/right-border-tabs/segments_tab/segment_statistics_modal.tsx +++ b/frontend/javascripts/viewer/view/right-border-tabs/segments_tab/segment_statistics_modal.tsx @@ -17,11 +17,7 @@ import { getBoundingBoxInMag1 } from "viewer/model/sagas/volume/helpers"; import { voxelToVolumeInUnit } from "viewer/model/scaleinfo"; import { api } from "viewer/singletons"; import type { Segment } from "viewer/store"; -import { - type SegmentHierarchyGroup, - type SegmentHierarchyNode, - getVolumeRequestUrl, -} from "./segments_view_helper"; +import type { SegmentHierarchyGroup, SegmentHierarchyNode } from "./segments_view_helper"; const MODAL_ERROR_MESSAGE = "Segment statistics could not be fetched. Check the console for more details."; @@ -104,12 +100,12 @@ export function SegmentStatisticsModal({ const voxelSize = dataset.dataSource.scale; // Omit checking that all prerequisites for segment stats (such as a segment index) are // met right here because that should happen before opening the modal. - const requestUrl = getVolumeRequestUrl( + const storeInfoType = { dataset, annotation, - visibleSegmentationLayer.tracingId, + tracingId: visibleSegmentationLayer.tracingId, visibleSegmentationLayer, - ); + }; const additionalCoordinates = useWkSelector((state) => state.flycam.additionalCoordinates); const hasAdditionalCoords = hasAdditionalCoordinates(additionalCoordinates); const additionalCoordinateStringForModal = getAdditionalCoordinatesAsString( @@ -119,7 +115,6 @@ export function SegmentStatisticsModal({ const segmentStatisticsObjects = useFetch( async () => { await api.tracing.save(); - if (requestUrl == null) return; const maybeVolumeTracing = tracingId != null ? getVolumeTracingById(annotation, tracingId) : null; const maybeGetMappingName = () => { @@ -132,14 +127,14 @@ export function SegmentStatisticsModal({ }; const segmentStatisticsObjects = await Promise.all([ getSegmentVolumes( - requestUrl, + storeInfoType, layersFinestMag, segments.map((segment) => segment.id), additionalCoordinates, maybeGetMappingName(), ), getSegmentBoundingBoxes( - requestUrl, + storeInfoType, layersFinestMag, segments.map((segment) => segment.id), additionalCoordinates, diff --git a/frontend/javascripts/viewer/view/right-border-tabs/segments_tab/segments_view_helper.tsx b/frontend/javascripts/viewer/view/right-border-tabs/segments_tab/segments_view_helper.tsx index dcba796a109..0e80ad191f3 100644 --- a/frontend/javascripts/viewer/view/right-border-tabs/segments_tab/segments_view_helper.tsx +++ b/frontend/javascripts/viewer/view/right-border-tabs/segments_tab/segments_view_helper.tsx @@ -3,7 +3,7 @@ import { Modal } from "antd"; import type { BasicDataNode } from "antd/es/tree"; import { waitForCondition } from "libs/utils"; import type { MenuClickEventHandler } from "rc-menu/lib/interface"; -import type { APIDataLayer, APIDataset, APISegmentationLayer } from "types/api_types"; +import type { APIDataLayer, APIDataset } from "types/api_types"; import { MappingStatusEnum } from "viewer/constants"; import { getMappingInfo } from "viewer/model/accessors/dataset_accessor"; import { @@ -47,20 +47,6 @@ export function getBaseSegmentationName(segmentationLayer: APIDataLayer) { ); } -export function getVolumeRequestUrl( - dataset: APIDataset, - annotation: StoreAnnotation | null, - tracingId: string | undefined, - visibleSegmentationLayer: APISegmentationLayer | APIDataLayer, -) { - if (annotation == null || tracingId == null) { - return `${dataset.dataStore.url}/data/datasets/${dataset.id}/layers/${visibleSegmentationLayer.name}`; - } else { - const tracingStoreHost = annotation?.tracingStore.url; - return `${tracingStoreHost}/tracings/volume/${tracingId}`; - } -} - export async function hasSegmentIndex( visibleSegmentationLayer: APIDataLayer, dataset: APIDataset, From 82821e348c25154aae058ab94bf372b0a9bf21e8 Mon Sep 17 00:00:00 2001 From: Charlie Meister Date: Thu, 2 Oct 2025 13:52:37 +0200 Subject: [PATCH 11/19] WIP: remove building requestUrl from ad_hoc_mesh_saga --- frontend/javascripts/admin/rest_api.ts | 8 ++-- .../bucket_data_handling/wkstore_adapter.ts | 3 +- .../model/sagas/meshes/ad_hoc_mesh_saga.ts | 42 +++++++++---------- 3 files changed, 26 insertions(+), 27 deletions(-) diff --git a/frontend/javascripts/admin/rest_api.ts b/frontend/javascripts/admin/rest_api.ts index f4dc10d1ab4..b62bb93c9d6 100644 --- a/frontend/javascripts/admin/rest_api.ts +++ b/frontend/javascripts/admin/rest_api.ts @@ -863,7 +863,7 @@ export const hasSegmentIndexInDataStoreCached = _.memoize(hasSegmentIndexInDataS export function getVolumeRequestUrl(layerSourceInfo: LayerSourceInfo) { const { dataset, annotation, visibleSegmentationLayer, tracingId } = layerSourceInfo; if (annotation == null || tracingId == null) { - return `${dataset.dataStore.url}/data/datasets/${dataset.id}/layers/${visibleSegmentationLayer.name}`; + return `${dataset.dataStore.url}/data/datasets/${dataset.id}/layers/${visibleSegmentationLayer?.name}`; } else { const tracingStoreHost = annotation?.tracingStore.url; return `${tracingStoreHost}/tracings/volume/${tracingId}`; @@ -1897,12 +1897,13 @@ type MeshRequest = { }; export function computeAdHocMesh( - requestUrl: string, + dataSourceInfo: LayerSourceInfo, meshRequest: MeshRequest, ): Promise<{ buffer: ArrayBuffer; neighbors: Array; }> { + const requestUrl = getVolumeRequestUrl(dataSourceInfo); const { positionWithPadding, additionalCoordinates, @@ -1944,13 +1945,14 @@ export function computeAdHocMesh( } export function getBucketPositionsForAdHocMesh( - requestUrl: string, + dataSourceInfo: LayerSourceInfo, segmentId: number, cubeSize: Vector3, mag: Vector3, additionalCoordinates: AdditionalCoordinate[] | null | undefined, mappingName: string | null | undefined, ): Promise { + const requestUrl = getVolumeRequestUrl(dataSourceInfo); return doWithToken(async (token) => { const params = new URLSearchParams(); params.set("token", token); diff --git a/frontend/javascripts/viewer/model/bucket_data_handling/wkstore_adapter.ts b/frontend/javascripts/viewer/model/bucket_data_handling/wkstore_adapter.ts index 6ea14d2dad7..9f2afd0eca4 100644 --- a/frontend/javascripts/viewer/model/bucket_data_handling/wkstore_adapter.ts +++ b/frontend/javascripts/viewer/model/bucket_data_handling/wkstore_adapter.ts @@ -39,7 +39,8 @@ export type LayerSourceInfo = { dataset: APIDataset; annotation: StoreAnnotation | null; tracingId: string | undefined; - visibleSegmentationLayer: APISegmentationLayer | APIDataLayer; + visibleSegmentationLayer: APISegmentationLayer | APIDataLayer | undefined | null; + forceUsingDataStore?: boolean | undefined | null; }; const decodeFourBit = createWorker(DecodeFourBitWorker); diff --git a/frontend/javascripts/viewer/model/sagas/meshes/ad_hoc_mesh_saga.ts b/frontend/javascripts/viewer/model/sagas/meshes/ad_hoc_mesh_saga.ts index 66fd826f378..0cec87edadd 100644 --- a/frontend/javascripts/viewer/model/sagas/meshes/ad_hoc_mesh_saga.ts +++ b/frontend/javascripts/viewer/model/sagas/meshes/ad_hoc_mesh_saga.ts @@ -42,6 +42,7 @@ import { type LoadAdHocMeshAction, loadPrecomputedMeshAction, } from "viewer/model/actions/segmentation_actions"; +import type { LayerSourceInfo } from "viewer/model/bucket_data_handling/wkstore_adapter"; import type DataLayer from "viewer/model/data_layer"; import type { MagInfo } from "viewer/model/helpers/mag_info"; import { zoomedAddressToAnotherZoomStepWithInfo } from "viewer/model/helpers/position_converter"; @@ -340,22 +341,20 @@ function* loadFullAdHocMesh( yield* put(startedLoadingMeshAction(layer.name, segmentId)); const cubeSize = marchingCubeSizeInTargetMag(); - const tracingStoreHost = yield* select((state) => state.annotation.tracingStore.url); const dataset = yield* select((state) => state.dataset); - const datasetId = dataset.id; - const dataStoreHost = dataset.dataStore.url; const mag = magInfo.getMagByIndexOrThrow(zoomStep); const volumeTracing = yield* select((state) => getActiveSegmentationTracing(state)); + const annotation = yield* select((state) => state.annotation); const visibleSegmentationLayer = yield* select((state) => getVisibleSegmentationLayer(state)); // Fetch from datastore if no volumetracing ... - let useDataStore = volumeTracing == null || visibleSegmentationLayer?.tracingId == null; + let forceUsingDataStore = volumeTracing == null || visibleSegmentationLayer?.tracingId == null; if (meshExtraInfo.useDataStore != null) { // ... except if the caller specified whether to use the data store ... - useDataStore = meshExtraInfo.useDataStore; + forceUsingDataStore = meshExtraInfo.useDataStore; } else if (volumeTracing?.hasEditableMapping) { // ... or if an editable mapping is active. - useDataStore = false; + forceUsingDataStore = false; } // Segment stats can only be used for segmentation layers that have a segment index @@ -367,13 +366,17 @@ function* loadFullAdHocMesh( visibleSegmentationLayer?.tracingId, ); - const dataStoreUrl = `${dataStoreHost}/data/datasets/${datasetId}/layers/${layer.name}`; - const tracingStoreUrl = `${tracingStoreHost}/tracings/volume/${layer.name}`; - const requestUrl = useDataStore ? dataStoreUrl : tracingStoreUrl; + const dataSourceInfo: LayerSourceInfo = { + dataset, + annotation, + tracingId: visibleSegmentationLayer?.tracingId, + visibleSegmentationLayer, + forceUsingDataStore, + }; let positionsToRequest = usePositionsFromSegmentIndex ? yield* getChunkPositionsFromSegmentIndex( - requestUrl, + dataSourceInfo, segmentId, cubeSize, mag, @@ -403,7 +406,7 @@ function* loadFullAdHocMesh( magInfo, isInitialRequest, removeExistingMesh && isInitialRequest, - useDataStore, + dataSourceInfo, !usePositionsFromSegmentIndex, ); isInitialRequest = false; @@ -420,7 +423,7 @@ function* loadFullAdHocMesh( } function* getChunkPositionsFromSegmentIndex( - requestUrl: string, + dataSourceInfo: LayerSourceInfo, segmentId: number, cubeSize: Vector3, mag: Vector3, @@ -430,7 +433,7 @@ function* getChunkPositionsFromSegmentIndex( ) { const targetMagPositions = yield* call( getBucketPositionsForAdHocMesh, - requestUrl, + dataSourceInfo, segmentId, cubeSize, mag, @@ -454,7 +457,7 @@ function* maybeLoadMeshChunk( magInfo: MagInfo, isInitialRequest: boolean, removeExistingMesh: boolean, - useDataStore: boolean, + dataSourceInfo: LayerSourceInfo, findNeighbors: boolean, ): Saga { const additionalCoordinates = yield* select((state) => state.flycam.additionalCoordinates); @@ -475,17 +478,10 @@ function* maybeLoadMeshChunk( batchCounterPerSegment[segmentId]++; threeDMap.set(paddedPositionWithinLayer, true); const scaleFactor = yield* select((state) => state.dataset.dataSource.scale.factor); - const dataStoreHost = yield* select((state) => state.dataset.dataStore.url); - const datasetId = yield* select((state) => state.dataset.id); - const tracingStoreHost = yield* select((state) => state.annotation.tracingStore.url); - const dataStoreUrl = `${dataStoreHost}/data/datasets/${datasetId}/layers/${ - layer.fallbackLayer != null ? layer.fallbackLayer : layer.name - }`; - const tracingStoreUrl = `${tracingStoreHost}/tracings/volume/${layer.name}`; if (isInitialRequest) { sendAnalyticsEvent("request_isosurface", { - mode: useDataStore ? "view" : "annotation", + mode: dataSourceInfo.forceUsingDataStore ? "view" : "annotation", }); } @@ -502,7 +498,7 @@ function* maybeLoadMeshChunk( context: null, fn: computeAdHocMesh, }, - useDataStore ? dataStoreUrl : tracingStoreUrl, + dataSourceInfo, { positionWithPadding: paddedPositionWithinLayer, additionalCoordinates: additionalCoordinates || undefined, From 3786af56a9f3aadef4c61058c032f9a7eb5fd191 Mon Sep 17 00:00:00 2001 From: Charlie Meister Date: Thu, 2 Oct 2025 15:50:05 +0200 Subject: [PATCH 12/19] fix adhoc mesh loading --- frontend/javascripts/admin/rest_api.ts | 7 ++++--- .../viewer/model/bucket_data_handling/wkstore_adapter.ts | 9 ++------- .../viewer/model/sagas/meshes/ad_hoc_mesh_saga.ts | 5 ++++- frontend/javascripts/viewer/view/context_menu.tsx | 2 +- .../segments_tab/segment_statistics_modal.tsx | 2 +- 5 files changed, 12 insertions(+), 13 deletions(-) diff --git a/frontend/javascripts/admin/rest_api.ts b/frontend/javascripts/admin/rest_api.ts index b62bb93c9d6..8fd8f423311 100644 --- a/frontend/javascripts/admin/rest_api.ts +++ b/frontend/javascripts/admin/rest_api.ts @@ -861,9 +861,10 @@ export const hasSegmentIndexInDataStoreCached = _.memoize(hasSegmentIndexInDataS ); export function getVolumeRequestUrl(layerSourceInfo: LayerSourceInfo) { - const { dataset, annotation, visibleSegmentationLayer, tracingId } = layerSourceInfo; - if (annotation == null || tracingId == null) { - return `${dataset.dataStore.url}/data/datasets/${dataset.id}/layers/${visibleSegmentationLayer?.name}`; + const { dataset, annotation, visibleSegmentationLayerName, tracingId, forceUsingDataStore } = + layerSourceInfo; + if (annotation == null || tracingId == null || forceUsingDataStore) { + return `${dataset.dataStore.url}/data/datasets/${dataset.id}/layers/${visibleSegmentationLayerName}`; } else { const tracingStoreHost = annotation?.tracingStore.url; return `${tracingStoreHost}/tracings/volume/${tracingId}`; diff --git a/frontend/javascripts/viewer/model/bucket_data_handling/wkstore_adapter.ts b/frontend/javascripts/viewer/model/bucket_data_handling/wkstore_adapter.ts index 9f2afd0eca4..f518cf565d8 100644 --- a/frontend/javascripts/viewer/model/bucket_data_handling/wkstore_adapter.ts +++ b/frontend/javascripts/viewer/model/bucket_data_handling/wkstore_adapter.ts @@ -5,12 +5,7 @@ import { parseMaybe } from "libs/utils"; import WebworkerPool from "libs/webworker_pool"; import window from "libs/window"; import _ from "lodash"; -import type { - APIDataLayer, - APIDataset, - APISegmentationLayer, - AdditionalCoordinate, -} from "types/api_types"; +import type { APIDataset, AdditionalCoordinate } from "types/api_types"; import type { BucketAddress, Vector3 } from "viewer/constants"; import constants, { MappingStatusEnum } from "viewer/constants"; import { @@ -39,7 +34,7 @@ export type LayerSourceInfo = { dataset: APIDataset; annotation: StoreAnnotation | null; tracingId: string | undefined; - visibleSegmentationLayer: APISegmentationLayer | APIDataLayer | undefined | null; + visibleSegmentationLayerName: string | null | undefined; forceUsingDataStore?: boolean | undefined | null; }; diff --git a/frontend/javascripts/viewer/model/sagas/meshes/ad_hoc_mesh_saga.ts b/frontend/javascripts/viewer/model/sagas/meshes/ad_hoc_mesh_saga.ts index 0cec87edadd..d3f62af7c67 100644 --- a/frontend/javascripts/viewer/model/sagas/meshes/ad_hoc_mesh_saga.ts +++ b/frontend/javascripts/viewer/model/sagas/meshes/ad_hoc_mesh_saga.ts @@ -370,7 +370,10 @@ function* loadFullAdHocMesh( dataset, annotation, tracingId: visibleSegmentationLayer?.tracingId, - visibleSegmentationLayer, + visibleSegmentationLayerName: + visibleSegmentationLayer?.tracingId == null + ? visibleSegmentationLayer?.name + : visibleSegmentationLayer?.fallbackLayer, forceUsingDataStore, }; diff --git a/frontend/javascripts/viewer/view/context_menu.tsx b/frontend/javascripts/viewer/view/context_menu.tsx index df699b78f4e..1866cca1fd2 100644 --- a/frontend/javascripts/viewer/view/context_menu.tsx +++ b/frontend/javascripts/viewer/view/context_menu.tsx @@ -1691,7 +1691,7 @@ function ContextMenuInner() { dataset, annotation, tracingId, - visibleSegmentationLayer, + visibleSegmentationLayerName: visibleSegmentationLayer.name, }; const magInfo = getMagInfo(visibleSegmentationLayer.resolutions); const layersFinestMag = magInfo.getFinestMag(); diff --git a/frontend/javascripts/viewer/view/right-border-tabs/segments_tab/segment_statistics_modal.tsx b/frontend/javascripts/viewer/view/right-border-tabs/segments_tab/segment_statistics_modal.tsx index a610b7d3ec3..c7992e0b784 100644 --- a/frontend/javascripts/viewer/view/right-border-tabs/segments_tab/segment_statistics_modal.tsx +++ b/frontend/javascripts/viewer/view/right-border-tabs/segments_tab/segment_statistics_modal.tsx @@ -104,7 +104,7 @@ export function SegmentStatisticsModal({ dataset, annotation, tracingId: visibleSegmentationLayer.tracingId, - visibleSegmentationLayer, + visibleSegmentationLayerName: visibleSegmentationLayer.name, }; const additionalCoordinates = useWkSelector((state) => state.flycam.additionalCoordinates); const hasAdditionalCoords = hasAdditionalCoordinates(additionalCoordinates); From 76091f977ed86c9bfe2b567f5e03c939ab694a30 Mon Sep 17 00:00:00 2001 From: Charlie Meister Date: Thu, 2 Oct 2025 16:03:04 +0200 Subject: [PATCH 13/19] move getURL function to adapter and rename some vars --- frontend/javascripts/admin/rest_api.ts | 26 +++++++------------ .../bucket_data_handling/wkstore_adapter.ts | 11 ++++++++ .../model/sagas/meshes/ad_hoc_mesh_saga.ts | 16 ++++++------ 3 files changed, 28 insertions(+), 25 deletions(-) diff --git a/frontend/javascripts/admin/rest_api.ts b/frontend/javascripts/admin/rest_api.ts index 8fd8f423311..c55ed350020 100644 --- a/frontend/javascripts/admin/rest_api.ts +++ b/frontend/javascripts/admin/rest_api.ts @@ -80,7 +80,10 @@ import type { DatasourceConfiguration } from "types/schemas/datasource.types"; import type { AnnotationTypeFilterEnum, LOG_LEVELS, Vector3 } from "viewer/constants"; import Constants, { ControlModeEnum, AnnotationStateFilterEnum } from "viewer/constants"; import type BoundingBox from "viewer/model/bucket_data_handling/bounding_box"; -import type { LayerSourceInfo } from "viewer/model/bucket_data_handling/wkstore_adapter"; +import { + type LayerSourceInfo, + getDataOrTracingStoreUrl, +} from "viewer/model/bucket_data_handling/wkstore_adapter"; import { parseProtoAnnotation, parseProtoListOfLong, @@ -860,17 +863,6 @@ export const hasSegmentIndexInDataStoreCached = _.memoize(hasSegmentIndexInDataS args.join("::"), ); -export function getVolumeRequestUrl(layerSourceInfo: LayerSourceInfo) { - const { dataset, annotation, visibleSegmentationLayerName, tracingId, forceUsingDataStore } = - layerSourceInfo; - if (annotation == null || tracingId == null || forceUsingDataStore) { - return `${dataset.dataStore.url}/data/datasets/${dataset.id}/layers/${visibleSegmentationLayerName}`; - } else { - const tracingStoreHost = annotation?.tracingStore.url; - return `${tracingStoreHost}/tracings/volume/${tracingId}`; - } -} - export function getSegmentVolumes( layerSourceInfo: LayerSourceInfo, mag: Vector3, @@ -878,7 +870,7 @@ export function getSegmentVolumes( additionalCoordinates: AdditionalCoordinate[] | undefined | null, mappingName: string | null | undefined, ): Promise { - const requestUrl = getVolumeRequestUrl(layerSourceInfo); + const requestUrl = getDataOrTracingStoreUrl(layerSourceInfo); return doWithToken((token) => Request.sendJSONReceiveJSON(`${requestUrl}/segmentStatistics/volume?token=${token}`, { data: { additionalCoordinates, mag, segmentIds, mappingName }, @@ -894,7 +886,7 @@ export function getSegmentBoundingBoxes( additionalCoordinates: AdditionalCoordinate[] | undefined | null, mappingName: string | null | undefined, ): Promise> { - const requestUrl = getVolumeRequestUrl(layerSourceInfo); + const requestUrl = getDataOrTracingStoreUrl(layerSourceInfo); return doWithToken((token) => Request.sendJSONReceiveJSON(`${requestUrl}/segmentStatistics/boundingBox?token=${token}`, { data: { additionalCoordinates, mag, segmentIds, mappingName }, @@ -1898,13 +1890,13 @@ type MeshRequest = { }; export function computeAdHocMesh( - dataSourceInfo: LayerSourceInfo, + layerSourceInfo: LayerSourceInfo, meshRequest: MeshRequest, ): Promise<{ buffer: ArrayBuffer; neighbors: Array; }> { - const requestUrl = getVolumeRequestUrl(dataSourceInfo); + const requestUrl = getDataOrTracingStoreUrl(layerSourceInfo); const { positionWithPadding, additionalCoordinates, @@ -1953,7 +1945,7 @@ export function getBucketPositionsForAdHocMesh( additionalCoordinates: AdditionalCoordinate[] | null | undefined, mappingName: string | null | undefined, ): Promise { - const requestUrl = getVolumeRequestUrl(dataSourceInfo); + const requestUrl = getDataOrTracingStoreUrl(dataSourceInfo); return doWithToken(async (token) => { const params = new URLSearchParams(); params.set("token", token); diff --git a/frontend/javascripts/viewer/model/bucket_data_handling/wkstore_adapter.ts b/frontend/javascripts/viewer/model/bucket_data_handling/wkstore_adapter.ts index f518cf565d8..3ac207f0a0d 100644 --- a/frontend/javascripts/viewer/model/bucket_data_handling/wkstore_adapter.ts +++ b/frontend/javascripts/viewer/model/bucket_data_handling/wkstore_adapter.ts @@ -273,6 +273,17 @@ export async function requestFromStore( } } +export function getDataOrTracingStoreUrl(layerSourceInfo: LayerSourceInfo) { + const { dataset, annotation, visibleSegmentationLayerName, tracingId, forceUsingDataStore } = + layerSourceInfo; + if (annotation == null || tracingId == null || forceUsingDataStore) { + return `${dataset.dataStore.url}/data/datasets/${dataset.id}/layers/${visibleSegmentationLayerName}`; + } else { + const tracingStoreHost = annotation?.tracingStore.url; + return `${tracingStoreHost}/tracings/volume/${tracingId}`; + } +} + function sliceBufferIntoPieces( layerInfo: DataLayerType, batch: Array, diff --git a/frontend/javascripts/viewer/model/sagas/meshes/ad_hoc_mesh_saga.ts b/frontend/javascripts/viewer/model/sagas/meshes/ad_hoc_mesh_saga.ts index d3f62af7c67..efe6160760f 100644 --- a/frontend/javascripts/viewer/model/sagas/meshes/ad_hoc_mesh_saga.ts +++ b/frontend/javascripts/viewer/model/sagas/meshes/ad_hoc_mesh_saga.ts @@ -366,7 +366,7 @@ function* loadFullAdHocMesh( visibleSegmentationLayer?.tracingId, ); - const dataSourceInfo: LayerSourceInfo = { + const layerSourceInfo: LayerSourceInfo = { dataset, annotation, tracingId: visibleSegmentationLayer?.tracingId, @@ -379,7 +379,7 @@ function* loadFullAdHocMesh( let positionsToRequest = usePositionsFromSegmentIndex ? yield* getChunkPositionsFromSegmentIndex( - dataSourceInfo, + layerSourceInfo, segmentId, cubeSize, mag, @@ -409,7 +409,7 @@ function* loadFullAdHocMesh( magInfo, isInitialRequest, removeExistingMesh && isInitialRequest, - dataSourceInfo, + layerSourceInfo, !usePositionsFromSegmentIndex, ); isInitialRequest = false; @@ -426,7 +426,7 @@ function* loadFullAdHocMesh( } function* getChunkPositionsFromSegmentIndex( - dataSourceInfo: LayerSourceInfo, + layerSourceInfo: LayerSourceInfo, segmentId: number, cubeSize: Vector3, mag: Vector3, @@ -436,7 +436,7 @@ function* getChunkPositionsFromSegmentIndex( ) { const targetMagPositions = yield* call( getBucketPositionsForAdHocMesh, - dataSourceInfo, + layerSourceInfo, segmentId, cubeSize, mag, @@ -460,7 +460,7 @@ function* maybeLoadMeshChunk( magInfo: MagInfo, isInitialRequest: boolean, removeExistingMesh: boolean, - dataSourceInfo: LayerSourceInfo, + layerSourceInfo: LayerSourceInfo, findNeighbors: boolean, ): Saga { const additionalCoordinates = yield* select((state) => state.flycam.additionalCoordinates); @@ -484,7 +484,7 @@ function* maybeLoadMeshChunk( if (isInitialRequest) { sendAnalyticsEvent("request_isosurface", { - mode: dataSourceInfo.forceUsingDataStore ? "view" : "annotation", + mode: layerSourceInfo.forceUsingDataStore ? "view" : "annotation", }); } @@ -501,7 +501,7 @@ function* maybeLoadMeshChunk( context: null, fn: computeAdHocMesh, }, - dataSourceInfo, + layerSourceInfo, { positionWithPadding: paddedPositionWithinLayer, additionalCoordinates: additionalCoordinates || undefined, From 30f80f63acf5a87f20b0ab0ee16ecc5744c1f8ce Mon Sep 17 00:00:00 2001 From: Charlie Meister Date: Thu, 2 Oct 2025 16:33:46 +0200 Subject: [PATCH 14/19] add yield* to function call --- frontend/javascripts/admin/rest_api.ts | 4 ++-- .../javascripts/viewer/model/sagas/meshes/ad_hoc_mesh_saga.ts | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/frontend/javascripts/admin/rest_api.ts b/frontend/javascripts/admin/rest_api.ts index c55ed350020..41000a5059d 100644 --- a/frontend/javascripts/admin/rest_api.ts +++ b/frontend/javascripts/admin/rest_api.ts @@ -1938,14 +1938,14 @@ export function computeAdHocMesh( } export function getBucketPositionsForAdHocMesh( - dataSourceInfo: LayerSourceInfo, + layerSourceInfo: LayerSourceInfo, segmentId: number, cubeSize: Vector3, mag: Vector3, additionalCoordinates: AdditionalCoordinate[] | null | undefined, mappingName: string | null | undefined, ): Promise { - const requestUrl = getDataOrTracingStoreUrl(dataSourceInfo); + const requestUrl = getDataOrTracingStoreUrl(layerSourceInfo); return doWithToken(async (token) => { const params = new URLSearchParams(); params.set("token", token); diff --git a/frontend/javascripts/viewer/model/sagas/meshes/ad_hoc_mesh_saga.ts b/frontend/javascripts/viewer/model/sagas/meshes/ad_hoc_mesh_saga.ts index efe6160760f..a7b973daee7 100644 --- a/frontend/javascripts/viewer/model/sagas/meshes/ad_hoc_mesh_saga.ts +++ b/frontend/javascripts/viewer/model/sagas/meshes/ad_hoc_mesh_saga.ts @@ -359,7 +359,8 @@ function* loadFullAdHocMesh( // Segment stats can only be used for segmentation layers that have a segment index // and that don't have editable mappings. - const usePositionsFromSegmentIndex = getUsePositionsFromSegmentIndex( + const usePositionsFromSegmentIndex = yield* call( + getUsePositionsFromSegmentIndex, volumeTracing, dataset, layer.name, From f0911f7139d9f275911228f247c88f6b0f1ec454 Mon Sep 17 00:00:00 2001 From: Charlie Meister Date: Thu, 2 Oct 2025 17:11:38 +0200 Subject: [PATCH 15/19] fix cyclic dependency --- frontend/javascripts/admin/rest_api.ts | 2 +- .../bucket_data_handling/wkstore_adapter.ts | 23 ++----------------- .../bucket_data_handling/wkstore_helper.ts | 21 +++++++++++++++++ .../model/sagas/meshes/ad_hoc_mesh_saga.ts | 2 +- 4 files changed, 25 insertions(+), 23 deletions(-) create mode 100644 frontend/javascripts/viewer/model/bucket_data_handling/wkstore_helper.ts diff --git a/frontend/javascripts/admin/rest_api.ts b/frontend/javascripts/admin/rest_api.ts index 9dc357ff6a8..e72180a2dbb 100644 --- a/frontend/javascripts/admin/rest_api.ts +++ b/frontend/javascripts/admin/rest_api.ts @@ -83,7 +83,7 @@ import type BoundingBox from "viewer/model/bucket_data_handling/bounding_box"; import { type LayerSourceInfo, getDataOrTracingStoreUrl, -} from "viewer/model/bucket_data_handling/wkstore_adapter"; +} from "viewer/model/bucket_data_handling/wkstore_helper"; import { parseProtoAnnotation, parseProtoListOfLong, diff --git a/frontend/javascripts/viewer/model/bucket_data_handling/wkstore_adapter.ts b/frontend/javascripts/viewer/model/bucket_data_handling/wkstore_adapter.ts index 3ac207f0a0d..afffe7d476b 100644 --- a/frontend/javascripts/viewer/model/bucket_data_handling/wkstore_adapter.ts +++ b/frontend/javascripts/viewer/model/bucket_data_handling/wkstore_adapter.ts @@ -5,7 +5,7 @@ import { parseMaybe } from "libs/utils"; import WebworkerPool from "libs/webworker_pool"; import window from "libs/window"; import _ from "lodash"; -import type { APIDataset, AdditionalCoordinate } from "types/api_types"; +import type { AdditionalCoordinate } from "types/api_types"; import type { BucketAddress, Vector3 } from "viewer/constants"; import constants, { MappingStatusEnum } from "viewer/constants"; import { @@ -22,7 +22,7 @@ import type { DataBucket } from "viewer/model/bucket_data_handling/bucket"; import { bucketPositionToGlobalAddress } from "viewer/model/helpers/position_converter"; import type { UpdateActionWithoutIsolationRequirement } from "viewer/model/sagas/volume/update_actions"; import { updateBucket } from "viewer/model/sagas/volume/update_actions"; -import type { DataLayerType, StoreAnnotation, VolumeTracing } from "viewer/store"; +import type { DataLayerType, VolumeTracing } from "viewer/store"; import Store from "viewer/store"; import ByteArraysToLz4Base64Worker from "viewer/workers/byte_arrays_to_lz4_base64.worker"; import { createWorker } from "viewer/workers/comlink_wrapper"; @@ -30,14 +30,6 @@ import DecodeFourBitWorker from "viewer/workers/decode_four_bit.worker"; import { getGlobalDataConnectionInfo } from "../data_connection_info"; import type { MagInfo } from "../helpers/mag_info"; -export type LayerSourceInfo = { - dataset: APIDataset; - annotation: StoreAnnotation | null; - tracingId: string | undefined; - visibleSegmentationLayerName: string | null | undefined; - forceUsingDataStore?: boolean | undefined | null; -}; - const decodeFourBit = createWorker(DecodeFourBitWorker); // For 32-bit buckets with 32^3 voxels, a COMPRESSION_BATCH_SIZE of @@ -273,17 +265,6 @@ export async function requestFromStore( } } -export function getDataOrTracingStoreUrl(layerSourceInfo: LayerSourceInfo) { - const { dataset, annotation, visibleSegmentationLayerName, tracingId, forceUsingDataStore } = - layerSourceInfo; - if (annotation == null || tracingId == null || forceUsingDataStore) { - return `${dataset.dataStore.url}/data/datasets/${dataset.id}/layers/${visibleSegmentationLayerName}`; - } else { - const tracingStoreHost = annotation?.tracingStore.url; - return `${tracingStoreHost}/tracings/volume/${tracingId}`; - } -} - function sliceBufferIntoPieces( layerInfo: DataLayerType, batch: Array, diff --git a/frontend/javascripts/viewer/model/bucket_data_handling/wkstore_helper.ts b/frontend/javascripts/viewer/model/bucket_data_handling/wkstore_helper.ts new file mode 100644 index 00000000000..01117241c99 --- /dev/null +++ b/frontend/javascripts/viewer/model/bucket_data_handling/wkstore_helper.ts @@ -0,0 +1,21 @@ +import type { APIDataset } from "types/api_types"; +import type { StoreAnnotation } from "viewer/store"; + +export type LayerSourceInfo = { + dataset: APIDataset; + annotation: StoreAnnotation | null; + tracingId: string | undefined; + visibleSegmentationLayerName: string | null | undefined; + forceUsingDataStore?: boolean | undefined | null; +}; + +export function getDataOrTracingStoreUrl(layerSourceInfo: LayerSourceInfo) { + const { dataset, annotation, visibleSegmentationLayerName, tracingId, forceUsingDataStore } = + layerSourceInfo; + if (annotation == null || tracingId == null || forceUsingDataStore) { + return `${dataset.dataStore.url}/data/datasets/${dataset.id}/layers/${visibleSegmentationLayerName}`; + } else { + const tracingStoreHost = annotation?.tracingStore.url; + return `${tracingStoreHost}/tracings/volume/${tracingId}`; + } +} diff --git a/frontend/javascripts/viewer/model/sagas/meshes/ad_hoc_mesh_saga.ts b/frontend/javascripts/viewer/model/sagas/meshes/ad_hoc_mesh_saga.ts index a7b973daee7..c4a85380cfe 100644 --- a/frontend/javascripts/viewer/model/sagas/meshes/ad_hoc_mesh_saga.ts +++ b/frontend/javascripts/viewer/model/sagas/meshes/ad_hoc_mesh_saga.ts @@ -42,7 +42,7 @@ import { type LoadAdHocMeshAction, loadPrecomputedMeshAction, } from "viewer/model/actions/segmentation_actions"; -import type { LayerSourceInfo } from "viewer/model/bucket_data_handling/wkstore_adapter"; +import type { LayerSourceInfo } from "viewer/model/bucket_data_handling/wkstore_helper"; import type DataLayer from "viewer/model/data_layer"; import type { MagInfo } from "viewer/model/helpers/mag_info"; import { zoomedAddressToAnotherZoomStepWithInfo } from "viewer/model/helpers/position_converter"; From c5fd2be371f16cd759e276229881c21f1cda1692 Mon Sep 17 00:00:00 2001 From: Charlie Meister Date: Thu, 2 Oct 2025 17:29:51 +0200 Subject: [PATCH 16/19] ensure visible segmentation layer is not null --- .../model/bucket_data_handling/wkstore_helper.ts | 2 +- .../viewer/model/sagas/meshes/ad_hoc_mesh_saga.ts | 15 +++++++++------ frontend/javascripts/viewer/view/context_menu.tsx | 6 +++--- 3 files changed, 13 insertions(+), 10 deletions(-) diff --git a/frontend/javascripts/viewer/model/bucket_data_handling/wkstore_helper.ts b/frontend/javascripts/viewer/model/bucket_data_handling/wkstore_helper.ts index 01117241c99..5762c808ca9 100644 --- a/frontend/javascripts/viewer/model/bucket_data_handling/wkstore_helper.ts +++ b/frontend/javascripts/viewer/model/bucket_data_handling/wkstore_helper.ts @@ -5,7 +5,7 @@ export type LayerSourceInfo = { dataset: APIDataset; annotation: StoreAnnotation | null; tracingId: string | undefined; - visibleSegmentationLayerName: string | null | undefined; + visibleSegmentationLayerName: string; forceUsingDataStore?: boolean | undefined | null; }; diff --git a/frontend/javascripts/viewer/model/sagas/meshes/ad_hoc_mesh_saga.ts b/frontend/javascripts/viewer/model/sagas/meshes/ad_hoc_mesh_saga.ts index c4a85380cfe..6b5a9f065a3 100644 --- a/frontend/javascripts/viewer/model/sagas/meshes/ad_hoc_mesh_saga.ts +++ b/frontend/javascripts/viewer/model/sagas/meshes/ad_hoc_mesh_saga.ts @@ -347,8 +347,13 @@ function* loadFullAdHocMesh( const volumeTracing = yield* select((state) => getActiveSegmentationTracing(state)); const annotation = yield* select((state) => state.annotation); const visibleSegmentationLayer = yield* select((state) => getVisibleSegmentationLayer(state)); + if (visibleSegmentationLayer == null) { + throw new Error( + "Loading the ad-hoc mesh failed because the visible segmentation layer must not be null.", + ); + } // Fetch from datastore if no volumetracing ... - let forceUsingDataStore = volumeTracing == null || visibleSegmentationLayer?.tracingId == null; + let forceUsingDataStore = volumeTracing == null || visibleSegmentationLayer.tracingId == null; if (meshExtraInfo.useDataStore != null) { // ... except if the caller specified whether to use the data store ... forceUsingDataStore = meshExtraInfo.useDataStore; @@ -364,17 +369,15 @@ function* loadFullAdHocMesh( volumeTracing, dataset, layer.name, - visibleSegmentationLayer?.tracingId, + visibleSegmentationLayer.tracingId, ); const layerSourceInfo: LayerSourceInfo = { dataset, annotation, - tracingId: visibleSegmentationLayer?.tracingId, + tracingId: visibleSegmentationLayer.tracingId, visibleSegmentationLayerName: - visibleSegmentationLayer?.tracingId == null - ? visibleSegmentationLayer?.name - : visibleSegmentationLayer?.fallbackLayer, + visibleSegmentationLayer.fallbackLayer ?? visibleSegmentationLayer.name, forceUsingDataStore, }; diff --git a/frontend/javascripts/viewer/view/context_menu.tsx b/frontend/javascripts/viewer/view/context_menu.tsx index 1866cca1fd2..4bc126a125e 100644 --- a/frontend/javascripts/viewer/view/context_menu.tsx +++ b/frontend/javascripts/viewer/view/context_menu.tsx @@ -1687,7 +1687,7 @@ function ContextMenuInner() { if (visibleSegmentationLayer == null || !isSegmentIndexAvailable) return []; const tracingId = volumeTracing?.tracingId; const additionalCoordinates = flycam.additionalCoordinates; - const infoForRequestUrl = { + const layerSourceInfo = { dataset, annotation, tracingId, @@ -1699,14 +1699,14 @@ function ContextMenuInner() { try { const [segmentSize] = await getSegmentVolumes( - infoForRequestUrl, + layerSourceInfo, layersFinestMag, [clickedSegmentOrMeshId], additionalCoordinates, mappingName, ); const [boundingBoxInRequestedMag] = await getSegmentBoundingBoxes( - infoForRequestUrl, + layerSourceInfo, layersFinestMag, [clickedSegmentOrMeshId], additionalCoordinates, From a459350a080938490fe0da0f3a34ae12c2292ee6 Mon Sep 17 00:00:00 2001 From: Charlie Meister Date: Tue, 7 Oct 2025 17:53:38 +0200 Subject: [PATCH 17/19] rename props of LayerSourceInfo --- .../model/bucket_data_handling/wkstore_helper.ts | 11 +++++------ .../viewer/model/sagas/meshes/ad_hoc_mesh_saga.ts | 7 +++---- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/frontend/javascripts/viewer/model/bucket_data_handling/wkstore_helper.ts b/frontend/javascripts/viewer/model/bucket_data_handling/wkstore_helper.ts index 5762c808ca9..0dc63ac3257 100644 --- a/frontend/javascripts/viewer/model/bucket_data_handling/wkstore_helper.ts +++ b/frontend/javascripts/viewer/model/bucket_data_handling/wkstore_helper.ts @@ -5,15 +5,14 @@ export type LayerSourceInfo = { dataset: APIDataset; annotation: StoreAnnotation | null; tracingId: string | undefined; - visibleSegmentationLayerName: string; - forceUsingDataStore?: boolean | undefined | null; + segmentationLayerName: string; + useDataStore?: boolean | undefined | null; }; export function getDataOrTracingStoreUrl(layerSourceInfo: LayerSourceInfo) { - const { dataset, annotation, visibleSegmentationLayerName, tracingId, forceUsingDataStore } = - layerSourceInfo; - if (annotation == null || tracingId == null || forceUsingDataStore) { - return `${dataset.dataStore.url}/data/datasets/${dataset.id}/layers/${visibleSegmentationLayerName}`; + const { dataset, annotation, segmentationLayerName, tracingId, useDataStore } = layerSourceInfo; + if (annotation == null || tracingId == null || useDataStore) { + return `${dataset.dataStore.url}/data/datasets/${dataset.id}/layers/${segmentationLayerName}`; } else { const tracingStoreHost = annotation?.tracingStore.url; return `${tracingStoreHost}/tracings/volume/${tracingId}`; diff --git a/frontend/javascripts/viewer/model/sagas/meshes/ad_hoc_mesh_saga.ts b/frontend/javascripts/viewer/model/sagas/meshes/ad_hoc_mesh_saga.ts index 6b5a9f065a3..7b5e558e98e 100644 --- a/frontend/javascripts/viewer/model/sagas/meshes/ad_hoc_mesh_saga.ts +++ b/frontend/javascripts/viewer/model/sagas/meshes/ad_hoc_mesh_saga.ts @@ -376,9 +376,8 @@ function* loadFullAdHocMesh( dataset, annotation, tracingId: visibleSegmentationLayer.tracingId, - visibleSegmentationLayerName: - visibleSegmentationLayer.fallbackLayer ?? visibleSegmentationLayer.name, - forceUsingDataStore, + segmentationLayerName: visibleSegmentationLayer.fallbackLayer ?? visibleSegmentationLayer.name, + useDataStore: forceUsingDataStore, }; let positionsToRequest = usePositionsFromSegmentIndex @@ -488,7 +487,7 @@ function* maybeLoadMeshChunk( if (isInitialRequest) { sendAnalyticsEvent("request_isosurface", { - mode: layerSourceInfo.forceUsingDataStore ? "view" : "annotation", + mode: layerSourceInfo.useDataStore ? "view" : "annotation", }); } From 189617ff145f9f39000d1da6025a605224736360 Mon Sep 17 00:00:00 2001 From: Charlie Meister Date: Tue, 7 Oct 2025 20:42:55 +0200 Subject: [PATCH 18/19] fix usage of layerSourceInfo --- frontend/javascripts/viewer/view/context_menu.tsx | 2 +- .../right-border-tabs/segments_tab/segment_statistics_modal.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/javascripts/viewer/view/context_menu.tsx b/frontend/javascripts/viewer/view/context_menu.tsx index 4bc126a125e..66028d508b3 100644 --- a/frontend/javascripts/viewer/view/context_menu.tsx +++ b/frontend/javascripts/viewer/view/context_menu.tsx @@ -1691,7 +1691,7 @@ function ContextMenuInner() { dataset, annotation, tracingId, - visibleSegmentationLayerName: visibleSegmentationLayer.name, + segmentationLayerName: visibleSegmentationLayer.name, }; const magInfo = getMagInfo(visibleSegmentationLayer.resolutions); const layersFinestMag = magInfo.getFinestMag(); diff --git a/frontend/javascripts/viewer/view/right-border-tabs/segments_tab/segment_statistics_modal.tsx b/frontend/javascripts/viewer/view/right-border-tabs/segments_tab/segment_statistics_modal.tsx index c7992e0b784..14c119625af 100644 --- a/frontend/javascripts/viewer/view/right-border-tabs/segments_tab/segment_statistics_modal.tsx +++ b/frontend/javascripts/viewer/view/right-border-tabs/segments_tab/segment_statistics_modal.tsx @@ -104,7 +104,7 @@ export function SegmentStatisticsModal({ dataset, annotation, tracingId: visibleSegmentationLayer.tracingId, - visibleSegmentationLayerName: visibleSegmentationLayer.name, + segmentationLayerName: visibleSegmentationLayer.name, }; const additionalCoordinates = useWkSelector((state) => state.flycam.additionalCoordinates); const hasAdditionalCoords = hasAdditionalCoordinates(additionalCoordinates); From 1765442382dd7f8f6e21938b7ff505d830598b0a Mon Sep 17 00:00:00 2001 From: Charlie Meister Date: Wed, 8 Oct 2025 10:29:34 +0200 Subject: [PATCH 19/19] lint --- .../javascripts/viewer/view/context_menu.tsx | 732 +++++++++--------- 1 file changed, 366 insertions(+), 366 deletions(-) diff --git a/frontend/javascripts/viewer/view/context_menu.tsx b/frontend/javascripts/viewer/view/context_menu.tsx index af2b81aace6..16b8ce280f6 100644 --- a/frontend/javascripts/viewer/view/context_menu.tsx +++ b/frontend/javascripts/viewer/view/context_menu.tsx @@ -435,12 +435,12 @@ function getMultiCutToolOptions( }, ...(doBothPartitionsHaveEntries ? [ - { - key: "min-cut-agglomerate-with-partitions", - onClick: () => Store.dispatch(minCutPartitionsAction()), - label: "Split partitions", - }, - ] + { + key: "min-cut-agglomerate-with-partitions", + onClick: () => Store.dispatch(minCutPartitionsAction()), + label: "Split partitions", + }, + ] : []), ]; } @@ -514,71 +514,71 @@ function getMeshItems( const proofreadingMultiSplitToolActions = isProofreadingActive && isMultiSplitActive && minCutPartitions && maybeUnmappedSegmentId != null ? getMultiCutToolOptions( - maybeUnmappedSegmentId, - clickedMeshId, - minCutPartitions, - segmentOrSuperVoxel, - segmentIdLabel, - ) + maybeUnmappedSegmentId, + clickedMeshId, + minCutPartitions, + segmentOrSuperVoxel, + segmentIdLabel, + ) : []; const maybeProofreadingItems: MenuItemType[] = isProofreadingActive ? [ - ...proofreadingMultiSplitToolActions, - { - key: "merge-agglomerate-skeleton", - disabled: shouldAgglomerateSkeletonActionsBeDisabled || clickedMeshId === activeCellId, - onClick: () => { - if (maybeUnmappedSegmentId == null) { - // Should not happen due to the disabled property. - return; - } - return Store.dispatch( - proofreadMergeAction(null, maybeUnmappedSegmentId, clickedMeshId), - ); + ...proofreadingMultiSplitToolActions, + { + key: "merge-agglomerate-skeleton", + disabled: shouldAgglomerateSkeletonActionsBeDisabled || clickedMeshId === activeCellId, + onClick: () => { + if (maybeUnmappedSegmentId == null) { + // Should not happen due to the disabled property. + return; + } + return Store.dispatch( + proofreadMergeAction(null, maybeUnmappedSegmentId, clickedMeshId), + ); + }, + label: ( + Merge with active segment + ), }, - label: ( - Merge with active segment - ), - }, - { - key: "min-cut-agglomerate-at-position", - disabled: - shouldAgglomerateSkeletonActionsBeDisabled || - clickedMeshId !== activeCellId || - activeUnmappedSegmentId == null || - maybeUnmappedSegmentId === activeUnmappedSegmentId, - onClick: () => { - if (maybeUnmappedSegmentId == null) { - // Should not happen due to the disabled property. - return; - } - Store.dispatch( - minCutAgglomerateWithPositionAction(null, maybeUnmappedSegmentId, clickedMeshId), - ); + { + key: "min-cut-agglomerate-at-position", + disabled: + shouldAgglomerateSkeletonActionsBeDisabled || + clickedMeshId !== activeCellId || + activeUnmappedSegmentId == null || + maybeUnmappedSegmentId === activeUnmappedSegmentId, + onClick: () => { + if (maybeUnmappedSegmentId == null) { + // Should not happen due to the disabled property. + return; + } + Store.dispatch( + minCutAgglomerateWithPositionAction(null, maybeUnmappedSegmentId, clickedMeshId), + ); + }, + label: ( + Split from active segment + ), }, - label: ( - Split from active segment - ), - }, - { - key: "split-from-all-neighbors", - disabled: maybeUnmappedSegmentId == null || meshFileMappingName != null, - onClick: () => { - if (maybeUnmappedSegmentId == null) { - // Should not happen due to the disabled property. - return; - } - Store.dispatch( - cutAgglomerateFromNeighborsAction(null, null, maybeUnmappedSegmentId, clickedMeshId), - ); + { + key: "split-from-all-neighbors", + disabled: maybeUnmappedSegmentId == null || meshFileMappingName != null, + onClick: () => { + if (maybeUnmappedSegmentId == null) { + // Should not happen due to the disabled property. + return; + } + Store.dispatch( + cutAgglomerateFromNeighborsAction(null, null, maybeUnmappedSegmentId, clickedMeshId), + ); + }, + label: ( + + Split from all neighboring segments + + ), }, - label: ( - - Split from all neighboring segments - - ), - }, - ] + ] : []; const isAlreadySelected = @@ -586,21 +586,21 @@ function getMeshItems( return [ isProofreadingActive && activeUnmappedSegmentId != null && isAlreadySelected ? { - // If a supervoxel is selected (and thus highlighted), allow to select it. - key: "deactivate-segment", - onClick: () => - Store.dispatch(setActiveCellAction(clickedMeshId, undefined, undefined, undefined)), - label: `Deselect ${segmentOrSuperVoxel} (${segmentIdLabel})`, - } + // If a supervoxel is selected (and thus highlighted), allow to select it. + key: "deactivate-segment", + onClick: () => + Store.dispatch(setActiveCellAction(clickedMeshId, undefined, undefined, undefined)), + label: `Deselect ${segmentOrSuperVoxel} (${segmentIdLabel})`, + } : { - key: "activate-segment", - onClick: () => - Store.dispatch( - setActiveCellAction(clickedMeshId, undefined, undefined, maybeUnmappedSegmentId), - ), - disabled: isAlreadySelected, - label: `Select ${segmentOrSuperVoxel} (${segmentIdLabel})`, - }, + key: "activate-segment", + onClick: () => + Store.dispatch( + setActiveCellAction(clickedMeshId, undefined, undefined, maybeUnmappedSegmentId), + ), + disabled: isAlreadySelected, + label: `Select ${segmentOrSuperVoxel} (${segmentIdLabel})`, + }, { key: "hide-mesh", onClick: () => Actions.hideMesh(Store.dispatch, visibleSegmentationLayer.name, clickedMeshId), @@ -705,129 +705,129 @@ function getNodeContextMenuOptions({ getMaybeMinCutItem(clickedTree, volumeTracing, userBoundingBoxes, isVolumeModificationAllowed), ...(allowUpdate ? [ - { - key: "merge-trees", - disabled: areInSameTree, - onClick: () => - activeNodeId != null - ? Store.dispatch(mergeTreesAction(activeNodeId, clickedNodeId)) - : null, - label: ( - <> - Create Edge & Merge with this Tree{" "} - {useLegacyBindings ? shortcutBuilder(["Shift", AltOrOptionKey, "leftMouse"]) : null} - - ), - }, - isProofreadingActive - ? { - key: "min-cut-node", - disabled: !areInSameTree || isTheSameNode, + { + key: "merge-trees", + disabled: areInSameTree, onClick: () => activeNodeId != null - ? Store.dispatch(minCutAgglomerateAction(clickedNodeId, activeNodeId)) + ? Store.dispatch(mergeTreesAction(activeNodeId, clickedNodeId)) : null, - label: "Perform Min-Cut between these Nodes", - } - : null, - - isProofreadingActive - ? { - key: "cut-agglomerate-from-neighbors", - disabled: !isProofreadingActive, - onClick: () => - Store.dispatch( - cutAgglomerateFromNeighborsAction( - clickedNode.untransformedPosition, - clickedTree, - ), - ), label: ( - - Split from all neighboring segments - + <> + Create Edge & Merge with this Tree{" "} + {useLegacyBindings ? shortcutBuilder(["Shift", AltOrOptionKey, "leftMouse"]) : null} + ), - } - : null, + }, + isProofreadingActive + ? { + key: "min-cut-node", + disabled: !areInSameTree || isTheSameNode, + onClick: () => + activeNodeId != null + ? Store.dispatch(minCutAgglomerateAction(clickedNodeId, activeNodeId)) + : null, + label: "Perform Min-Cut between these Nodes", + } + : null, - { - key: "delete-edge", - disabled: !areNodesConnected, - onClick: () => - activeNodeId != null - ? Store.dispatch(deleteEdgeAction(activeNodeId, clickedNodeId)) - : null, - label: ( - <> - Delete Edge to this Node{" "} - {useLegacyBindings ? shortcutBuilder(["Shift", CtrlOrCmdKey, "leftMouse"]) : null} - - ), - }, - { - key: "delete-node", - onClick: () => Actions.deleteNode(Store.dispatch, clickedNodeId, clickedTree.treeId), - label: ( - <> - Delete this Node {activeNodeId === clickedNodeId ? shortcutBuilder(["Del"]) : null} - - ), - }, - isBranchpoint - ? { - key: "branchpoint-node", - className: "node-context-menu-item", - onClick: () => - activeNodeId != null - ? Store.dispatch(deleteBranchpointByIdAction(clickedNodeId, clickedTree.treeId)) - : null, - label: "Unmark as Branchpoint", - } - : { - key: "branchpoint-node", - className: "node-context-menu-item", + isProofreadingActive + ? { + key: "cut-agglomerate-from-neighbors", + disabled: !isProofreadingActive, + onClick: () => + Store.dispatch( + cutAgglomerateFromNeighborsAction( + clickedNode.untransformedPosition, + clickedTree, + ), + ), + label: ( + + Split from all neighboring segments + + ), + } + : null, + + { + key: "delete-edge", + disabled: !areNodesConnected, onClick: () => activeNodeId != null - ? Store.dispatch(createBranchPointAction(clickedNodeId, clickedTree.treeId)) + ? Store.dispatch(deleteEdgeAction(activeNodeId, clickedNodeId)) : null, label: ( <> - Mark as Branchpoint{" "} - {activeNodeId === clickedNodeId ? shortcutBuilder(["B"]) : null} + Delete Edge to this Node{" "} + {useLegacyBindings ? shortcutBuilder(["Shift", CtrlOrCmdKey, "leftMouse"]) : null} ), }, - isTheSameNode - ? null - : { - key: "extract-shortest-path", - disabled: activeNodeId == null || !areInSameTree || isTheSameNode, - onClick: () => - activeNodeId != null - ? extractShortestPathAsNewTree(clickedTree, activeNodeId, clickedNodeId) - : null, - label: "Extract shortest Path to this Node", + { + key: "delete-node", + onClick: () => Actions.deleteNode(Store.dispatch, clickedNodeId, clickedTree.treeId), + label: ( + <> + Delete this Node {activeNodeId === clickedNodeId ? shortcutBuilder(["Del"]) : null} + + ), }, - ] + isBranchpoint + ? { + key: "branchpoint-node", + className: "node-context-menu-item", + onClick: () => + activeNodeId != null + ? Store.dispatch(deleteBranchpointByIdAction(clickedNodeId, clickedTree.treeId)) + : null, + label: "Unmark as Branchpoint", + } + : { + key: "branchpoint-node", + className: "node-context-menu-item", + onClick: () => + activeNodeId != null + ? Store.dispatch(createBranchPointAction(clickedNodeId, clickedTree.treeId)) + : null, + label: ( + <> + Mark as Branchpoint{" "} + {activeNodeId === clickedNodeId ? shortcutBuilder(["B"]) : null} + + ), + }, + isTheSameNode + ? null + : { + key: "extract-shortest-path", + disabled: activeNodeId == null || !areInSameTree || isTheSameNode, + onClick: () => + activeNodeId != null + ? extractShortestPathAsNewTree(clickedTree, activeNodeId, clickedNodeId) + : null, + label: "Extract shortest Path to this Node", + }, + ] : []), ...meshItems, isTheSameNode ? null : { - key: "measure-node-path-length", - disabled: activeNodeId == null || !areInSameTree || isTheSameNode, - onClick: () => - activeNodeId != null - ? measureAndShowLengthBetweenNodes(activeNodeId, clickedNodeId, voxelSize.unit) - : null, - label: "Path Length to this Node", - }, + key: "measure-node-path-length", + disabled: activeNodeId == null || !areInSameTree || isTheSameNode, + onClick: () => + activeNodeId != null + ? measureAndShowLengthBetweenNodes(activeNodeId, clickedNodeId, voxelSize.unit) + : null, + label: "Path Length to this Node", + }, { key: "measure-whole-tree-length", onClick: () => @@ -836,10 +836,10 @@ function getNodeContextMenuOptions({ }, allowUpdate ? { - key: "hide-tree", - onClick: () => Store.dispatch(setTreeVisibilityAction(clickedTree.treeId, false)), - label: "Hide this Tree", - } + key: "hide-tree", + onClick: () => Store.dispatch(setTreeVisibilityAction(clickedTree.treeId, false)), + label: "Hide this Tree", + } : null, ...infoRows, ]; @@ -1169,7 +1169,7 @@ function getNoNodeContextMenuOptions(props: NoNodeContextMenuProps): ItemType[] ), - onOk() { }, + onOk() {}, }); const isVolumeBasedToolActive = VolumeTools.includes(activeTool); @@ -1179,136 +1179,136 @@ function getNoNodeContextMenuOptions(props: NoNodeContextMenuProps): ItemType[] : undefined; const skeletonActions: ItemType[] = skeletonTracing != null && - globalPosition != null && - globalPositionForNode != null && - allowUpdate + globalPosition != null && + globalPositionForNode != null && + allowUpdate ? [ - { - key: "create-node", - onClick: () => - handleCreateNodeFromGlobalPosition(globalPositionForNode, viewport, false), - label: "Create Node here", - disabled: areGeometriesTransformed(state), - }, - { - key: "create-node-with-tree", - onClick: () => { - Store.dispatch(createTreeAction()); - handleCreateNodeFromGlobalPosition(globalPositionForNode, viewport, false); + { + key: "create-node", + onClick: () => + handleCreateNodeFromGlobalPosition(globalPositionForNode, viewport, false), + label: "Create Node here", + disabled: areGeometriesTransformed(state), }, - label: ( - <> - Create new Tree here{" "} - {!isVolumeBasedToolActive && !isBoundingBoxToolActive - ? shortcutBuilder(["C"]) - : null} - - ), - disabled: areGeometriesTransformed(state), - }, - { - key: "load-agglomerate-skeleton", - // Do not disable menu entry, but show modal advertising automated segmentation services if no agglomerate file is activated - onClick: () => - isAgglomerateMappingEnabled.value - ? loadAgglomerateSkeletonAtPosition(globalPosition) - : showAutomatedSegmentationServicesModal( - isAgglomerateMappingEnabled.reason, - "Agglomerate files", - ), - label: ( - { - Store.dispatch(ensureLayerMappingsAreLoadedAction()); - }} - > - - Import Agglomerate Skeleton{" "} - {!isAgglomerateMappingEnabled.value ? ( - - ) : null}{" "} - {shortcutBuilder(["Shift", "middleMouse"])} - - - ), - }, - - ...(isProofreadingActive && - isMultiSplitActive && - maybeMinCutPartitions && - maybeUnmappedSegmentId - ? getMultiCutToolOptions( - maybeUnmappedSegmentId, - segmentIdAtPosition, - maybeMinCutPartitions, - segmentOrSuperVoxel, - segmentIdLabel, - ) - : []), - isAgglomerateMappingEnabled.value - ? { - key: "merge-agglomerate-skeleton", - disabled: !isProofreadingActive, - onClick: () => Store.dispatch(proofreadMergeAction(globalPosition)), + { + key: "create-node-with-tree", + onClick: () => { + Store.dispatch(createTreeAction()); + handleCreateNodeFromGlobalPosition(globalPositionForNode, viewport, false); + }, label: ( - - - Merge with active segment{" "} - {isMultiSplitActive ? "" : shortcutBuilder(["Shift", "leftMouse"])} - - + <> + Create new Tree here{" "} + {!isVolumeBasedToolActive && !isBoundingBoxToolActive + ? shortcutBuilder(["C"]) + : null} + ), - } - : null, - isAgglomerateMappingEnabled.value - ? { - key: "min-cut-agglomerate-at-position", - disabled: !isProofreadingActive, - onClick: () => Store.dispatch(minCutAgglomerateWithPositionAction(globalPosition)), + disabled: areGeometriesTransformed(state), + }, + { + key: "load-agglomerate-skeleton", + // Do not disable menu entry, but show modal advertising automated segmentation services if no agglomerate file is activated + onClick: () => + isAgglomerateMappingEnabled.value + ? loadAgglomerateSkeletonAtPosition(globalPosition) + : showAutomatedSegmentationServicesModal( + isAgglomerateMappingEnabled.reason, + "Agglomerate files", + ), label: ( { + Store.dispatch(ensureLayerMappingsAreLoadedAction()); + }} > - Split from active segment{" "} - {isMultiSplitActive ? "" : shortcutBuilder([CtrlOrCmdKey, "leftMouse"])} + Import Agglomerate Skeleton{" "} + {!isAgglomerateMappingEnabled.value ? ( + + ) : null}{" "} + {shortcutBuilder(["Shift", "middleMouse"])} ), - } - : null, - isAgglomerateMappingEnabled.value - ? { - key: "cut-agglomerate-from-neighbors", - disabled: !isProofreadingActive, - onClick: () => Store.dispatch(cutAgglomerateFromNeighborsAction(globalPosition)), - label: ( - - Split from all neighboring segments - - ), - } - : null, - ] + }, + + ...(isProofreadingActive && + isMultiSplitActive && + maybeMinCutPartitions && + maybeUnmappedSegmentId + ? getMultiCutToolOptions( + maybeUnmappedSegmentId, + segmentIdAtPosition, + maybeMinCutPartitions, + segmentOrSuperVoxel, + segmentIdLabel, + ) + : []), + isAgglomerateMappingEnabled.value + ? { + key: "merge-agglomerate-skeleton", + disabled: !isProofreadingActive, + onClick: () => Store.dispatch(proofreadMergeAction(globalPosition)), + label: ( + + + Merge with active segment{" "} + {isMultiSplitActive ? "" : shortcutBuilder(["Shift", "leftMouse"])} + + + ), + } + : null, + isAgglomerateMappingEnabled.value + ? { + key: "min-cut-agglomerate-at-position", + disabled: !isProofreadingActive, + onClick: () => Store.dispatch(minCutAgglomerateWithPositionAction(globalPosition)), + label: ( + + + Split from active segment{" "} + {isMultiSplitActive ? "" : shortcutBuilder([CtrlOrCmdKey, "leftMouse"])} + + + ), + } + : null, + isAgglomerateMappingEnabled.value + ? { + key: "cut-agglomerate-from-neighbors", + disabled: !isProofreadingActive, + onClick: () => Store.dispatch(cutAgglomerateFromNeighborsAction(globalPosition)), + label: ( + + Split from all neighboring segments + + ), + } + : null, + ] : []; const segmentationLayerName = visibleSegmentationLayer != null ? visibleSegmentationLayer.name : null; @@ -1322,17 +1322,17 @@ function getNoNodeContextMenuOptions(props: NoNodeContextMenuProps): ItemType[] // Do not disable menu entry, but show modal advertising automated segmentation services if no connectome file is activated onClick: isConnectomeMappingEnabled.value ? withMappingActivationConfirmation( - () => loadSynapsesOfAgglomerateAtPosition(globalPosition), - connectomeFileMappingName, - "connectome file", - segmentationLayerName, - mappingInfo, - ) + () => loadSynapsesOfAgglomerateAtPosition(globalPosition), + connectomeFileMappingName, + "connectome file", + segmentationLayerName, + mappingInfo, + ) : () => - showAutomatedSegmentationServicesModal( - isConnectomeMappingEnabled.reason, - "Connectome files", - ), + showAutomatedSegmentationServicesModal( + isConnectomeMappingEnabled.reason, + "Connectome files", + ), label: isConnectomeMappingEnabled.value ? ( "Import Synapses" ) : ( @@ -1391,41 +1391,41 @@ function getNoNodeContextMenuOptions(props: NoNodeContextMenuProps): ItemType[] const nonSkeletonActions: ItemType[] = globalPosition != null && visibleSegmentationLayer != null ? [ - // Segment 0 cannot/shouldn't be made active (as this - // would be an eraser effectively). - segmentIdAtPosition > 0 && !disabledVolumeInfo.PICK_CELL.isDisabled - ? { - key: "select-cell", - onClick: () => { - Store.dispatch( - setActiveCellAction(segmentIdAtPosition, globalPosition, additionalCoordinates), - ); - }, - disabled: - volumeTracing == null || // satisfy TS - segmentIdAtPosition === getActiveCellId(volumeTracing), - label: ( - <> - Activate Segment ({segmentIdAtPosition}){" "} - {isVolumeBasedToolActive ? shortcutBuilder(["Shift", "leftMouse"]) : null} - - ), - } - : null, - segmentIdAtPosition > 0 ? onlyShowThisSegmentItem : null, - segmentIdAtPosition > 0 ? toggleSegmentVisibilityItem : null, - segmentIdAtPosition > 0 ? showAllSegmentsItem : null, - focusInSegmentListItem, - loadPrecomputedMeshItem, - computeMeshAdHocItem, - allowUpdate && !disabledVolumeInfo.FILL_CELL.isDisabled - ? { - key: "fill-cell", - onClick: () => handleFloodFillFromGlobalPosition(globalPosition, viewport), - label: "Fill Segment (flood-fill region)", - } - : null, - ] + // Segment 0 cannot/shouldn't be made active (as this + // would be an eraser effectively). + segmentIdAtPosition > 0 && !disabledVolumeInfo.PICK_CELL.isDisabled + ? { + key: "select-cell", + onClick: () => { + Store.dispatch( + setActiveCellAction(segmentIdAtPosition, globalPosition, additionalCoordinates), + ); + }, + disabled: + volumeTracing == null || // satisfy TS + segmentIdAtPosition === getActiveCellId(volumeTracing), + label: ( + <> + Activate Segment ({segmentIdAtPosition}){" "} + {isVolumeBasedToolActive ? shortcutBuilder(["Shift", "leftMouse"]) : null} + + ), + } + : null, + segmentIdAtPosition > 0 ? onlyShowThisSegmentItem : null, + segmentIdAtPosition > 0 ? toggleSegmentVisibilityItem : null, + segmentIdAtPosition > 0 ? showAllSegmentsItem : null, + focusInSegmentListItem, + loadPrecomputedMeshItem, + computeMeshAdHocItem, + allowUpdate && !disabledVolumeInfo.FILL_CELL.isDisabled + ? { + key: "fill-cell", + onClick: () => handleFloodFillFromGlobalPosition(globalPosition, viewport), + label: "Fill Segment (flood-fill region)", + } + : null, + ] : []; const boundingBoxActions = getBoundingBoxMenuOptions(props); @@ -1596,7 +1596,7 @@ function ContextMenuInner() { const currentConnectomeFile = useWkSelector((state) => visibleSegmentationLayer != null ? state.localSegmentationData[visibleSegmentationLayer.name].connectomeData - .currentConnectomeFile + .currentConnectomeFile : null, ); @@ -1767,12 +1767,12 @@ function ContextMenuInner() { const distanceToSelection = activeNode != null && positionToMeasureDistanceTo != null ? [ - formatNumberToLength( - V3.scaledDist(getActiveNodePosition(), positionToMeasureDistanceTo, voxelSize.factor), - LongUnitToShortUnitMap[voxelSize.unit], - ), - formatLengthAsVx(V3.length(V3.sub(getActiveNodePosition(), positionToMeasureDistanceTo))), - ] + formatNumberToLength( + V3.scaledDist(getActiveNodePosition(), positionToMeasureDistanceTo, voxelSize.factor), + LongUnitToShortUnitMap[voxelSize.unit], + ), + formatLengthAsVx(V3.length(V3.sub(getActiveNodePosition(), positionToMeasureDistanceTo))), + ] : null; const nodePositionAsString = nodeContextMenuNode != null && clickedNodesPosition != null @@ -1935,17 +1935,17 @@ function ContextMenuInner() { ? [] : maybeClickedNodeId != null ? getNodeContextMenuOptions({ - clickedNodeId: maybeClickedNodeId, - infoRows, - viewport: maybeViewport, - ...props, - }) + clickedNodeId: maybeClickedNodeId, + infoRows, + viewport: maybeViewport, + ...props, + }) : getNoNodeContextMenuOptions({ - segmentIdAtPosition, - infoRows, - viewport: maybeViewport, - ...props, - }), + segmentIdAtPosition, + infoRows, + viewport: maybeViewport, + ...props, + }), }; if (inputRef == null || inputRef.current == null) return null;