diff --git a/frontend/javascripts/admin/rest_api.ts b/frontend/javascripts/admin/rest_api.ts index 86223a90ee..8faf6af665 100644 --- a/frontend/javascripts/admin/rest_api.ts +++ b/frontend/javascripts/admin/rest_api.ts @@ -1,5 +1,4 @@ import dayjs from "dayjs"; -import { V3 } from "libs/mjs"; import type { RequestOptions, RequestOptionsWithData } from "libs/request"; import Request from "libs/request"; import type { Message } from "libs/toast"; @@ -1965,7 +1964,7 @@ export function computeAdHocMesh( // is added here to the position and bbox size. position: positionWithPadding, // position is in mag1 additionalCoordinates, - cubeSize: V3.toArray(V3.add(cubeSize, [1, 1, 1])), //cubeSize is in target mag + cubeSize, // cubeSize is in target mag // Name and type of mapping to apply before building mesh (optional) mapping: mappingName, voxelSizeFactorInUnit: scaleFactor, diff --git a/frontend/javascripts/viewer/model/bucket_data_handling/bounding_box.ts b/frontend/javascripts/viewer/model/bucket_data_handling/bounding_box.ts index a0ef16f2c0..24c4c01ed5 100644 --- a/frontend/javascripts/viewer/model/bucket_data_handling/bounding_box.ts +++ b/frontend/javascripts/viewer/model/bucket_data_handling/bounding_box.ts @@ -103,10 +103,6 @@ class BoundingBox { }; } - clipPositionIntoBoundingBox(position: Vector3): Vector3 { - return V3.toArray(V3.max(this.min, V3.min(position, this.max))); - } - extend(other: BoundingBox): BoundingBox { const newMin = V3.min(this.min, other.min); const newMax = V3.max(this.max, other.max); 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 74847f20d9..1292054ce3 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 @@ -192,6 +192,7 @@ function* loadAdHocMeshFromAction(action: LoadAdHocMeshAction): Saga { ); } catch (exc) { Toast.error(`The mesh for segment ${action.segmentId} could not be loaded. Please try again.`); + console.log("Exception when loading ad-hoc mesh for segment", action.segmentId, ":", exc); ErrorHandling.notify(exc as any); } } @@ -469,11 +470,23 @@ function* maybeLoadMeshChunk( const additionalCoordinates = yield* select((state) => state.flycam.additionalCoordinates); const threeDMap = getOrAddMapForSegment(layer.name, segmentId, additionalCoordinates); const mag = magInfo.getMagByIndexOrThrow(zoomStep); - const paddedPosition = V3.toArray(V3.sub(clippedPosition, mag)); - const paddedPositionWithinLayer = - layer.cube.boundingBox.clipPositionIntoBoundingBox(paddedPosition); - if (threeDMap.get(paddedPositionWithinLayer)) { + /* + Both cube position and cubeSize are padded. This is to achieve two effects: + (1) An overlap of 1vx (target mag) is added between cubes to fill visible gaps in the + mesh chunk grid. + (2) Cubes directly at dataset layer borders should actually extend by 1vx (target mag) + *beyond* the layer border so that marchingCubes can add closing surfaces for segments + that touch the layer borders. + To achieve both, all positions are moved by 1vx towards topleft and all sizes increased by 1. + Additionally, cubes that touch a lower layer border are increased by 1 again in that direction. + Note that this process can result in negative positions at the topleft layer border. + This is expected and the backend will handle it, adding the closing surface. + */ + const paddedPosition = V3.sub(clippedPosition, mag); + const paddedCubeSize = getPaddedCubeSizeInTargetMag(paddedPosition, mag, layer); + + if (threeDMap.get(paddedPosition)) { return []; } @@ -482,7 +495,7 @@ function* maybeLoadMeshChunk( } batchCounterPerSegment[segmentId]++; - threeDMap.set(paddedPositionWithinLayer, true); + threeDMap.set(paddedPosition, true); const scaleFactor = yield* select((state) => state.dataset.dataSource.scale.factor); if (isInitialRequest) { @@ -495,8 +508,6 @@ function* maybeLoadMeshChunk( const { segmentMeshController } = getSceneController(); - const cubeSize = marchingCubeSizeInTargetMag(); - while (retryCount < MAX_RETRY_COUNT) { try { const { buffer: responseBuffer, neighbors } = yield* call( @@ -506,11 +517,11 @@ function* maybeLoadMeshChunk( }, layerSourceInfo, { - positionWithPadding: paddedPositionWithinLayer, + positionWithPadding: paddedPosition, additionalCoordinates: additionalCoordinates || undefined, mag, segmentId, - cubeSize, + cubeSize: paddedCubeSize, scaleFactor, findNeighbors, ...meshExtraInfo, @@ -550,6 +561,29 @@ function* maybeLoadMeshChunk( return []; } +function getPaddedCubeSizeInTargetMag( + paddedPosition: Vector3, + mag: Vector3, + layer: DataLayer, +): Vector3 { + let cubeSize = marchingCubeSizeInTargetMag(); + + // Always increase cubeSize by 1,1,1 to fill grid gaps + cubeSize = V3.add(cubeSize, [1, 1, 1]); + + // If a cube precisely touches a lower layer border, increase its size in that direction + // by 1 again, to provide a closing surface on that layer border. + // Note that the paddedPosition already takes care of the upper layer borders + const cubeBottomRight = V3.add(paddedPosition, V3.scale3(cubeSize, mag)); + const layerBottomRight = layer.cube.boundingBox.max; + for (let dimension = 0; dimension < 3; dimension++) { + if (cubeBottomRight[dimension] === layerBottomRight[dimension]) { + cubeSize[dimension] += 1; + } + } + return cubeSize; +} + function* markEditedCellAsDirty(): Saga { const volumeTracing = yield* select((state) => getActiveSegmentationTracing(state)); diff --git a/unreleased_changes/9143.md b/unreleased_changes/9143.md new file mode 100644 index 0000000000..da59c6aee3 --- /dev/null +++ b/unreleased_changes/9143.md @@ -0,0 +1,2 @@ +### Fixed +- Fixed that some ad-hoc meshes that touch the dataset layer edges wouldn’t have closing edges there. diff --git a/webknossos-datastore/app/com/scalableminds/webknossos/datastore/services/BinaryDataService.scala b/webknossos-datastore/app/com/scalableminds/webknossos/datastore/services/BinaryDataService.scala index 21fd55abed..6dc353e75b 100644 --- a/webknossos-datastore/app/com/scalableminds/webknossos/datastore/services/BinaryDataService.scala +++ b/webknossos-datastore/app/com/scalableminds/webknossos/datastore/services/BinaryDataService.scala @@ -216,13 +216,13 @@ class BinaryDataService(val dataBaseDir: Path, rs.reverse.foreach { case (bucket, data) => - val xMin = math.max(cuboid.topLeft.voxelXInMag, bucket.topLeft.voxelXInMag) - val yMin = math.max(cuboid.topLeft.voxelYInMag, bucket.topLeft.voxelYInMag) - val zMin = math.max(cuboid.topLeft.voxelZInMag, bucket.topLeft.voxelZInMag) + val xMin = math.max(0, math.max(cuboid.topLeft.voxelXInMag, bucket.topLeft.voxelXInMag)) + val yMin = math.max(0, math.max(cuboid.topLeft.voxelYInMag, bucket.topLeft.voxelYInMag)) + val zMin = math.max(0, math.max(cuboid.topLeft.voxelZInMag, bucket.topLeft.voxelZInMag)) - val xMax = math.min(cuboid.bottomRight.voxelXInMag, bucket.topLeft.voxelXInMag + bucketLength) - val yMax = math.min(cuboid.bottomRight.voxelYInMag, bucket.topLeft.voxelYInMag + bucketLength) - val zMax = math.min(cuboid.bottomRight.voxelZInMag, bucket.topLeft.voxelZInMag + bucketLength) + val xMax = math.max(0, math.min(cuboid.bottomRight.voxelXInMag, bucket.topLeft.voxelXInMag + bucketLength)) + val yMax = math.max(0, math.min(cuboid.bottomRight.voxelYInMag, bucket.topLeft.voxelYInMag + bucketLength)) + val zMax = math.max(0, math.min(cuboid.bottomRight.voxelZInMag, bucket.topLeft.voxelZInMag + bucketLength)) for { z <- zMin until zMax