Skip to content

Commit df72b85

Browse files
authored
Fix ad-hoc mesh closing edges on layer borders (#9143)
Ad-hoc meshing showed some open edges - with segment index: at layer topleft borders and layer bottomright borders - without segment index: only at layer topleft borders The topleft problem was caused because the backend didn’t like negative requested coordinates, leading to crash fixes which shifted the toplefts into the layer bbox. This PR fixes this in the backend and undoes this change in the frontend, so negative coordinates are again sent, giving a clean upper edge. The bottomright problem was caused because the segment index doesn’t indicate anything below the layer bbox. The neighbors case would add a cube outside of the layer, providing the outer edge, but the segment index case would skip that. Now the frontend embiggens the cubes to request if they align with the bottomright edges. ### URL of deployed dev instance (used for testing): - https://adhocmeshedges.webknossos.xyz ### Steps to test: - Request some ad-hoc meshes that touch the layer borders with and without segment index - test also with a dataset that starts at 0,0,0 so you can test that the backend now handles the negative-coordinate requests. l4_sample doesn’t test this. ### Issues: - fixes #9069 ------ - [x] Added changelog entry (create a `$PR_NUMBER.md` file in `unreleased_changes` or use `./tools/create-changelog-entry.py`) - [x] Removed dev-only changes like prints and application.conf edits - [x] Considered [common edge cases](../blob/master/.github/common_edge_cases.md) - [x] Needs datastore update after deployment
1 parent eed62c4 commit df72b85

File tree

5 files changed

+52
-21
lines changed

5 files changed

+52
-21
lines changed

frontend/javascripts/admin/rest_api.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import dayjs from "dayjs";
2-
import { V3 } from "libs/mjs";
32
import type { RequestOptions, RequestOptionsWithData } from "libs/request";
43
import Request from "libs/request";
54
import type { Message } from "libs/toast";
@@ -1965,7 +1964,7 @@ export function computeAdHocMesh(
19651964
// is added here to the position and bbox size.
19661965
position: positionWithPadding, // position is in mag1
19671966
additionalCoordinates,
1968-
cubeSize: V3.toArray(V3.add(cubeSize, [1, 1, 1])), //cubeSize is in target mag
1967+
cubeSize, // cubeSize is in target mag
19691968
// Name and type of mapping to apply before building mesh (optional)
19701969
mapping: mappingName,
19711970
voxelSizeFactorInUnit: scaleFactor,

frontend/javascripts/viewer/model/bucket_data_handling/bounding_box.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -103,10 +103,6 @@ class BoundingBox {
103103
};
104104
}
105105

106-
clipPositionIntoBoundingBox(position: Vector3): Vector3 {
107-
return V3.toArray(V3.max(this.min, V3.min(position, this.max)));
108-
}
109-
110106
extend(other: BoundingBox): BoundingBox {
111107
const newMin = V3.min(this.min, other.min);
112108
const newMax = V3.max(this.max, other.max);

frontend/javascripts/viewer/model/sagas/meshes/ad_hoc_mesh_saga.ts

Lines changed: 43 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,7 @@ function* loadAdHocMeshFromAction(action: LoadAdHocMeshAction): Saga<void> {
192192
);
193193
} catch (exc) {
194194
Toast.error(`The mesh for segment ${action.segmentId} could not be loaded. Please try again.`);
195+
console.log("Exception when loading ad-hoc mesh for segment", action.segmentId, ":", exc);
195196
ErrorHandling.notify(exc as any);
196197
}
197198
}
@@ -469,11 +470,23 @@ function* maybeLoadMeshChunk(
469470
const additionalCoordinates = yield* select((state) => state.flycam.additionalCoordinates);
470471
const threeDMap = getOrAddMapForSegment(layer.name, segmentId, additionalCoordinates);
471472
const mag = magInfo.getMagByIndexOrThrow(zoomStep);
472-
const paddedPosition = V3.toArray(V3.sub(clippedPosition, mag));
473-
const paddedPositionWithinLayer =
474-
layer.cube.boundingBox.clipPositionIntoBoundingBox(paddedPosition);
475473

476-
if (threeDMap.get(paddedPositionWithinLayer)) {
474+
/*
475+
Both cube position and cubeSize are padded. This is to achieve two effects:
476+
(1) An overlap of 1vx (target mag) is added between cubes to fill visible gaps in the
477+
mesh chunk grid.
478+
(2) Cubes directly at dataset layer borders should actually extend by 1vx (target mag)
479+
*beyond* the layer border so that marchingCubes can add closing surfaces for segments
480+
that touch the layer borders.
481+
To achieve both, all positions are moved by 1vx towards topleft and all sizes increased by 1.
482+
Additionally, cubes that touch a lower layer border are increased by 1 again in that direction.
483+
Note that this process can result in negative positions at the topleft layer border.
484+
This is expected and the backend will handle it, adding the closing surface.
485+
*/
486+
const paddedPosition = V3.sub(clippedPosition, mag);
487+
const paddedCubeSize = getPaddedCubeSizeInTargetMag(paddedPosition, mag, layer);
488+
489+
if (threeDMap.get(paddedPosition)) {
477490
return [];
478491
}
479492

@@ -482,7 +495,7 @@ function* maybeLoadMeshChunk(
482495
}
483496

484497
batchCounterPerSegment[segmentId]++;
485-
threeDMap.set(paddedPositionWithinLayer, true);
498+
threeDMap.set(paddedPosition, true);
486499
const scaleFactor = yield* select((state) => state.dataset.dataSource.scale.factor);
487500

488501
if (isInitialRequest) {
@@ -495,8 +508,6 @@ function* maybeLoadMeshChunk(
495508

496509
const { segmentMeshController } = getSceneController();
497510

498-
const cubeSize = marchingCubeSizeInTargetMag();
499-
500511
while (retryCount < MAX_RETRY_COUNT) {
501512
try {
502513
const { buffer: responseBuffer, neighbors } = yield* call(
@@ -506,11 +517,11 @@ function* maybeLoadMeshChunk(
506517
},
507518
layerSourceInfo,
508519
{
509-
positionWithPadding: paddedPositionWithinLayer,
520+
positionWithPadding: paddedPosition,
510521
additionalCoordinates: additionalCoordinates || undefined,
511522
mag,
512523
segmentId,
513-
cubeSize,
524+
cubeSize: paddedCubeSize,
514525
scaleFactor,
515526
findNeighbors,
516527
...meshExtraInfo,
@@ -550,6 +561,29 @@ function* maybeLoadMeshChunk(
550561
return [];
551562
}
552563

564+
function getPaddedCubeSizeInTargetMag(
565+
paddedPosition: Vector3,
566+
mag: Vector3,
567+
layer: DataLayer,
568+
): Vector3 {
569+
let cubeSize = marchingCubeSizeInTargetMag();
570+
571+
// Always increase cubeSize by 1,1,1 to fill grid gaps
572+
cubeSize = V3.add(cubeSize, [1, 1, 1]);
573+
574+
// If a cube precisely touches a lower layer border, increase its size in that direction
575+
// by 1 again, to provide a closing surface on that layer border.
576+
// Note that the paddedPosition already takes care of the upper layer borders
577+
const cubeBottomRight = V3.add(paddedPosition, V3.scale3(cubeSize, mag));
578+
const layerBottomRight = layer.cube.boundingBox.max;
579+
for (let dimension = 0; dimension < 3; dimension++) {
580+
if (cubeBottomRight[dimension] === layerBottomRight[dimension]) {
581+
cubeSize[dimension] += 1;
582+
}
583+
}
584+
return cubeSize;
585+
}
586+
553587
function* markEditedCellAsDirty(): Saga<void> {
554588
const volumeTracing = yield* select((state) => getActiveSegmentationTracing(state));
555589

unreleased_changes/9143.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
### Fixed
2+
- Fixed that some ad-hoc meshes that touch the dataset layer edges wouldn’t have closing edges there.

webknossos-datastore/app/com/scalableminds/webknossos/datastore/services/BinaryDataService.scala

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -216,13 +216,13 @@ class BinaryDataService(val dataBaseDir: Path,
216216

217217
rs.reverse.foreach {
218218
case (bucket, data) =>
219-
val xMin = math.max(cuboid.topLeft.voxelXInMag, bucket.topLeft.voxelXInMag)
220-
val yMin = math.max(cuboid.topLeft.voxelYInMag, bucket.topLeft.voxelYInMag)
221-
val zMin = math.max(cuboid.topLeft.voxelZInMag, bucket.topLeft.voxelZInMag)
219+
val xMin = math.max(0, math.max(cuboid.topLeft.voxelXInMag, bucket.topLeft.voxelXInMag))
220+
val yMin = math.max(0, math.max(cuboid.topLeft.voxelYInMag, bucket.topLeft.voxelYInMag))
221+
val zMin = math.max(0, math.max(cuboid.topLeft.voxelZInMag, bucket.topLeft.voxelZInMag))
222222

223-
val xMax = math.min(cuboid.bottomRight.voxelXInMag, bucket.topLeft.voxelXInMag + bucketLength)
224-
val yMax = math.min(cuboid.bottomRight.voxelYInMag, bucket.topLeft.voxelYInMag + bucketLength)
225-
val zMax = math.min(cuboid.bottomRight.voxelZInMag, bucket.topLeft.voxelZInMag + bucketLength)
223+
val xMax = math.max(0, math.min(cuboid.bottomRight.voxelXInMag, bucket.topLeft.voxelXInMag + bucketLength))
224+
val yMax = math.max(0, math.min(cuboid.bottomRight.voxelYInMag, bucket.topLeft.voxelYInMag + bucketLength))
225+
val zMax = math.max(0, math.min(cuboid.bottomRight.voxelZInMag, bucket.topLeft.voxelZInMag + bucketLength))
226226

227227
for {
228228
z <- zMin until zMax

0 commit comments

Comments
 (0)