diff --git a/CHANGELOG.unreleased.md b/CHANGELOG.unreleased.md index 8c418a35341..d91d9ad733b 100644 --- a/CHANGELOG.unreleased.md +++ b/CHANGELOG.unreleased.md @@ -23,6 +23,7 @@ For upgrade instructions, please check the [migration guide](MIGRATIONS.released ### Fixed - Fixed a bug where a toast that was reopened had a flickering effect during the reopening animation. [#7793](https://github.com/scalableminds/webknossos/pull/7793) - Fixed a bug where some annotation times would be shown double. [#7787](https://github.com/scalableminds/webknossos/pull/7787) +- Fixed a bug where ad-hoc meshes for coarse magnifications would have gaps. [#7799](https://github.com/scalableminds/webknossos/pull/7799) ### Removed diff --git a/frontend/javascripts/admin/admin_rest_api.ts b/frontend/javascripts/admin/admin_rest_api.ts index 5362294a0d6..6c86320b3eb 100644 --- a/frontend/javascripts/admin/admin_rest_api.ts +++ b/frontend/javascripts/admin/admin_rest_api.ts @@ -2302,6 +2302,7 @@ export function computeAdHocMesh( additionalCoordinates, cubeSize, mappingName, + mag, ...rest } = meshRequest; @@ -2317,11 +2318,12 @@ export function computeAdHocMesh( // The back-end needs a small padding at the border of the // bounding box to calculate the mesh. This padding // is added here to the position and bbox size. - position: V3.toArray(V3.sub(position, [1, 1, 1])), + position: V3.toArray(V3.sub(position, mag)), // position is in mag1 additionalCoordinates, - cubeSize: V3.toArray(V3.add(cubeSize, [1, 1, 1])), + cubeSize: V3.toArray(V3.add(cubeSize, [1, 1, 1])), //cubeSize is in target mag // Name and type of mapping to apply before building mesh (optional) mapping: mappingName, + mag, ...rest, }, }, diff --git a/frontend/javascripts/oxalis/model/sagas/mesh_saga.ts b/frontend/javascripts/oxalis/model/sagas/mesh_saga.ts index 0e5851301f3..138c43dfd61 100644 --- a/frontend/javascripts/oxalis/model/sagas/mesh_saga.ts +++ b/frontend/javascripts/oxalis/model/sagas/mesh_saga.ts @@ -104,10 +104,10 @@ const MESH_CHUNK_THROTTLE_LIMIT = 50; // Maps from additional coordinates, layerName and segmentId to a ThreeDMap that stores for each chunk // (at x, y, z) position whether the mesh chunk was loaded. const adhocMeshesMapByLayer: Record>>> = {}; -function marchingCubeSizeInMag1(): Vector3 { - return (window as any).__marchingCubeSizeInMag1 != null - ? (window as any).__marchingCubeSizeInMag1 - : [128, 128, 128]; +function marchingCubeSizeInTargetMag(): Vector3 { + return (window as any).__marchingCubeSizeInTargetMag != null + ? (window as any).__marchingCubeSizeInTargetMag + : [64, 64, 64]; } const modifiedCells: Set = new Set(); export function isMeshSTL(buffer: ArrayBuffer): boolean { @@ -155,20 +155,25 @@ function removeMapForSegment( adhocMeshesMapByLayer[additionalCoordinateKey][layerName].delete(segmentId); } -function getZoomedCubeSize(zoomStep: number, resolutionInfo: ResolutionInfo): Vector3 { - // Convert marchingCubeSizeInMag1 to another resolution (zoomStep) +function getCubeSizeInMag1(zoomStep: number, resolutionInfo: ResolutionInfo): Vector3 { + // Convert marchingCubeSizeInTargetMag to mag1 via zoomStep + // Drop the last element of the Vector4; const [x, y, z] = zoomedAddressToAnotherZoomStepWithInfo( - [...marchingCubeSizeInMag1(), 0], + [...marchingCubeSizeInTargetMag(), zoomStep], resolutionInfo, - zoomStep, + 0, ); - // Drop the last element of the Vector4; return [x, y, z]; } -function clipPositionToCubeBoundary(position: Vector3): Vector3 { - const currentCube = V3.floor(V3.divide3(position, marchingCubeSizeInMag1())); - const clippedPosition = V3.scale3(currentCube, marchingCubeSizeInMag1()); +function clipPositionToCubeBoundary( + position: Vector3, + zoomStep: number, + resolutionInfo: ResolutionInfo, +): Vector3 { + const cubeSizeInMag1 = getCubeSizeInMag1(zoomStep, resolutionInfo); + const currentCube = V3.floor(V3.divide3(position, cubeSizeInMag1)); + const clippedPosition = V3.scale3(currentCube, cubeSizeInMag1); return clippedPosition; } @@ -182,12 +187,18 @@ const NEIGHBOR_LOOKUP = [ [1, 0, 0], ]; -function getNeighborPosition(clippedPosition: Vector3, neighborId: number): Vector3 { +function getNeighborPosition( + clippedPosition: Vector3, + neighborId: number, + zoomStep: number, + resolutionInfo: ResolutionInfo, +): Vector3 { const neighborMultiplier = NEIGHBOR_LOOKUP[neighborId]; + const cubeSizeInMag1 = getCubeSizeInMag1(zoomStep, resolutionInfo); const neighboringPosition: Vector3 = [ - clippedPosition[0] + neighborMultiplier[0] * marchingCubeSizeInMag1()[0], - clippedPosition[1] + neighborMultiplier[1] * marchingCubeSizeInMag1()[1], - clippedPosition[2] + neighborMultiplier[2] * marchingCubeSizeInMag1()[2], + clippedPosition[0] + neighborMultiplier[0] * cubeSizeInMag1[0], + clippedPosition[1] + neighborMultiplier[1] * cubeSizeInMag1[1], + clippedPosition[2] + neighborMultiplier[2] * cubeSizeInMag1[2], ]; return neighboringPosition; } @@ -316,7 +327,7 @@ function* loadFullAdHocMesh( ): Saga { let isInitialRequest = true; const { mappingName, mappingType } = meshExtraInfo; - const clippedPosition = clipPositionToCubeBoundary(position); + const clippedPosition = clipPositionToCubeBoundary(position, zoomStep, resolutionInfo); yield* put( addAdHocMeshAction( layer.name, @@ -329,7 +340,7 @@ function* loadFullAdHocMesh( ); yield* put(startedLoadingMeshAction(layer.name, segmentId)); - const cubeSize = getZoomedCubeSize(zoomStep, resolutionInfo); + const cubeSize = marchingCubeSizeInTargetMag(); const tracingStoreHost = yield* select((state) => state.tracing.tracingStore.url); const mag = resolutionInfo.getResolutionByIndexOrThrow(zoomStep); @@ -347,12 +358,12 @@ function* loadFullAdHocMesh( // Segment stats can only be used for volume tracings that have a segment index // and that don't have editable mappings. - const usePositionsFromSegmentStats = + const usePositionsFromSegmentIndex = volumeTracing?.hasSegmentIndex && !volumeTracing.mappingIsEditable && visibleSegmentationLayer?.tracingId != null; - let positionsToRequest = usePositionsFromSegmentStats - ? yield* getChunkPositionsFromSegmentStats( + let positionsToRequest = usePositionsFromSegmentIndex + ? yield* getChunkPositionsFromSegmentIndex( tracingStoreHost, layer, segmentId, @@ -384,13 +395,13 @@ function* loadFullAdHocMesh( isInitialRequest, removeExistingMesh && isInitialRequest, useDataStore, - !usePositionsFromSegmentStats, + !usePositionsFromSegmentIndex, ); isInitialRequest = false; // If we are using the positions from the segment index, the backend will // send an empty neighbors array, as it's not necessary to have them. - if (usePositionsFromSegmentStats && neighbors.length > 0) { + if (usePositionsFromSegmentIndex && neighbors.length > 0) { throw new Error("Retrieved neighbor positions even though these were not requested."); } positionsToRequest = positionsToRequest.concat(neighbors); @@ -399,7 +410,7 @@ function* loadFullAdHocMesh( yield* put(finishedLoadingMeshAction(layer.name, segmentId)); } -function* getChunkPositionsFromSegmentStats( +function* getChunkPositionsFromSegmentIndex( tracingStoreHost: string, layer: DataLayer, segmentId: number, @@ -408,7 +419,7 @@ function* getChunkPositionsFromSegmentStats( clippedPosition: Vector3, additionalCoordinates: AdditionalCoordinate[] | null | undefined, ) { - const unscaledPositions = yield* call( + const targetMagPositions = yield* call( getBucketPositionsForAdHocMesh, tracingStoreHost, layer.name, @@ -417,8 +428,8 @@ function* getChunkPositionsFromSegmentStats( mag, additionalCoordinates, ); - const positions = unscaledPositions.map((pos) => V3.scale3(pos, mag)); - return sortByDistanceTo(positions, clippedPosition) as Vector3[]; + const mag1Positions = targetMagPositions.map((pos) => V3.scale3(pos, mag)); + return sortByDistanceTo(mag1Positions, clippedPosition) as Vector3[]; } function hasMeshChunkExceededThrottleLimit(segmentId: number): boolean { @@ -472,7 +483,7 @@ function* maybeLoadMeshChunk( const { segmentMeshController } = getSceneController(); - const cubeSize = getZoomedCubeSize(zoomStep, resolutionInfo); + const cubeSize = marchingCubeSizeInTargetMag(); while (retryCount < MAX_RETRY_COUNT) { try { @@ -505,7 +516,9 @@ function* maybeLoadMeshChunk( layer.name, additionalCoordinates, ); - return neighbors.map((neighbor) => getNeighborPosition(clippedPosition, neighbor)); + return neighbors.map((neighbor) => + getNeighborPosition(clippedPosition, neighbor, zoomStep, resolutionInfo), + ); } catch (exception) { retryCount++; ErrorHandling.notify(exception as Error); diff --git a/webknossos-datastore/app/com/scalableminds/webknossos/datastore/models/datasource/DataLayer.scala b/webknossos-datastore/app/com/scalableminds/webknossos/datastore/models/datasource/DataLayer.scala index 506758277f9..485edcd529b 100644 --- a/webknossos-datastore/app/com/scalableminds/webknossos/datastore/models/datasource/DataLayer.scala +++ b/webknossos-datastore/app/com/scalableminds/webknossos/datastore/models/datasource/DataLayer.scala @@ -228,6 +228,7 @@ object DataLayer { * Defines the length of a bucket per axis. This is the minimal size that can be loaded from a wkw file. */ val bucketLength: Int = 32 + val bucketSize: Vec3Int = Vec3Int(bucketLength, bucketLength, bucketLength) implicit object dataLayerFormat extends Format[DataLayer] { override def reads(json: JsValue): JsResult[DataLayer] = diff --git a/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/controllers/VolumeTracingController.scala b/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/controllers/VolumeTracingController.scala index 5950cec292f..734986b268d 100644 --- a/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/controllers/VolumeTracingController.scala +++ b/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/controllers/VolumeTracingController.scala @@ -563,6 +563,7 @@ class VolumeTracingController @Inject()( fallbackLayer <- tracingService.getFallbackLayer(tracingId) tracing <- tracingService.find(tracingId) ?~> Messages("tracing.notFound") mappingName <- tracingService.baseMappingName(tracing) + _ <- bool2Fox(DataLayer.bucketSize <= request.body.cubeSize) ?~> "cubeSize must be at least one bucket (32³)" bucketPositionsRaw: ListOfVec3IntProto <- volumeSegmentIndexService .getSegmentToBucketIndexWithEmptyFallbackWithoutBuffer( fallbackLayer,